ぐっちゃんのチェックポイント

身近なことからキワドイとこまで、幅広い領域を担当しています

Flutterで「ウマ娘」の画面を再現してみた

前置

最近、広告で気になった漫画を買った。


「しょせん他人事ですから~とある弁護士の本音の仕事~」
って漫画だ。
ちなみに3巻買った

いや~~~法律って奥が深いんだね。


"弁護士"って聞くと 異議ありで有名な逆転裁判みたいなことを考えちゃう私だけど、
そんな法知識に疎い人でもすんなりと内容が入ってくるんよ。


というのも話の内容がネットトラブルなんよね。


スマホがないと生きていけないサイボーグみたいな現代人にとっては当たり前に存在するインターネット。
普段使っている掲示板や配信、SNS等身近に存在する内容があるので、
法についての理解がしやすい!

おススメの漫画ですよ。


こんにちは。
ご無沙汰してます、pLotsです。


今回はFlutterのレイアウトの勉強を兼ねてウマ娘の画面を再現してみたよ。

この記事では、
・ページや画面の遷移の方法を学ぶ
・ボタンの配置や画面のレイアウトの整え方を学ぶ
を目的としてやっていくゾ


今回のゴール設定だが、
・4つの画面を下のボタンで自由に画面遷移できる
・画像をボタンにする
・画面遷移するときにスライドして移動できる
にした。

各画面の画像
ホーム画面

ストーリー画面

レース画面

強化編成画面


なお、ボタンに使う写真はいったんパワーポイントに貼り付けて、
背景を削除したものを画像として使用した。

本編

準備するもの

Flutter

リンクページを貼っておきます
flutter.dev

Android Studio

リンクページを貼っておきます
developer.android.com

ディレクトリ構造

.
└── lib
    ├── asset
     | └── image
    ├── kyoka.dart
    ├── main.dart
    ├── story.dart
    └── race.dart

簡単に内容説明
lib・・・・・Dartファイルと画像フォルダ
kyoka.dart・・・強化編成画面(画面はボタンと背景のみ)
main.dart ・・・ホーム画面 (画面はボタンと背景、キャラクターを配置)
story.dart ・・・ストーリー画面(画面はボタンと背景のみ)
race.dart ・・・レース選択画面(画面はボタンと背景のみ)

コード

main.dart

import 'package:flutter/material.dart';
import 'package:untitled/kyoka.dart';
import 'package:untitled/story.dart';
import 'package:untitled/race.dart';

void main()  => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}):super (key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          //背景の画像
          image: DecorationImage(
              image: AssetImage("lib/asset/image/back/background_home.png"),
            fit: BoxFit.fill,
          ),
        ),
        child: Container(
          decoration: const BoxDecoration(
            image: DecorationImage(
              image: AssetImage("lib/asset/image/mihono3.png"),
            ),
          ),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Padding(
                padding: const EdgeInsets.all(12.5),

                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children:  [
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return KyokaPage();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/kyokahensei.png")),),

                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return StoryPage();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(1.0, 0.0); // 右から左
                              // final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/story.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => MyApp()),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/home.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return RacePage();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(1.0, 0.0); // 右から左
                              // final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/race.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/gacha.png")),),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}


kyoka.dart

import 'package:flutter/material.dart';
import 'package:untitled/story.dart';
import 'package:untitled/race.dart';
import 'package:untitled/main.dart';

class KyokaPage extends StatelessWidget {
  const KyokaPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          //背景の画像
          image: DecorationImage(
            image: AssetImage("lib/asset/image/back/background_kyoka.png"),
            fit: BoxFit.fill,
          ),
        ),
        child: Container(
             child: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Padding(
                padding: const EdgeInsets.all(12.5),

                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children:  [
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => KyokaPage()),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/kyokahensei.png")),),

                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return StoryPage();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(1.0, 0.0); // 右から左
                              // final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/story.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return MyApp();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(1.0, 0.0); // 右から左
                             final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/home.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return RacePage();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(1.0, 0.0); // 右から左
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/race.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/gacha.png")),),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}


story.dart

import 'package:flutter/material.dart';
import 'package:untitled/kyoka.dart';
import 'package:untitled/race.dart';
import 'package:untitled/main.dart';

class StoryPage extends StatelessWidget {
  const StoryPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          //背景の画像
          image: DecorationImage(
            image: AssetImage("lib/asset/image/back/background_story.png"),
            fit: BoxFit.fill,
          ),
        ),
        child: Container(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Padding(
                padding: const EdgeInsets.all(12.5),

                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children:  [
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return KyokaPage();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              //final Offset begin = Offset(1.0, 0.0); // 右から左
                              final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/kyokahensei.png")),),

                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => StoryPage()),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/story.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return MyApp();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(1.0, 0.0); // 右から左
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/home.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return RacePage();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(1.0, 0.0); // 右から左
                              // final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/race.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/gacha.png")),),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

race.dart

import 'package:flutter/material.dart';
import 'package:untitled/kyoka.dart';
import 'package:untitled/story.dart';
import 'package:untitled/main.dart';

class RacePage extends StatelessWidget {
  const RacePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      //appBar: AppBar(
      //title: const Text("プロッターのテスト"),
      //),
      body: Container(
        decoration: const BoxDecoration(
          //背景の画像
          image: DecorationImage(
            image: AssetImage("lib/asset/image/back/background_race.png"),
            fit: BoxFit.fill,
          ),
        ),
        child: Container(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Padding(
                padding: const EdgeInsets.all(12.5),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children:  [
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return KyokaPage();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/kyokahensei.png")),),

                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return StoryPage();
                            },
                            transitionsBuilder: (context, animation, secon
                              final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/story.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return MyApp();
                            },
                            transitionsBuilder: (context, animation, secondaryAnimation, child) {
                              final Offset begin = Offset(-1.0, 0.0); // 左から右
                              final Offset end = Offset.zero;
                              final Animatable<Offset> tween = Tween(begin: begin, end: end)
                                  .chain(CurveTween(curve: Curves.easeInOut));
                              final Animation<Offset> offsetAnimation = animation.drive(tween);
                              return SlideTransition(
                                position: offsetAnimation,
                                child: child,
                              );
                            },
                          ),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/home.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                        Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => RacePage()),
                        );
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/race.png")),),
                    GestureDetector(
                      onTap: (){
                        //タップ時のイベントを記述
                      },
                      child: Image(image: AssetImage("lib/asset/image/icons/gacha.png")),),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

注意点

やってみるうえで引っかかったところです。

ローカルにある写真を持っていきたい場合、
pubspec.yamlファイルのuses-material-design: trueの下に
「asset: (改行) - lib/asset/image/~~~.png」と追加しなければ使えないです
例)
uses-material-design: true
assets:
- lib/asset/image/back/background_home.png
- lib/asset/image/back/background_kyoka.png

インターネットから引っ張ってくる場合ならymlファイルは弄らなくて大丈夫です


・実行する際、「task assembleDebug...」ですごい時間かかる
仮想端末(私はPixel5にした)で動かすとかなり重い
バグではない模様

・一応VScodeでも動かせるけど、、、、
体感Android Studioよりもカクカク。

動かしてみた

実行した際の動画をYoutubeに載せました。
youtube.com

終わりに

FlutterってHTMLみたいにポンポン内容を追加できて自由に編集できるのかぁ、、、
って思ったけど、いやめちゃめちゃむずない??

正直私自身も完全に理解しきれてないところある。

調べてみたけど構成図はこうなってるんですって

一日二日じゃマジで分からん。
自分で手を動かして学んでいく必要があるなぁ


役に立ったURL:
画面遷移アニメーション | Flutterで始めるアプリ開発