実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

Flutterで自在に枠を描画・Container

はじめに

本記事ではFlutterのContainerウィジェットについてポイントを述べていきます。

Containerウィジェットは囲みを作るためのパーツです。スペースなどを調整するレイアウト系の働きもありますが、丸角の枠を作ったり背景色を塗ったりという目に見える側面もあります。丁度HTMLのdivタグで作る枠のようなものです。

なので必須とはいいませんが、そこそこかっこいいUIデザインをしていく上では欠かせないものになります。





Container

Containerウィジェットは囲みを作るレイアウト用の要素です。指定された大きさの囲みを作る、背景をペイントする、隙間を作るなどの用途で使います。

まずは色々試すためのベースのコードを作ります。試してみるためにはFlutterの新しいプロジェクトを作って、main.dartを下記の内容に置き換えてみてください:

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    //(1) 文字列出力に必要
    return Directionality(
        textDirection: TextDirection.ltr,

        //(2) 要素を縦に並べる
        child:Column(
            children: [
               //(3) Containerウィジェットの例
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.amber
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.blueGrey
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.deepOrange
              ),
            ]
        )

    );
  }
}

このコードの出力結果は下のようになります:

f:id:linkedsort:20211014234250p:plain:w300

コードのポイント

コードのコメントに付けた番号ごとにポイントを述べていきます。

(1) 文字列出力に必要

最初にDirectionalityで囲んでいるのは前回記事と同様、Textウィジェットを使用するためです。TextDirection.ltrによって左から右の横書きを指定しています。

サンプルの便宜上置いているだけです。Containerを理解する上では、あまり気にされなくてOKです。

(2) 要素を縦に並べる

前回使用したColumnウィジェットを挟んでいます。これによってchildrenで与えたウィジェットのリストの要素を縦に並べて表示します。

(3) Contaierウィジェットの例

Columnのchildrenとして3つのContainerをリストの要素として与えています。

Containerの使い方は単純で、高さや幅や色などを指定すると、そのサイズ・形状・色の枠ができるというものです。

ベースのコードでは各Containerに高さ100・幅200のサイズ、色、そして中身としてTextウィジェットを与えています。

主なContainerの属性値は以下の通りです:

属性 意味
child 中身の子ウィジェット
height 高さ
width
alignment 中身の位置
padding 内側の余白
margin 外側の余白
transform 回転など
decoration 影やグラデーションの装飾

以下の節ではContainerに与える属性値をいろいろと変えて、出力の変化を見ていきます。各ンプルのコードは、上記のベースコードのうちColumnウィジェット以下の部分のみを示していきます。


中身の配置

Containerのalignment属性は、そのContainerの中身(child)に指定したウィジェットがどの位置に置かれるかを指定します。

前節のベースコードではalignmentを指定していませんので、Textは全て左上に寄せられていますね。

これを下記のように変えてみます:

        child:Column(
            children: [
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  alignment: Alignment.center,
                  color: Colors.amber
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  alignment: Alignment.bottomLeft,
                  color: Colors.blueGrey
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  alignment: Alignment.topRight,
                  color: Colors.deepOrange
              ),
            ]
        )

これを実行した結果は以下になります:

f:id:linkedsort:20211014235314p:plain:w300

文字の表示位置がそれぞれど真ん中、左下、右上になっていますね。

alignmentの値と位置の関係は以下の通りです:

alignment 位置
Alignment.topLeft 左上
Alignment.topCenter 真ん中上
Alignment.topRight 右上
Alignment.centerLeft 左中央
Alignment.center 真ん中
Alignment.centerRight 右中央
Alignment.bottomLeft 左下
Alignment.bottomCenter 真ん中下
Alignment.bottomRight 右下

 

内側の隙間

Containerの内側に余白を作るにはpadding属性に値を与えていきます。丁度HTML/CSSの余白と同じです。

paddingの値はEdgeInsets型をとります。EdgeInsetsは上下左右の端(エッジ)に関する数値をまとめるクラスで、下記の表のようにいくつかの名前付きコンストラクタを持っています。こういうと複雑そうですが、よく見ると単純です:

コンストラクタ パラメタ
EdgeInsets.all double型のパラメタをとり、上下左右が同一の数値の場合に使用
EdgeInsets.symmetric verticalが上下、horizontalが左右の数値を指定。上下・左右の組でそれぞれ同一の数値の場合に使用
EdgeInsets.only top、left、right、bottomでそれぞれ上左右下のそれぞれの数値をバラバラに設定する場合に使用

次に使用例を示します:

        child:Column(
            children: [
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.amber,
                  padding:const EdgeInsets.all(30),
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.blueGrey,
                  padding:const EdgeInsets.symmetric(vertical:10, horizontal: 20),
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.deepOrange,
                  padding:const EdgeInsets.only(top: 40, left: 60),
              ),
            ]
        )

これを実行した結果は以下になります:

f:id:linkedsort:20211016020142p:plain:w300

一番上の箱は上下左右とも30、真ん中の箱では上下が10・左右が20、一番下の箱では上が40、左が60の設定でそれぞれ余白を入れています。


外側の隙間

Containerの外側に余白を作るにはmargin属性に値を与えていきます。marginの値はpaddingと同様、EdgeInsets型をとります。

下記コードではpaddingで示したコードのpaddingを全部marginに入れ替えてみました:

        child:Column(
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.amber,
                  margin:const EdgeInsets.all(30),
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.blueGrey,
                  margin:const EdgeInsets.symmetric(vertical:10, horizontal: 20),
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  color: Colors.deepOrange,
                  margin:const EdgeInsets.only(top: 40, left: 60),
              ),
            ]
        )

これを実行した結果は以下になります:

f:id:linkedsort:20211016014029p:plain:w300

それぞれ隙間が空いていますね。一番上の箱の余白は全辺30になっていて、二番目の左右が20なのに横軸が揃っているのは、今回の場合縦に1つしかウィジェットがなく余白を含めて中央揃えになっているためです。左右対称なので横に余白があるのですが違いが見えません。

それに比べて一番下の箱は左の余白が60、右が0で差があるので箱の表示は中央からずれています。


丸角

decoration属性にBoxDecorationインスタンスを与えることで丸角など様々にContainer枠の形を変えることができます。

ここでは一番使われそうな丸角を試してみます:

        child:Column(
            children: [
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: Colors.amber,
                    borderRadius: BorderRadius.circular(10),
                  ),
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: Colors.blueGrey,
                    borderRadius: BorderRadius.circular(20),
                  ),
              ),
              Container(
                  child: const Text('hello'),
                  height:100,
                  width:200,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: Colors.deepOrange,
                    borderRadius: BorderRadius.circular(30),
                  ),
              ),
            ]
        )

これを実行した結果は以下になります:

f:id:linkedsort:20211015000903p:plain:w300

丸角の丸みの半径を10、20、30と徐々に増やしています。


回転

(注:この節は高校数学に馴染みがないと理解が難しい上にあまりContainerの回転は使わないと思いますので、スキップして次節のグラデーションに進んで問題ないです)

Containerのtransform属性は形状を変形させるためのものです。変形は線形代数的な座標変換を任意の4次元行列を与えることで指定します。

とはいえ任意の変形が必要な場面はほとんどないと思いますので、ここでは回転に特化したMatrix4.rotationX/Y/Zを与える例を見ていきます。

Matrix4.rotationX/Y/Z(=名前付きコンストラクタ)は下記の表にある通りの回転を指定する行列になります:

コンストラクタ 行列の作用
Matrix4.rotationX(double r) rラジアン分縦回転
Matrix4.rotationY(double r) rラジアン分横回転
Matrix4.rotationZ(double r) rラジアン分回転

縦回転、横回転の意味が伝わりにくいと思いますので、下記のサンプルコードと結果を参照してみてください:

        child:Column(
            children: [
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  color: Colors.amber,
                  borderRadius: BorderRadius.circular(15),
                ),
                transform: Matrix4.rotationX(0.7),
              ),
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  color: Colors.blueGrey,
                  borderRadius: BorderRadius.circular(15),
                ),
                transform: Matrix4.rotationY(0.7),
              ),
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  color: Colors.deepOrange,
                  borderRadius: BorderRadius.circular(15),
                ),
                transform: Matrix4.rotationZ(0.7),
              ),
            ]
        )

これを実行した結果は以下になります:

f:id:linkedsort:20211016021820p:plain:w300

この結果から、縦と横の回転の意味合いは伝わるかと思います。この縦横回転も使う場面が限られるかと思います。

ラジアンについて馴染みがない場合は、以下の数式で変換できるので覚えておくと良いでしょう。一回転360°の角度表記をaとするとラジアンは下記の数式で変換できます:

a / 180 * pi

ここで「pi」は円周率\piを表します。これを使用するためには「import 'dart:math'」を冒頭に書いてdartのmathライブラリをインポートしておく必要があります。


グラデーション

Containerにグラデーションをつけるには丸角のところでも利用したdecoration属性にBoxDecorationの値を指定します。

BoxDecorationのgradient属性値に下記表のクラスの値を与えることで様々な種類のグラデーションを表現できます:

gradient値 意味
LinearGradient 線形グラデーション。開始点から終了点に向かって色が徐々に変化
RadialGradient 中央の点から始まって外側に向かって色が変化
SweepGradient 中心の点を軸にぐるっと回転するように色が変化

これも下記サンプルコードとその出力結果を見比べていただくと意味が伝わると思います:

        child:Column(
            children: [
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(15),
                  gradient: const LinearGradient(
                    begin: Alignment.topRight,
                    end: Alignment.bottomLeft,
                    colors: [
                      Colors.lightGreenAccent,
                      Colors.amber
                    ]
                  ),
                ),
              ),
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(15),
                  gradient: const RadialGradient(
                      colors: [
                        Colors.grey,
                        Colors.blueGrey,
                      ]
                  ),
                ),
              ),
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(15),
                  gradient: const SweepGradient(
                      colors: [
                        Colors.amberAccent,
                        Colors.deepOrange,
                        Colors.amberAccent,
                      ]
                  ),
                ),
              ),
            ]
        )

これを実行した結果は以下になります:

f:id:linkedsort:20211016023618p:plain:w300

色は2つ以上、複数の色を与えることができます。同じ色を適度に混ぜることで色の変化の割合を調整したりもできます。3番めの例では3色の色を与えて、最初と最後の色を同一にすることで、一周回転してきたときに同じ色でスムーズにつながるようにしています。


影をつけるにも丸角やグラデーションと同様、decoration属性を指定します。decoration属性に与えるBoxDecoration型の値のなかでboxShadow属性を指定します。

boxShadowの値は影の情報を表現するBoxShadow型で与えます。BoxShadow型の属性値は主に下記の通りです:

属性値 意味
color 影の色。灰色であればColors.greyなどで指定
spreadRadius 影の広がり半径。数値を大きくすると影が大きくなります
blurRadius 影のぼかし半径。数値を大きくするとぼやけ方が増します
offset Offset(x, y)で影のずれ方、位置を指定します。数値を大きくするほど右・下にずれていきます。マイナスの数値で逆に上がります

これらも言葉の説明よりもコードと結果の対応を見ていただくと雰囲気が伝わるかと思います。

ここでは丸角とグラデーションも合わせて複合的に効果を入れる例にしていますのですこしコードが長いですが、特にshadowの部分に注目してみてください:

        child:Column(
            children: [
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                margin: const EdgeInsets.all(20),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(15),
                  gradient: const LinearGradient(
                    begin: Alignment.topRight,
                    end: Alignment.bottomLeft,
                    colors: [
                      Colors.lightGreenAccent,
                      Colors.amber
                    ]
                  ),
                  boxShadow: const [
                    BoxShadow(
                      color: Colors.grey,
                      spreadRadius: 1,
                      blurRadius: 5,
                      offset: Offset(4,4)
                    )
                  ]
                ),
              ),
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                margin: const EdgeInsets.all(20),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(15),
                  gradient: const LinearGradient(
                    begin: Alignment.topRight,
                    end: Alignment.bottomLeft,
                    colors: [
                      Colors.blue,
                      Colors.blueGrey
                    ]
                  ),
                  boxShadow: const [
                    BoxShadow(
                      color: Colors.grey,
                      spreadRadius: 5,
                      blurRadius: 5,
                      offset: Offset(4,4)
                    )
                  ]
                ),
              ),
              Container(
                child: const Text('hello'),
                height:100,
                width:200,
                alignment: Alignment.center,
                margin: const EdgeInsets.all(20),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(15),
                  gradient: const LinearGradient(
                      begin: Alignment.topRight,
                      end: Alignment.bottomLeft,
                      colors: [
                        Colors.amberAccent,
                        Colors.deepOrange,
                      ]
                  ),
                  boxShadow: const [
                    BoxShadow(
                      color: Colors.grey,
                      spreadRadius: 1,
                      blurRadius: 20,
                      offset: Offset(4,4)
                    )
                  ]
                ),
              ),
            ]
        )

これを実行した結果は以下になります:

f:id:linkedsort:20211016025818p:plain:w300

1番上はスタンダードな影、真ん中は濃い目に拡げた影、下はほわっとぶらした影です。色々と数値を変えて試してみてください。

お好みの影は見つかりましたでしょうか。





おわりに

Containerの例を色々とみてきました。最後の方の丸角やシャドウを駆使するとクールなUIが作れそうですよね。

そして記述の雰囲気はかなりHTML/CSSと似ています。似ていますが、非常に表現力の高いプログラミング言語でありますし、カチッと構文が決まっていて歴史的な経緯から非常にゆるい作りの言語になっているHTMLよりも安心感があります。

今現在Googleが進めているWebをはじめとするFlutterのマルチプラットフォーム化や、次世代OSのことを考え合わせると、将来的にはHTML/CSS/javascriptの位置に取って代わるものとしてFlutterが直接ブラウザ上でレンダリングされる将来も見えてくるのでは、と思えてきます。

HTML/CSS/javascriptの世界は歴史的に積み上がった技術的負債も多く、今後一気にダイナミックなコンテンツの需要が増えたりするときに、耐えられないのではないかと思いますが、いかがでしょうか。耐えられないならどうするか、という議論になったとき、一番取って代わる可能性がいま高いのはFlutterではないかなと思います。 

これはまた将来への含みということで、推移を見守りましょう。

f:id:linkedsort:20211107174431j:plain