Flutterを使ってみよう その9(レイアウト方法③)

前回の続きで複数の子ウィジェットをレイアウトする場合の説明とそれらを使った例などに触れていきたい思います。

以前までに触れてきた下記のウィジェットも複数の子ウィジェットをレイアウトするウィジェットです。


 ・Row :水平整列時に使うウィジェット

 ・Column :垂直整列時に使うウィジェット

 ・ListView :リスト表示時に使うウィジェット

 ・GridView :グリッド表示時に使うウィジェット

以降は、その他でも有用なレイアウトウィジェットについて触れておきたいと思います。

1. Stack ウィジェット

Stack ウィジェットは、複数の子ウィジェットを重ねるときに使用するレイアウトウィジェットです。

SafeArea(
  child: Align(
    alignment: Alignment.topCenter,
    child: Stack(
      alignment: AlignmentDirectional.center,
      children: [
        Container(
          width: 300,
          height: 300,
          color: Colors.blue,
        ),
        Container(
          width: 200,
          height: 200,
          color: Colors.red,
        ),
        Container(
          width: 100,
          height: 100,
          color: Colors.green,
        )
      ],
    ),
  ),
),



コード例だと、 Stack ウィジェットを使用して、幅300×高さ300の青の領域の上に、幅200×高さ200の赤の領域の上に、幅100×高さ100の緑の領域で表示しますように指定しています。

子ウィジェットの表示位置は、引数 alignment の指定に従います。(引数内容については、省略します)

ウィジェットを重ねるという点では、 Overlay という方法があり、その場合は自由な位置に一時的に表示するときなどに使用します。

2. Overlay について

Overlay は、独立して管理できるエントリのスタックで、あるウィジェットを自由な位置に表示したい場合に使用する仕組みです。

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  OverlayEntry? _overlayEntry;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text(
          'Overlay',
        ),
      ),
      child: SafeArea(
        child: Container(
          alignment: Alignment.topCenter,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              IconButton(
                icon: const Icon(Icons.alarm),
                onPressed: () {
                  _openMenu();
                },
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _openMenu() {
    _overlayEntry = OverlayEntry(
      builder: (context) {
        return Stack(
          children: [
            Positioned.fill(
              child: GestureDetector(
                onTap: _closeMenu,
                child: Container(color: Colors.black.withAlpha(128)),
              ),
            ),
            const Center(
              child: SizedBox(
                width: 200,
                height: 200,
                child: Stack(alignment: AlignmentDirectional.center, children: [
                  SizedBox.expand(
                    child: ColoredBox(
                      color: Colors.blue,
                    ),
                  ),
                  Text('Overlay Box')
                ]),
              ),
            ),
          ],
        );
      },
    );
    Overlay.of(context).insert(_overlayEntry!);
  }

  void _closeMenu() {
    _overlayEntry?.remove();
    _overlayEntry = null;
  }
}


コード例では、アラームアイコンのボタンをタップすると、 Overlay を利用し、下記のことを行い、青の領域のウィジェットを表示しています。


1.OverlayEntry クラスを作って、オーバーレイ用のエントリ作成(子ウィジェット作成)

2.Overlay.of で insert メソッドで、生成した OverlayEntry を追加(子ウィジェットを表示)


また、青の領域のウィジェット表示中、背面タップすると、表示中の OverlayEntry に対して、 remove メソッドを呼び出すことで、青の領域のウィジェットを削除しています。

3. Wrap ウィジェット

Wrap ウィジェットは、複数の子ウィジェットを表示する際、超過するウィジェットは、次の行へ折り返して表示するときに使うレイアウトウィジェットです。

SafeArea(
  child: Container(
    width: double.infinity,
    height: 300,
    color: Colors.grey,
    child: Wrap(
      alignment: WrapAlignment.center,
      runAlignment: WrapAlignment.center,
      crossAxisAlignment: WrapCrossAlignment.center,
      spacing: 8,
      runSpacing: 4,
      children: [
        Container(
          width: 100,
          height: 150,
          color: Colors.blue,
        ),
        Container(
          width: 50,
          height: 50,
          color: Colors.red,
        ),
        Container(
          height: 80,
          width: 80,
          color: Colors.orange,
          child: const Center(child: Text('Box')),
        ),
        const Text(
          'テキスト',
          style: TextStyle(
              color: Colors.white, backgroundColor: Colors.brown),
        ),
        Container(
          width: 50,
          height: 30,
          color: Colors.yellow,
        ),
        Container(
          width: 100,
          height: 10,
          color: Colors.green,
        ),
        Container(
          width: 80,
          height: 20,
          color: Colors.pink,
        ),
      ],
    ),
  ),
),


コード例だと、 Wrap ウィジェットを使用して、複数の子ウィジェットを並べ、子ウィジェットの表示領域がなくなった場合、次の行へ折り返し表示しています。

子ウィジェットは、引数 spacing の幅で並んでおり、行折り返した場合、引数 runSpacing の高さで並べます。


引数 alignment は、1行の子ウェジェット郡に配置位置を指定しており、コードだと、 WrapAlignment.center 指定のため、2行目の子ウェジェット郡(緑の領域と、ピンクの領域)は、中央寄せになっています。

引数 runAlignment は、全体の子ウェジェット郡に配置位置を指定しており、コードだと、 WrapAlignment.center 指定のため、全体の子ウェジェット郡は、中央寄せになっています。

引数 crossAxisAlignment は、行における子ウェジェットの配置位置を指定しており、コードだと、 WrapCrossAlignment.center 指定のため、各子ウィジェットは、行においての中央寄せになっています。

4. LayoutBuilder ウィジェット

LayoutBuilder ウィジェットは、親ウィジェットのサイズに依存するウィジェットを構築する場合などで利用します。

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

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text(
          'LayoutBuilder ',
        ),
      ),
      child: SafeArea(
        child: Container(
          width: double.infinity,
          height: double.infinity,
          color: Colors.grey,
          child: LayoutBuilder(
            builder: (context, constraints) {
              Widget content;
              if (constraints.maxWidth < 400) {
                content = _buildContainer1();
              } else {
                content = _buildContainer2();
              }
              return Center(child: content);
            },
          ),
        ),
      ),
    );
  }

  Widget _buildContainer1() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Container(
          height: 50.0,
          width: 50.0,
          color: Colors.blue,
        ),
        Container(
          height: 100.0,
          width: 100.0,
          color: Colors.green,
        ),
      ],
    );
  }

  Widget _buildContainer2() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Container(
          height: 80.0,
          width: 80.0,
          color: Colors.red,
        ),
        Container(
          height: 100.0,
          width: 100.0,
          color: Colors.yellow,
        ),
      ],
    );
  }
}


コード例だと、 LayoutBuilder ウィジェットを使用して、 builder 関数で渡される context 、 constraints の内容を見て、ウィジェット内容を切り替えてます。


context には、 BuildContext クラスで、ウィジェット自身がどこに配置されているかを知るための要素が入力され、 constraints には、 BoxConstraints クラスで、ウィジェット自身の幅・高さ制約が入力されます。

最大幅( constraints.maxWidth )が400未満の場合は、 _buildContainer1() ウィジェット内容(青領域と緑領域が縦並び)を、それ以外の場合、_buildContainer2() ウィジェット内容(赤領域と黄領域が横並び)が表示されます。

5. Flexible、Expanded、Spacer ウィジェット

Flexible 、 Expanded 、 Spacer ウィジェットは、 Column 、 Row ウィジェットの子ウィジェットとして扱えるレイアウトウィジェットで、比率によって、子ウィジェットを埋める役割を補います。

Flexible ウィジェットは、可変な領域を設定する場合に利用します。

Expanded ウィジェットは、固定な領域を設定する場合に利用します。


また、 Flexible の引数 fit に FlexFit.tight 設定した場合と同じ内容になります。

Spacer ウィジェットは、固定の空白エリアを作ります。

各ウィジェットは、引数 flex に従い、比率を決定します。

SafeArea(
  child: Container(
    width: double.infinity,
    height: double.infinity,
    color: Colors.grey,
    child: Column(
      children: [
        Expanded(
          flex: 5,
          child: Container(
            padding: const EdgeInsets.all(15),
            width: double.infinity,
            color: Colors.blue,
            child: Container(
              color: Colors.blueGrey,
              child: const Center(
                child: Text('Top Title'),
              ),
            ),
          ),
        ),
        const Spacer(
          flex: 5,
        ),
        Flexible(
          flex: 10,
          child: Container(
            padding: const EdgeInsets.all(15),
            width: double.infinity,
            color: Colors.green,
            child: Row(
              children: [
                Expanded(
                  flex: 10,
                  child: Container(
                    color: Colors.lightGreen,
                    child: const Center(
                      child: Text('Row1-1'),
                    ),
                  ),
                ),
                const Flexible(
                  flex: 20,
                  child: ColoredBox(color: Colors.black),
                ),
                Expanded(
                  flex: 10,
                  child: Container(
                    color: Colors.red,
                    child: const Center(
                      child: Text('Row1-2'),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
        Flexible(
          flex: 8,
          child: Container(
            padding: const EdgeInsets.all(15),
            width: double.infinity,
            color: Colors.purple,
            child: Row(
              children: [
                Expanded(
                  flex: 10,
                  child: Container(
                    color: Colors.yellow,
                    child: const Center(
                      child: Text('Row2-1'),
                    ),
                  ),
                ),
                const Flexible(
                  flex: 20,
                  child: ColoredBox(
                    color: Colors.pink,
                    child: Center(
                      child: Text('Row2-2'),
                    ),
                  ),
                ),
                Expanded(
                  flex: 10,
                  child: Container(
                    color: Colors.orange,
                    child: const Center(
                      child: Text('Row2-3'),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  ),
),


コード例だと、 Column ウィジェットを使用

6. まとめ

今回は、複数の子ウィジェットをレイアウトの知識を深めるのを目的に記事を書きました。

次回は、他の知識を深める予定です。