システム開発部のTです。
前回、Flutterの勉強ということで、電卓を作ることを決めて、画面テーマとしてダークテーマの実装までやりましたが、ついに画面レイアウトを作り込んでいきたいと思います。

前回の記事「Flutterでアプリ開発・画面UIのテーマを決める」からのつづきです。

とりあえず完成画面

最初に完成版の画面とコードを提示します。
左がAndroid、右がiPhoneの画面になります。

以降では、上記のレイアウトの実装になるまでの内容を記事にしています。

代表的なWidget

本件でレイアウトするために扱う代表的なWidgetは以下となります。

  • Container
  • Column
  • Row
  • Stack
  • Center

Container

幅や高さ、マージン、パディング、Container内の装飾などの設定ができるWidgetです。
基本的に、このWidgetを大枠として、子のWidgetを置いたりします。

たとえば、横300dp、縦400dpのContainer内にTextを一つ置きます。
さらに、Containerに8dpの赤枠、バックグラウンドカラーに青を設定する場合、以下のように書きます。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        width: 300,
        height: 400,
        decoration: BoxDecoration(
            color: Colors.blue,
            border: Border.all(
                color: Colors.red,
                width: 8.0)
        ),
        child: Text('x:300 y:400',
          style: TextStyle(fontSize: 20),
        ),
      ),
    );
  }

以下の画像のように表示されます。

さて、Containerについて理解していただけたかと思いますが、
今回の計算機のデザインをもう一度見ていただければと思いますが、
大きく分けて結果を表示するResultエリア、テンキーを設置するテンキーエリアと上下に別れていますね。

上記のように上にResultエリア用のContainer、下にテンキー用のContainerを並べる必要がありますね。
これを可能にするのが、次のWidgetになります。

Column

このWidgetは自身の子のWidgetを縦に並べることを可能としたWidgetです。
また、子のWidgetを複数配置できるWidgetの一つになります。
Containerという枠でも、子のWidgetは一つしか配置できないんですよね。
この縦に並べるColumn、横に並べるRow、立体的に並べるStackを使いこなすことで、UIデザインを作っていくことになります。

まずは使い方。
親のContainerの中にColumnを設置し、そのColumnの中にContainerを2つ縦に配置し、その中にTextを入れて表示してみようと思います。

上記を実装したコードがこちら。

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final padding = MediaQuery.of(context).padding;
    var maxHeight = size.height - padding.top - padding.bottom;

    // アプリ描画エリアの縦サイズを取得
    if (Platform.isAndroid) {
      maxHeight = size.height - padding.top - kToolbarHeight;
    } else if (Platform.isIOS) {
      maxHeight = size.height - padding.top - padding.bottom - 22;
    }

    // Resultエリアの縦サイズ
    final resultAreaHeight = maxHeight * (30 / 100);
    // テンキーエリアの縦サイズ
    final tenkeyAreaHeight = maxHeight * (70 / 100);

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        child: Column(
          children: <Widget>[
            Container(
              decoration: BoxDecoration(color: Colors.blueGrey, border: Border.all(color: Colors.red)),
              width: size.width,
              height: resultAreaHeight,
              child: Text('Resultエリア', style: TextStyle(fontSize: 40),),
            ),
            Container(
              decoration: BoxDecoration(color: Colors.black, border: Border.all(color: Colors.blue)),
              width: size.width,
              height: tenkeyAreaHeight,
              child: Text('テンキーエリア', style: TextStyle(fontSize: 40),),
            ),
          ],
        ),
      ),
    );
  }

実際の画面はこちら。
左がiPhone、右がAndroidになります。

いかがでしょうか。
今回、2つに並べたContainerに高さ値Heightを設定しました。
画面サイズを取得し、縦サイズの30%をResultエリアの高さに設定し、
もう一方のテンキーエリアには画面サイズの70%を高さに指定しています。
あとは、Containerのバックグラウンドに色を指定すれば、なんとなくですが、計算機のイメージに近づいたかと思います。

とりあえず、このままResultエリアのレイアウトを仕上げていこうと思います。
計算結果の値を縦中央の右寄せで表示し、四則演算文字をResultエリア最下部の左寄せに設置します。

右寄せで仕様するのが次のRowになります。

Row

このWidgetは自身の子のWidgetを横に並べることを可能としたWidgetです。
また、子のWidgetを複数配置できるWidgetの一つになります。
Columnとの違いは縦ではなく、横に並べるってこと。それ意外の使いみちは同じです。
また、本Widgetを利用することで文字列の位置を横中央、右寄せという指定も可能にします。

本件の場合、計算結果の値を右寄せするために利用します。
実装例としては以下になります。

Container(
  decoration: BoxDecoration(color: Colors.blueGrey),
  width: MediaQuery.of(context).size.width, // 横幅最大
  height: MediaQuery.of(context).size.height * (30 / 100), // 最大高さの30%
  padding: EdgeInsets.only(right: 10.0),
  child: Column(
     mainAxisSize: MainAxisSize.max, // 親Containerの高さ最大まで広げる
     mainAxisAlignment: MainAxisAlignment.center, // 上記の高さの中央に設置
	children: <Widget>[
	  Row(
	    mainAxisSize: MainAxisSize.max, // 親Containerの幅最大まで広げる
	    mainAxisAlignment: MainAxisAlignment.end, // 上記の幅の右端に設置
	        children: <Widget>[
		    Text('1,000,000,000.000', style: TextStyle(fontSize: 40),),
		],
	    ),
	]
  ),
),

上記のように実装した場合の表示内容はこちら。

計算結果の値(ここでは固定値を設定)が縦中央、右寄せになりました。
この調子で四則演算の文字も、上記のエリアの左下に設置しましょう。

しかし、今のコードでは、四則演算を表示させるための余地が無いですよね。
理由として、ColumnにはAlingnmentにCenterを、Rowにはendを指定しているので、左下に設置するためにはResultエリアのContainerに計算結果の値を表示するWidgetとは別のWidgetを重ねて設置する必要があります。

この「重ねる」ために利用するのが次のStackになります。

Stack

このWidgetは自身の子のWidgetを重ねて設置することを可能としたWidgetです。また、子のWidgetを複数配置できるWidgetの一つになります。
ColumnがY軸、RowがX軸に対応すると考えると、StackはZ軸というイメージになります。

では、このStackを使って計算結果を表示しているWidgetの上に重ねる形で四則演算文字を設置してみたいと思います。

以下のコードを確認ください。

Container(
  decoration: BoxDecoration(color: Colors.blueGrey),
  width: MediaQuery.of(context).size.width,
  height: MediaQuery.of(context).size.height * (30 / 100),
  padding: EdgeInsets.only(right: 10.0),
  child: Stack(  // 親Container最上部をStackに変更
      children: <Widget>[
        Column(  // 親Container最上部にあったColumnをStackの配下に移動
           mainAxisSize: MainAxisSize.max, 
           mainAxisAlignment: MainAxisAlignment.center,
           children: <Widget>[
	      Row(
	        mainAxisSize: MainAxisSize.max,
	        mainAxisAlignment: MainAxisAlignment.end,
	        children: <Widget>[
		    Text('1,000,000,000.000', style: TextStyle(fontSize: 40),),
		]),
	  ]),
     ]),
),

上記のように実装後、一度実行してみてください。
多分、見た目の変化は無いかと思います。
ここから、今度はStack配下の計算結果表示Widgetに続いて、四則演算表示のWidgetを追加します。

追加後のコードはこちらです。

Container(
  decoration: BoxDecoration(color: Colors.blueGrey),
  width: MediaQuery.of(context).size.width,
  height: MediaQuery.of(context).size.height * (30 / 100),
  padding: EdgeInsets.only(right: 10.0),
  child: Stack(
	children: <Widget>[
		Column(  // 計算結果のWidget
			mainAxisSize: MainAxisSize.max,
			mainAxisAlignment: MainAxisAlignment.center,
			children: <Widget>[
				Row(
				mainAxisSize: MainAxisSize.max,
				mainAxisAlignment: MainAxisAlignment.end,
				children: <Widget>[
					Text('1,000,000,000.000', style: TextStyle(fontSize: 40),),
				]),
			]),
		Column( // 追加した四則演算表示Widget
			mainAxisSize: MainAxisSize.max,
			mainAxisAlignment: MainAxisAlignment.end,
			children: <Widget>[
			Row(
				children: <Widget>[
				Text('+-*/', style: TextStyle(fontSize: 40),),
				])
			])
	])
),

画面表示結果はこちら

うまく設置できましたね。
このColumn、Row、Stackの3種のWidgetを使ってレイアウトするのがFlutterでの基本となるかと思います。

以上で、Resultエリア内のレイアウトは完成となります。

Center

利用していないけど、Centerの説明をします。
プロジェクト作成時のサンプルコードでも利用されていたので、一応説明させてください。

このWidgetは、子のWidgetを自身の中央に配置します。
自身の親Widgetのエリアが大きい場合、縦横ともに中央となります。
本Widgetが配置できる子は一つなので、CenterにContainerを配置し、ポップアップ風の表示するときに便利かと思います。

一応、ColumnとRowを利用して同じように中央表示させることは可能ですが、こちらを利用することで、少ないコードで表現することが可能になります。

テンキーエリアの作成

Resultエリアも作ったし、最後にテンキーエリアの作成です。
っていっても、上記で説明したWidgetを利用することで簡単に作れてしまうことは理解していただけたかと思います。

とりあえず完成イメージを見てみましょう。

並べ方は単純で、Rowを利用して「C」、「CE」、「→」、「/」等の各ボタンを並べ、それらをColumnで縦に並べるだけですね。

ただ、縦列と横列を均等に並べる手段が必要になりますが、そのためのパラメータがあるので、見ていただければと思います。

child: Column(
	mainAxisSize: MainAxisSize.max,
	mainAxisAlignment: MainAxisAlignment.spaceEvenly,// これを設定
	children: <Widget>[
	Row(
		mainAxisSize: MainAxisSize.max,
		mainAxisAlignment: MainAxisAlignment.spaceEvenly,// これを設定
		children: <Widget>[
			FlatButton(

上記のように「MainAxisAlignment.spaceEvenly」を設定することで、均等に配置することが可能となります。

そして、本件で利用しているのがFlatButtonになります。
Button関連のWidgetについては、以下のサイトを参照ください。

https://flutter.dev/docs/development/ui/widgets/material#Buttons

本件では、以下のように並べています。

Row(
	mainAxisSize: MainAxisSize.max,
	mainAxisAlignment: MainAxisAlignment.spaceEvenly,
	children: <Widget>[
		FlatButton(
		child: Text("C"),
		onPressed: _onPressed,
		),
		FlatButton(
		child: Text("CE"),
		onPressed: _onPressed,
		),
		FlatButton(
		child: Text("→"),
		onPressed: _onPressed,
		),
		FlatButton(
		child: Text("/"),
		onPressed: _onPressed,
		),
	]
),

非常に単純なWidgetでchildに任意のWidget(本件ではTextWidget)、onPressedにボタン押下時のイベントハンドラメソッドを設定するだけです。
表示する子Widgetには制限なさそうなので、いろいろな表現の仕方ができそうですね。今回は文字のみにしています。

そして最終的なコードは以下になります。
(長いので非表示にしています)

main.dart(ここを押下でコード展開)
import 'dart:io';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter計算機',
theme: ThemeData.dark(),
home: MyHomePage(title: 'Flutter計算機'),
);
}
}

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

final String title;

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

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

void _onPressed() {
}

@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final padding = MediaQuery.of(context).padding;
var maxHeight = size.height - padding.top - padding.bottom;

if (Platform.isAndroid) {
maxHeight = size.height - padding.top - kToolbarHeight;
} else if (Platform.isIOS) {
maxHeight = size.height - padding.top - padding.bottom - 22;
}

final resultAreaHeight = maxHeight * (30 / 100);
final tenkeyAreaHeight = maxHeight * (70 / 100);

return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
child: Column(
children: <Widget>[
Container(
decoration: BoxDecoration(color: Colors.blueGrey),
width: size.width,
height: resultAreaHeight,
padding: EdgeInsets.only(right: 10.0),
child: Stack(
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text('1,000,000,000.000', style: TextStyle(fontSize: 40),),
],
),
]
),
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Row(
children: <Widget>[
Text('+-*/', style: TextStyle(fontSize: 40),),
],
)
],
)
],
)
),
Container(
decoration: BoxDecoration(color: Colors.black),
width: size.width,
height: tenkeyAreaHeight,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
child: Text("C"),
onPressed: _onPressed,
),
FlatButton(
child: Text("CE"),
onPressed: _onPressed,
),
FlatButton(
child: Text("→"),
onPressed: _onPressed,
),
FlatButton(
child: Text("/"),
onPressed: _onPressed,
),
]
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
child: Text("7"),
onPressed: _onPressed,
),
FlatButton(
child: Text("8"),
onPressed: _onPressed,
),
FlatButton(
child: Text("9"),
onPressed: _onPressed,
),
FlatButton(
child: Text("*"),
onPressed: _onPressed,
),
]
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
child: Text("4"),
onPressed: _onPressed,
),
FlatButton(
child: Text("5"),
onPressed: _onPressed,
),
FlatButton(
child: Text("6"),
onPressed: _onPressed,
),
FlatButton(
child: Text("ー"),
onPressed: _onPressed,
),
]
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
child: Text("1"),
onPressed: _onPressed,
),
FlatButton(
child: Text("2"),
onPressed: _onPressed,
),
FlatButton(
child: Text("3"),
onPressed: _onPressed,
),
FlatButton(
child: Text("+"),
onPressed: _onPressed,
),
]
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
FlatButton(
child: Text("0"),
onPressed: _onPressed,
),
FlatButton(
child: Text("00"),
onPressed: _onPressed,
),
FlatButton(
child: Text("."),
onPressed: _onPressed,
),
FlatButton(
child: Text("="),
onPressed: _onPressed,
),
]
),
],
),
),
],
),
),
);
}
}

まとめ

いかがでしたでしょうか。
今回はUIのWidgetを配置するための基本的なWidgetを説明していきました。
以下、簡単な復習です。

  • Container
    幅や高さ、Padding、Magine、装飾等ができるWidgetを格納する箱
  • Column
    縦にWedgetを並べる
  • Row
    横にWidgetを並べる
  • Stack
    Widgetを重ねる
  • Center
    自身の子のWidgetを中央に設置する

上記を利用することで、簡単なデザインであれば実現可能かと思います。
凝ったデザインになると、もう少し工夫するところもありますが、まずは上記を覚えていただければと思います。

次回はボタン押下時のイベントハンドラの実装に入りたいと思います。