Flutterで「ウマ娘」の画面を再現してみた
前置
最近、広告で気になった漫画を買った。
「しょせん他人事ですから~とある弁護士の本音の仕事~」
って漫画だ。
ちなみに3巻買った
いや~~~法律って奥が深いんだね。
"弁護士"って聞くと 異議あり!で有名な逆転裁判みたいなことを考えちゃう私だけど、
そんな法知識に疎い人でもすんなりと内容が入ってくるんよ。
というのも話の内容がネットトラブルなんよね。
スマホがないと生きていけないサイボーグみたいな現代人にとっては当たり前に存在するインターネット。
普段使っている掲示板や配信、SNS等身近に存在する内容があるので、
法についての理解がしやすい!
おススメの漫画ですよ。
こんにちは。
ご無沙汰してます、pLotsです。
今回はFlutterのレイアウトの勉強を兼ねてウマ娘の画面を再現してみたよ。
この記事では、
・ページや画面の遷移の方法を学ぶ
・ボタンの配置や画面のレイアウトの整え方を学ぶ
を目的としてやっていくゾ
今回のゴール設定だが、
・4つの画面を下のボタンで自由に画面遷移できる
・画像をボタンにする
・画面遷移するときにスライドして移動できる
にした。
各画面の画像
ホーム画面
ストーリー画面
レース画面
強化編成画面
なお、ボタンに使う写真はいったんパワーポイントに貼り付けて、
背景を削除したものを画像として使用した。
本編
ディレクトリ構造
. └── 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で始めるアプリ開発