リバース・エンジニアリング

Flultterとテックブログと時々iOS

60. FlutterのGridViewでメインのゲーム画面のデザインを作る

【目次】

メインのゲーム画面のデザイン GridView で作る

それでは今回からアプリのメイン機能であるゲーム画面を作って行きます(というか作りました)。
前回までのあらすじを読みたい方はこちらを読んでね!

blog.tamappe.com

メインの画面のデザインをおさらいします。

f:id:qed805:20200507232203p:plain
ゲーム画面

上下に部品を置いています。

上が現在の数字とタイマーで、下にゲーム用のボタンが1から25まで並んでいます。
実際にアプリで遊べる頃にはこの1から25の番号ボタンはランダムで出る想定です。でないと簡単すぎますw。

番号のボタンを GridView で作る

Flutter ではそれぞれの部品を細かく作ってから乗せるよりも先にざっくばらんにレイアウトを作ったほうがスムーズに開発できます。
まずはちなみにこのデザインを iOS で作るなら、多分 UICollectionView で DataSource と Delegate を使ってゴリゴリ書いていく気がします。
Flutter で UICollectionView に近い widget は GridView です。
これを使ってデザインを作っていきます。

盤面 GridView のソースコード

それでは実際にレイアウトを簡単に作ったものと GridView で下のデザインを作ったときのソースコードを載せていきます。

game_play_page.dart

import 'package:flutter/material.dart';
import 'package:twentyfive/utils/constants.dart';

class GamePlayPage extends StatefulWidget {
  @override
  _GamePlayPageState createState() => _GamePlayPageState();
}

class _GamePlayPageState extends State<GamePlayPage> {
  void _tappedNumberButton() {}

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[Text('a'), Text('b')],
              ),
              SizedBox(
                height: 48.0,
              ),
              SizedBox(
                height: 400.0,
                child: GridView.count(
                    mainAxisSpacing: 8,
                    crossAxisSpacing: 8,
                    physics: const NeverScrollableScrollPhysics(),
                    crossAxisCount: 5,
                    children: List.generate(25, (index) {
                      return NumberButton(index + 1, _tappedNumberButton);
                    })),
              )
            ],
          ),
        ),
      ),
    );
  }
}

今回は GridView.count を使って横幅に関係なく横に5つボタンを並べるようにしました。
ボタン一つのサイズを決める場合は GridView.extent を使えばいいのかなと。

GridView の中身はスクロールさせたくなかったので

physics: const NeverScrollableScrollPhysics()

と設定しています。

GridView の中にはあとで説明する NumberButton の widget を乗せています。

                   children: List.generate(25, (index) {
                      return NumberButton(index + 1, _tappedNumberButton);
                    })),

ボタンを縦 5 x 横 5 並べるために 25 と設定しています。

ボタン NumberButton のソースコード

次に NumberButton のソースコードを載せます。

新しいファイルは作成せずに GamePlayPage の下に定義しました。

game_play_page.dart

class NumberButton extends StatelessWidget {
  final int number;
  final Function onPressed;

  NumberButton(this.number, this.onPressed);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        border: Border.all(color: Colors.white, width: 2.0),
        borderRadius: BorderRadius.circular(10),
        color: Constants.orangeColor,
      ),
      child: FlatButton(
          child: Text(
            '$number',
            style: TextStyle(
              fontSize: 20.0,
              fontWeight: FontWeight.bold,
            ),
          ),
          onPressed: onPressed),
    );
  }
}

番号としての index とボタンをタップした時の関数 onPressed をプロパティとして定義しました。
ボタンは FlatButton を使って作成しています。

ここで iOS エンジニアに注目していただきたいのがソースコードの行数です。
iOS アプリ開発ではボタンに枠線と丸みを設定するのは色々調整が必要ですが、 Flutter ではほんの数行で定義することができるのです。

角丸・枠線の部分

     decoration: BoxDecoration(
        /// 枠線の色とサイズ
        border: Border.all(color: Colors.white, width: 2.0),
        /// 角丸の丸み
        borderRadius: BorderRadius.circular(10),
        /// ボタンの色
        color: Constants.orangeColor,
      ),

非常にシンプルで可読性がいいですね。定義を塊で見れるのも非常に good!

角丸・枠線の付け方自体は過去に記事にしています。

blog.tamappe.com

カウント画面からゲームプレイ画面への画面遷移

最後にプレイ画面への遷移についてはまだ定義していませんので補足しておきます。

画面遷移の定義は MaterialApp で宣言します。

main.dart

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: Constants.startRoute,
      routes: <String, WidgetBuilder>{
        Constants.startRoute: (BuildContext context) => StartPage(),
        /// プレイ画面への遷移
        Constants.playRoute: (BuildContext context) => GamePlayPage(),
      },
      title: 'Numbers',
      theme: new ThemeData(
        fontFamily: 'Arial',
        brightness: Brightness.dark,
        primaryColor: Colors.lightBlue[800],
        accentColor: Colors.cyan[600],
      ),
    );
  }
}

routes のところに "/play" のパスを追加しています。

とあるボタンをタップしてプレイ画面へ遷移させたい場合はボタンをタップした時の処理で

Navigator.pushReplacement(
          context,
          PageRouteBuilder(
            pageBuilder: (context, animation1, animation2) => GamePlayPage(),
          ),
        );

を入れてあげればOKです。
アニメーションはかけたくなかったので書いていません。

stackoverflow.com

これだけで画面遷移せずにゲームプレイ画面に飛ぶようになります。

これでビルドするとゲームプレイ画面が下のように表示されます。

f:id:qed805:20200527223952p:plain:w300
ボタンの盤面

まだまだブラッシュアップしていく必要がありますが、下の方のデザインが完成しました。

作ったあとの感想

改めて見返すと GridView はめちゃくちゃ便利すぎます。
Swift だと UICollectionView でゴリゴリ書いていくところたった数行で縦x横に item を並べるUIが出来上がりました。
このように Flutter は UI の開発のコスパが優れているんですよね。
しかも触っていく度に新しい発見があります。

今日はここまでにします。

それではバイバイ!