2020年05月10日

Staggered Grid ViewでSmartNewsみたいなレイアウトの実装方法をご紹介します。

概要

Flutter の GridView だと汎用性のあるグリッドは難しいのですが、Staggered Grid View パッケージを利用すれば柔軟なグリッドレイアウトをとることができます。ここでは SmartNews みたいなレイアウトの実装方法をご紹介いたします。



Staggered Grid View について

パケージの説明には A Flutter staggered grid view which supports multiple columns with rows of varying sizes. (意訳:複数の列をもつ行をサポートしています)とあります。
自由にグリッドタイルを組み合わせたレイアウトを可能にしてくれるとても素晴らしいパッケージです。
タイルという考え方が重要ですので、まずはここから説明を始めていきます。



タイルの考え方

Staggered Grid View を実装するにはタイルを定義する必要があります。 例えば、以下のようなレイアウトを実装するとします。

グリッドサンプル

このレイアウトは、列が 4、行が 2 のグリッドになっています。
上段は等分されており、1 列分が 4 つ、
下段は結合されており 2 列分が 2 つになっています。

Staggered Grid View ではこの定義を Staggered.Tile.count()メソッドを使って以下のように定義します。

タイルの定義
//* StaggeredTile.count(結合する列数、結合する行数)

static const List<StaggeredTile> _tiles = [
  //* 上段
  StaggeredTile.count(1, 1),
  StaggeredTile.count(1, 1),
  StaggeredTile.count(1, 1),
  StaggeredTile.count(1, 1),
  //* 下段
  StaggeredTile.count(2, 1),
  StaggeredTile.count(2, 1),
];

ではスマートニュースではどういったレイアウトになっているのでしょうか。

本来は複雑なレイアウトなのですが、ここでは単純化して、以下のようなレイアウトが繰り返されているものとして説明していきます。

スマートニュースの例

上図のレイアウトは、1 行目が 3 列、2 行目が 2 列、3 行目が 1 列となっています。
3 列と 2 列ではグリッドが組めませんが、こういった場合は最小公倍数の 6 を列数とすると、以下のようにタイルを定義することができます。

スマートニュースのグリッドサンプル

スマートニュースのタイル定義
static const List<StaggeredTile> _tiles = [
  //* 1行目
  StaggeredTile.count(2, 1),
  StaggeredTile.count(2, 1),
  StaggeredTile.count(2, 1),
  //* 2行目
  StaggeredTile.count(3, 1),
  StaggeredTile.count(3, 1),
  //* 3行目
  StaggeredTile.count(6, 1),
];

このタイルの定義ができれば、あとは各タイルの位置に対応したウィジェットを当てはめるだけです。 次はその Staggered Grid View のビルダーについて説明いたします。



StaggeredGridView ウィジェットとビルダー

StaggeredGridView ウィジェットには以下のビルダーが用意されています。

  • builder
  • custom
  • count
  • countBuilder
  • extent
  • extentBuilder

count か countBuilder が便利ですので、ここでは2つの説明をいたします。

count

アイテム数が決まっている場合に利用。ただし、一度に全て生成されるため、数が多くなった場合は countBuilder の利用を検討。

countBuilder

こちらもアイテム数が決まっている場合に利用。ただし、ビューに入ってきた時に生成されるため、アイテム数が多い場合(無限)でも対応できる。基本的にこちらの利用を推奨。
ここでは countBuilder を使っての実装を説明いたします。



countBuilder について

最小構成

最小構成は以下のようになります。
必須が crossAxisCount, itemBuilder, staggeredTitleBuilder の3つです。
itemBuilder の index に応じたタイルを staggeredTileBuilder で返す必要があります。

countBuilder
StaggeredGridView.countBuilder(
  //* 列数
  crossAxisCount: 6,
  //* アイテム
  itemBuilder: (BuilderContext context, int index) {
    return Text(index);
  },
  //* アイテムに対応したタイル
  staggeredTileBuilder: (int index) {
    return StaggeredTile.count(1, 1)
  }
)

オプション

基本的に ListView 等のオプションと同じです。 個人的には ListView をネストする場面も多いため、ScrollPyhsics と shrinkWrap にはお世話になっています。

countBuilderのオプション
StaggeredGridView.countBuilder({
  Axis scrollDirection: Axis.vertical,
  bool reverse: false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap: false,
  EdgeInsetsGeometry padding,
   int crossAxisCount,
   IndexedWidgetBuilder itemBuilder,
   IndexedStaggeredTileBuilder staggeredTileBuilder,
  int itemCount,
  double mainAxisSpacing: 0.0,
  double crossAxisSpacing: 0.0,
  bool addAutomaticKeepAlives: true,
  bool addRepaintBoundaries: true,
})


実装

以上を踏まえ、実際に実装したコードが以下です。

ファイル構成

ファイルは main.dart と、Staggered Grid View を実装した staggered _grid _view _demo.dart の 2 つです。

ファイル構成
lib/
├── main.dart
└── staggered_grid_view_demo.dart

コード

main.dart
import 'package:flutter/material.dart';
import 'staggered_grid_view_demo.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Staggered Grid View',
      home: StaggeredGridViewDemo(),
    );
  }
}

こちらが Staggered Grid View を利用して、スマートニュースの用なレイアウトを実装した本体です。

staggered_grid_view_demo.dart
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

class StaggeredGridViewDemo extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Staggered Grid View Demo'),
      ),
      body: _StaggeredGridView(),
    );
  }
}

class _StaggeredGridView extends StatelessWidget {
  const _StaggeredGridView({
    Key key,
  }) : super(key: key);

  /*
  * StaggeredTiles
  * 3,2,1列のレイアウトを1グループとして、それをループして表示する場合
  * 引数はdoubleなので、少数も指定できる
  ------------------------------------------*/
  static const List<StaggeredTile> _tiles = [
    StaggeredTile.count(2, 1),
    StaggeredTile.count(2, 1),
    StaggeredTile.count(2, 1),
    StaggeredTile.count(3, 1),
    StaggeredTile.count(3, 1),
    StaggeredTile.count(6, 1),
  ];

  
  Widget build(BuildContext context) {
    return Container(
      child: Padding(
        padding: EdgeInsets.all(5.0),
        child: StaggeredGridView.countBuilder(
          //* 最大列数。今回は3列と2列が共存しているため最小公倍数の6を設定する
          crossAxisCount: 6,

          //* アイテム数
          itemCount: 24,

          //* アイテムビルダー
          itemBuilder: (BuildContext context, int index) {
            return _ItemWidget(index: index);
          },

          //* タイルビルダー。同じindexのアイテムに対して、どのタイルを反映させるか。
          staggeredTileBuilder: (int index) {
            //* indexを_tilesの長さで割ったときのあまりが、該当するタイルのインデックスとなる。
            int _tileIndex = index % _tiles.length;
            return _tiles[_tileIndex];
          },

          //* スペーサー
          mainAxisSpacing: 5.0,
          crossAxisSpacing: 5.0,
        ),
      ),
    );
  }
}

class _ItemWidget extends StatelessWidget {
  final int index;
  _ItemWidget({Key key,  this.index})
      : assert(index != null),
        super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(5.0),
      color: Colors.black12,
      child: Text('Title $index'),
    );
  }
}


あとがき

Staggered Grid View は、標準の Grid View では難しい、柔軟なレイアウトを提供してくれます。 先日紹介したContainer Transformとの相性もいいので、ぜひ試していただければと思います。