Help us understand the problem. What is going on with this article?

Flutter - GridViewを少し柔軟にしたい#2

More than 1 year has passed since last update.

Flutter - GridViewを少し柔軟にしたい#1の続きです。
こういうレイアウトがしたいということで始めたGridViewの改造計画
img.png
今回で解決編です。

前回までにわかったこと

GridView > SliverGrid > SliverGridLayout > SliverGridDelegate
と長い旅を経て

  • SliverGridLayout
  • SliverGridDelegateWithBounds

の派生クラスを作ってGridViewに食わせれば画像のような領域をまたぐレイアウトが作れそう。
それでは派生クラスを作る前に元のコードをしっかり見ていきます。

SliverGridLayoutの派生クラスを作る

SliverGridLayoutの元コード

rendering/sliver_grid.dart
@immutable
abstract class SliverGridLayout {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const SliverGridLayout();

  /// The minimum child index that is visible at (or after) this scroll offset.
  int getMinChildIndexForScrollOffset(double scrollOffset);

  /// The maximum child index that is visible at (or before) this scroll offset.
  int getMaxChildIndexForScrollOffset(double scrollOffset);

  /// The size and position of the child with the given index.
  SliverGridGeometry getGeometryForChildIndex(int index);

  /// The scroll extent needed to fully display all the tiles if there are
  /// `childCount` children in total.
  ///
  /// The child count will never be null.
  double computeMaxScrollOffset(int childCount);
}

抽象クラスだったんですね。
これを継承して新しいSliverGridLayoutを作ればいいということになります。
実際の継承の例としてSliverGridLayoutを継承しているSliverGridRegularTileLayoutクラスを見てみましょう。

SliverGridRegularTileLayoutの元コード

SliverGridRegularTileLayout
class SliverGridRegularTileLayout extends SliverGridLayout {

  const SliverGridRegularTileLayout({
    @required this.crossAxisCount,
    @required this.mainAxisStride,
    @required this.crossAxisStride,
    @required this.childMainAxisExtent,
    @required this.childCrossAxisExtent,
    @required this.reverseCrossAxis,
  }) : assert(crossAxisCount != null && crossAxisCount > 0),
       assert(mainAxisStride != null && mainAxisStride >= 0),
       assert(crossAxisStride != null && crossAxisStride >= 0),
       assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
       assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0),
       assert(reverseCrossAxis != null);

  final int crossAxisCount;

  final double mainAxisStride;

  final double crossAxisStride;

  final double childMainAxisExtent;

  final double childCrossAxisExtent;

  final bool reverseCrossAxis;

  @override
  int getMinChildIndexForScrollOffset(double scrollOffset) {
    return mainAxisStride > 0.0 ? crossAxisCount * (scrollOffset ~/ mainAxisStride) : 0;
  }

  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset) {

    if (mainAxisStride > 0.0) {
      final int mainAxisCount = (scrollOffset / mainAxisStride).ceil();
      return math.max(0, crossAxisCount * mainAxisCount - 1);
    }
    return 0;
  }

  double _getOffsetFromStartInCrossAxis(double crossAxisStart) {
    if (reverseCrossAxis)
      return crossAxisCount * crossAxisStride - crossAxisStart - childCrossAxisExtent;
    return crossAxisStart;
  }

  @override
  SliverGridGeometry getGeometryForChildIndex(int index) {
    final double crossAxisStart = (index % crossAxisCount) * crossAxisStride;
    return SliverGridGeometry(
      scrollOffset: (index ~/ crossAxisCount) * mainAxisStride,
      crossAxisOffset: _getOffsetFromStartInCrossAxis(crossAxisStart),
      mainAxisExtent: childMainAxisExtent,
      crossAxisExtent: childCrossAxisExtent,
    );
  }

  @override
  double computeMaxScrollOffset(int childCount) {
    assert(childCount != null);
    final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
    final double mainAxisSpacing = mainAxisStride - childMainAxisExtent;
    return mainAxisStride * mainAxisCount - mainAxisSpacing;
  }
}

コンストラクタで渡されている値たちを簡単に言うと、

  • crossAxisCount = スクロール方向に垂直な方向のWidget数
  • mainAxisStride = Widgetに与えられた余白込みのスクロール方向の長さ
  • crossAxisStride = Widgetに与えられた余白込みのスクロール方向に垂直な方向の長さ
  • childMainAxisExtent = Widgetに与えられた描画領域のスクロール方向の長さ
  • childCrossAxisExtent = Widgetに与えられた余白込みのスクロール方向に垂直な方向の長さ
  • reverseCrossAxis = Widgetを(左あるいは上)から並べるならfalse、反対からならtrueを指定。

SliverGridのpreformLayout()で使用されていた超重要メソッドはこの2つでしたね。

dart
  @override
  int getMinChildIndexForScrollOffset(double scrollOffset) {
    return mainAxisStride > 0.0 ? crossAxisCount * (scrollOffset ~/ mainAxisStride) : 0;
  }

  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset) {

    if (mainAxisStride > 0.0) {
      final int mainAxisCount = (scrollOffset / mainAxisStride).ceil();
      return math.max(0, crossAxisCount * mainAxisCount - 1);
    }
    return 0;
  }

現在のスクロール時点で描画すべきWidgetの最初と最後のindexを取得するメソッドです。
最初と最後がわかれば、Widgetは順番に並んでいるから描画すべきものを全て取得することができるだろうという算段のようです。

gridview.jpg

しかし、今回作りたいものは下の画像のようなレイアウト。
そうなると、描画する最初のアイテムは9番。最後のアイテムは28番が正解となります。

newgridview.jpg

getMinChildIndexForScrollOffsetで8(indexは9-1)が取得されるように、
getMaxChildIndexForScrollOffsetで27が取得されるようにする必要があります。

これを解消する方法はただ一つ!
1番目のWidgetは、上から1番目、左から1番目、横幅は2つ分、縦幅は1つ分の領域に置かれる
2番目のWidgetは、上から1番目、左から3番目、横幅は1つ分、縦幅は1つ分の領域に置かれる
3番目のWidgetは、上から1番目、左から4番目、横幅は1つ分、縦幅は2つ分の領域に置かれる
4番目のWidgetは、上から2番目、左から1番目、横幅は3つ分、縦幅は2つ分の領域に置かれる
・・・のように全部のWidgetの置かれる座標とサイズをあらかじめ知っておくことです。

新たなSliverGridLayoutを作る

座標とサイズを格納するGridBoundsクラスを作る

まずは上記の座標とサイズを格納するクラスを作ります。GridBoundsという名前にしましょうか。

GridBounds
@immutable
class GridBounds {
  GridBounds(
    this.mainAxisPosition,
    this.crossAxisPosition,
    this.mainAxisExtent,
    this.crossAxisExtent
  ):  assert(mainAxisPosition != null && mainAxisPosition >= 0),
      assert(crossAxisPosition != null && crossAxisPosition >= 0),
      assert(mainAxisExtent != null && mainAxisExtent >= 0),
      assert(crossAxisExtent != null && crossAxisExtent >= 0);

  // 座標のスクロール軸
  final int mainAxisPosition;

  // 座標のスクロールの垂直軸
  final int crossAxisPosition;

  // Widgetの下端の座標
  int get trailingMainAxisPosition {
    return mainAxisPosition + mainAxisExtent;
  }

  // Widgetの右端の座標
  int get trailingCrossAxisPosition {
    return crossAxisPosition + crossAxisExtent;
  }

  // スクロール方向の大きさ
  final int mainAxisExtent;

  // スクロールと垂直な方向の大きさ
  final int crossAxisExtent;
}

GridBoundsの集合を管理するクラスを作る

この座標と大きさの情報は一つだけあっても何も意味をなさないので、
これの集合を管理するものを作ります。

GridBoundsList
class GridBoundsList {
  GridBoundsList(this.boundsList): assert(boundsList != null);

  /// [GridBounds]の配列。GridViewの全てのレイアウトが入っている。
  List<GridBounds> boundsList;

  /// 座標から合致する[GridBounds]を[boundsList]の中から探し、そのIndexを返す。
  /// 合致しない場合は-1を返す。
  int getIndexAtPosition(int _mainAxisPosition, int _crossAxisPosition) {
    assert(boundsList != null);
    assert(_mainAxisPosition != null && _mainAxisPosition >= 0);
    assert(_crossAxisPosition != null && _crossAxisPosition >= 0);
    for (int i = 0, len = boundsList.length; i < len; i++) {
      GridBounds bounds = boundsList[i];
      if (bounds.mainAxisPosition <= _mainAxisPosition &&
            _mainAxisPosition < bounds.trailingMainAxisPosition &&
              bounds.crossAxisPosition <= _crossAxisPosition &&
                _crossAxisPosition < bounds.trailingCrossAxisPosition) return i;
    }
    return -1;
  }

  /// [index]から[boundsList]の中にある[GridBounds]を返す。
  GridBounds getBoundsAtIndex(int index){
    assert(boundsList != null);
    assert(index != null && index >= 0);
    return (0 <= index && index < boundsList.length) ? boundsList[index] : null;
  }

  /// 全ての[GridBounds]から算出された、スクロール方向の領域数を算出する。
  int get mainAxisCount {
    assert(boundsList != null);
    int maxCount = 0;
    for (int i = 0, length = this.boundsList.length; i < length; i++) {
      final GridBounds bounds = this.boundsList[i];
      maxCount = math.max(maxCount, bounds.mainAxisPosition + bounds.mainAxisExtent).toInt();
    }
    return maxCount;
  }

  /// 全ての[GridBounds]から算出された、スクロールと垂直の方向の領域数を算出する。
  int get crossAxisCount {
    assert(boundsList != null);
    int maxCount = 0;
    for (int i = 0, length = this.boundsList.length; i < length; i++) {
      final GridBounds bounds = this.boundsList[i];
      maxCount = math.max(maxCount, bounds.crossAxisPosition + bounds.crossAxisExtent).toInt();
    }
    return maxCount;
  }

  /// [boundsList]の順番を上から左からの順番に直す。その際に引数の[children]も同じように並べなおす。
  /// 並べ直された[List<Widget>]を返す。
  List<Widget> sort(List<Widget> children) {
    assert(boundsList != null);
    assert(children.length <= boundsList.length);

    List<int> indexes = [];
    for (int i = 0, len = children.length; i < len; i++) {
      indexes.add(i);
    }
    indexes.sort((a, b) => _compare(boundsList[a], boundsList[b]));
    List<Widget> sortedChildren = [];
    indexes.forEach((index)=> sortedChildren.add(children[index]));
    List<GridBounds> _boundsList = [];
    indexes.forEach((index)=> _boundsList.add(boundsList[index]));
    boundsList = _boundsList;
    return sortedChildren;
  }

  /// [sort]で使用されるプライベートメソッド。[GridBounds]を上から左からの順番に直るようなcompare関数。
  int _compare(GridBounds a, GridBounds b) {
    int _crossAxisCount = crossAxisCount;
    int mainAxisPositionDiff = (a.mainAxisPosition - b.mainAxisPosition) * _crossAxisCount;
    int crossAxisPositionDiff = a.crossAxisPosition - b.crossAxisPosition;
    return mainAxisPositionDiff + crossAxisPositionDiff;
  }

  /// boundsListの最後のIndexを返す。
  int get lastIndex {
    assert(boundsList != null);
    return boundsList.length - 1;
  }

}

こん中でも特に重要なのはここ。

GridBoundsList
  /// 座標から合致する[GridBounds]を[boundsList]の中から探し、そのIndexを返す。
  /// 合致しない場合は-1を返す。
  int getIndexAtPosition(int _mainAxisPosition, int _crossAxisPosition) {
    assert(boundsList != null);
    assert(_mainAxisPosition != null && _mainAxisPosition >= 0);
    assert(_crossAxisPosition != null && _crossAxisPosition >= 0);
    for (int i = 0, len = boundsList.length; i < len; i++) {
      GridBounds bounds = boundsList[i];
      if (bounds.mainAxisPosition <= _mainAxisPosition &&
            _mainAxisPosition < bounds.trailingMainAxisPosition &&
              bounds.crossAxisPosition <= _crossAxisPosition &&
                _crossAxisPosition < bounds.trailingCrossAxisPosition) return i;
    }
    return -1;
  }

座標を引数にとってその座標のGridBoundsを返す部分です。
SliverGridLayoutのgetMinChildIndexForScrollOffsetメソッド時に生きてきます。

さてお待ちかね!新しいSliverGridLayoutを作っていきましょうか!

SliverGridBoundsLayoutを作る。

今回のレイアウトの核となる部分です!
SliverGridRegularTileLayoutを元に作ったので、それとの違いを書いていきましょう。

SliverGridBoundsLayout
class SliverGridBoundsLayout extends SliverGridLayout {

  const SliverGridBoundsLayout({
    @required this.boundsList,
    @required this.mainAxisStride,
    @required this.crossAxisStride,
    @required this.childMainAxisExtent,
    @required this.childCrossAxisExtent,
    @required this.reverseCrossAxis,
  }) : assert(boundsList != null),
       assert(mainAxisStride != null && mainAxisStride >= 0),
       assert(crossAxisStride != null && crossAxisStride >= 0),
       assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
       assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0),
       assert(reverseCrossAxis != null);

  final GridBoundsList boundsList;

  final double mainAxisStride;

  final double crossAxisStride;

  final double childMainAxisExtent;

  final double childCrossAxisExtent;

  final bool reverseCrossAxis;

  @override
  int getMinChildIndexForScrollOffset(double scrollOffset) {
    final int scrollOffsetMainAxisPosition = mainAxisStride > 0.0 ? scrollOffset ~/ mainAxisStride : 0;
    final int mainAxisCount = boundsList.mainAxisCount;
    final int crossAxisCount = boundsList.crossAxisCount;
    int index = mainAxisCount + crossAxisCount;
    for (int crossAxisPosition = 0; crossAxisPosition < crossAxisCount; crossAxisPosition++) {
      int _index = boundsList.getIndexAtPosition(scrollOffsetMainAxisPosition, crossAxisPosition);
      if (_index >= 0) {
        index = math.min(index, _index);
      }
    }
    return 0;
  }

  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset) {
    final int scrollOffsetMainAxisPosition = mainAxisStride > 0.0 ? (scrollOffset / mainAxisStride).ceil() : 0;
    final int mainAxisCount = boundsList.mainAxisCount;
    final int crossAxisCount = boundsList.crossAxisCount;

    if (mainAxisCount - 1 <= scrollOffsetMainAxisPosition) {
      return boundsList.lastIndex;
    }
    for (int mainAxisPosition = scrollOffsetMainAxisPosition; mainAxisPosition >= 0; mainAxisPosition--) {
      for (int crossAxisPosition = crossAxisCount - 1; crossAxisPosition >= 0; crossAxisPosition--) {
        int index = boundsList.getIndexAtPosition(mainAxisPosition, crossAxisPosition);
        if (index >= 0) return index;
      }
    }
    return 0;
  }

  double _getOffsetFromStartInCrossAxis(double crossAxisStart) {
    if (reverseCrossAxis)
      return boundsList.crossAxisCount * crossAxisStride - crossAxisStart - childCrossAxisExtent;
    return crossAxisStart;
  }

  @override
  SliverGridGeometry getGeometryForChildIndex(int index) {
    GridBounds bounds = boundsList.getBoundsAtIndex(index);
    final double crossAxisStart = bounds.crossAxisPosition * crossAxisStride;
    final double mainAxisSpacing = mainAxisStride - childMainAxisExtent;
    final double crossAxisSpacing = crossAxisStride - childCrossAxisExtent;

    return SliverGridGeometry(
      scrollOffset: bounds.mainAxisPosition * mainAxisStride,
      crossAxisOffset: _getOffsetFromStartInCrossAxis(crossAxisStart),
      mainAxisExtent: childMainAxisExtent * bounds.mainAxisExtent + (bounds.mainAxisExtent - 1) * mainAxisSpacing,
      crossAxisExtent: childCrossAxisExtent * bounds.crossAxisExtent + (bounds.crossAxisExtent - 1) * crossAxisSpacing,
    );
  }

  @override
  double computeMaxScrollOffset(int childCount) {
    assert(childCount != null);
    final int mainAxisCount = boundsList.mainAxisCount;
    final double mainAxisSpacing = mainAxisStride - childMainAxisExtent;
    return mainAxisStride * mainAxisCount - mainAxisSpacing;
  }
}

コンストラクタ

SliverGridBoundsLayout
const SliverGridBoundsLayout({
    @required this.boundsList,
    @required this.mainAxisStride,
    @required this.crossAxisStride,
    @required this.childMainAxisExtent,
    @required this.childCrossAxisExtent,
    @required this.reverseCrossAxis,
  }) : assert(boundsList != null),
       assert(mainAxisStride != null && mainAxisStride >= 0),
       assert(crossAxisStride != null && crossAxisStride >= 0),
       assert(childMainAxisExtent != null && childMainAxisExtent >= 0),
       assert(childCrossAxisExtent != null && childCrossAxisExtent >= 0),
       assert(reverseCrossAxis != null);

crossAxisCountをやめて、先ほどのBoundsListクラスを引数に取ります。
他はさっきと同じです。
これでもうレイアウトに必要な情報を全て読み込んだと言っても過言ではありません。

getMinChildIndexForScrollOffsetメソッド  < これ重要!!!

SliverGridBoundsLayout
  @override
  int getMinChildIndexForScrollOffset(double scrollOffset) {
    final int scrollOffsetMainAxisPosition = mainAxisStride > 0.0 ? scrollOffset ~/ mainAxisStride : 0;
    final int crossAxisCount = boundsList.crossAxisCount;
    int index = boundsList.lastIndex;
    for (int crossAxisPosition = 0; crossAxisPosition < crossAxisCount; crossAxisPosition++) {
      int _index = boundsList.getIndexAtPosition(scrollOffsetMainAxisPosition, crossAxisPosition);
      if (_index >= 0) {
        index = math.min(index, _index);
      }
    }
    return 0;
  }

だいぶ様変わりしました。

getMinChildIndexForScrollOffset
final int scrollOffsetMainAxisPosition = mainAxisStride > 0.0 ? scrollOffset ~/ mainAxisStride : 0;

ここで上から何番目を計算して取得しています。
これはSliverGridRegularTileLayoutにもあったのでそのまま転用しました。
「~/」って便利ですね。こんな演算子を用意してくれるなんてGoogleさん素敵です!

上から何番目がわかったらその中で一番Indexが若いやつを探します。一番左のやつを探すのではなく、一番Indexが若いものを探すことが重要です。

newgridview.jpg

画像の7番でなく、4番を取得する必要があるってことですね。

見つからなかった場合は0を返します。

getMaxChildIndexForScrollOffsetメソッド  < これ重要!!!

SliverGridBoundsLayout
  @override
  int getMaxChildIndexForScrollOffset(double scrollOffset) {
    final int scrollOffsetMainAxisPosition = mainAxisStride > 0.0 ? (scrollOffset / mainAxisStride).ceil() : 0;
    final int mainAxisCount = boundsList.mainAxisCount;
    final int crossAxisCount = boundsList.crossAxisCount;

    if (mainAxisCount - 1 <= scrollOffsetMainAxisPosition) {
      return boundsList.lastIndex;
    }
    int index = 0;
    for (int crossAxisPosition = crossAxisCount - 1; crossAxisPosition >= 0; crossAxisPosition--) {
      int _index = boundsList.getIndexAtPosition(scrollOffsetMainAxisPosition, crossAxisPosition);
      if (_index >= 0) {
        return math.max(index, _index);
      }
    }
    return 0;
  }

ここでも先ほどと同じように、でも逆に一番Indexが大きいものを探します。

getMaxChildIndexForScrollOffset
    if (mainAxisCount - 1 <= scrollOffsetMainAxisPosition) {
      return boundsList.lastIndex;
    }

この部分は上からの行数が画面高さに満たない場合の処理です。

getGeometryForChildIndex  < これも重要!!

ついに描画される位置や幅を指定するGeometryを作るところまでやってまいりました!

SliverGridRegularTileLayoutのこのメソッドから着想を得てここまでやってきております。
さぁ、描画位置を指定してあげましょう!

getGeometryForChildIndex
  @override
  SliverGridGeometry getGeometryForChildIndex(int index) {
    GridBounds bounds = boundsList.getBoundsAtIndex(index);
    final double crossAxisStart = bounds.crossAxisPosition * crossAxisStride;
    final double mainAxisSpacing = mainAxisStride - childMainAxisExtent;
    final double crossAxisSpacing = crossAxisStride - childCrossAxisExtent;

    return SliverGridGeometry(
      scrollOffset: bounds.mainAxisPosition * mainAxisStride,
      crossAxisOffset: _getOffsetFromStartInCrossAxis(crossAxisStart),
      mainAxisExtent: childMainAxisExtent * bounds.mainAxisExtent + (bounds.mainAxisExtent - 1) * mainAxisSpacing,
      crossAxisExtent: childCrossAxisExtent * bounds.crossAxisExtent + (bounds.crossAxisExtent - 1) * crossAxisSpacing,
    );
  }

まずはIndexからGridBoundsをゲットします。
crossAxisStartとはこのIndexの描画領域の左からWidgetが描かれる領域の左端までの距離です。
mainAxisSpacingとcrossAxisSpacingは
(余白ありの長さ) - (余白なしの長さ) = (余白)
各方向における余白の大きさを計算して取得しています。

crossAxisOffsetの設定時に_getOffsetFromStartInCrossAxisが使用されています。
reverseCrossAxis(左右逆から並べる)がtrueかそうでないかで左からの距離が変わるので、その計算をしています。(試してないです)

(bounds.mainAxisExtent - 1) * mainAxisSpacing
とか
(bounds.crossAxisExtent - 1) * crossAxisSpacing
は跨いだ境界線の数だけ余白の分を追加してあげないと寸足らずになるので必須です。

これでレイアウトはバッチリです!
あとはこのレイアウトを生成するSliverGridDelegateを作るだけでOKです!

SliverGridDelegateの派生クラスを作る

SliverGridDelegateWithBounds
class SliverGridDelegateWithBounds extends SliverGridDelegate {

  const SliverGridDelegateWithBounds({
    @required this.boundsList,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
  }) : assert(boundsList != null),
       assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
       assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
       assert(childAspectRatio != null && childAspectRatio > 0);

  final GridBoundsList boundsList;

  final double mainAxisSpacing;

  final double crossAxisSpacing;

  final double childAspectRatio;

  bool _debugAssertIsValid() {
    assert(boundsList != null);
    assert(mainAxisSpacing >= 0.0);
    assert(crossAxisSpacing >= 0.0);
    assert(childAspectRatio > 0.0);
    return true;
  }

  @override
  SliverGridLayout getLayout(SliverConstraints constraints) {
    assert(_debugAssertIsValid());
    final int crossAxisCount = boundsList.crossAxisCount;
    final double usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
    final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
    final double childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
    return SliverGridBoundsLayout(
      boundsList: boundsList,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
    );
  }

  @override
  bool shouldRelayout(SliverGridDelegateWithBounds oldDelegate) {
    return oldDelegate.boundsList != boundsList
        || oldDelegate.mainAxisSpacing != mainAxisSpacing
        || oldDelegate.crossAxisSpacing != crossAxisSpacing
        || oldDelegate.childAspectRatio != childAspectRatio;
  }
}

コンストラクタ

SliverGridDelegateWithBounds
  const SliverGridDelegateWithBounds({
    @required this.boundsList,
    this.mainAxisSpacing = 0.0,
    this.crossAxisSpacing = 0.0,
    this.childAspectRatio = 1.0,
  }) : assert(boundsList != null),
       assert(mainAxisSpacing != null && mainAxisSpacing >= 0),
       assert(crossAxisSpacing != null && crossAxisSpacing >= 0),
       assert(childAspectRatio != null && childAspectRatio > 0);

boundsListは使用者が作ってここに指定する形です。
mainAxisSpacingはスクロール方向の余白。
crossAxisSpacingはスクロールに垂直な方向の余白。
childAspectRatioは領域の縦横比になります。

getLayout < これ重要!

SliverGridDelegateWithBounds
  @override
  SliverGridLayout getLayout(SliverConstraints constraints) {
    assert(_debugAssertIsValid());
    final int crossAxisCount = boundsList.crossAxisCount;
    final double usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1);
    final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
    final double childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
    return SliverGridBoundsLayout(
      boundsList: boundsList,
      mainAxisStride: childMainAxisExtent + mainAxisSpacing,
      crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
      childMainAxisExtent: childMainAxisExtent,
      childCrossAxisExtent: childCrossAxisExtent,
      reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
    );
  }

ここがGridDelegateの最重要メソッドです。
行の高さ(余白込み)、列の幅(余白込み)、Widgetの縦幅と横幅。
そしてコンストラクタで受け取ったboundsListをSliverGridBoundsLayoutに渡します。

最重要の割にスッキリ簡単な処理でしたね!

細々スルーしたものなどもありますが、これでほぼ全てです。

このDelegateをGridViewに渡せば柔軟なGridViewができるはず!

使用サンプル

bounds_grid_view.dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;
import 'package:bounds_grid_view/bounds_grid_view.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new FirstPageWidget(),
    );
  }
}

class FirstPageWidget extends StatefulWidget {
  FirstPageWidget({Key key}) : super(key: key);

  @override
  _FirstPageWidgetState createState() => new _FirstPageWidgetState();
}

class _FirstPageWidgetState extends State<FirstPageWidget> {

  Widget generateBody(BuildContext context) {
    List<GridBounds> boundsList = [];
    List<Widget> children = [];

    // 列数
    const int CROSS_AXIS_COUNT = 10;
    // 行数
    const int MAIN_AXIS_COUNT = 100;

    // Widgetの最大サイズ
    const int MAX_EXTENT = 3;

    // 領域マップの2次元配列 初期値は-1で埋める
    List<List<int>> indexMap = [];
    for (int i = 0; i < MAIN_AXIS_COUNT; i++) {
      indexMap.add(List<int>.filled(CROSS_AXIS_COUNT, -1));
    }

    // アイテム数のカウント
    int itemCount = 0;

    // 二次元配列を1セルずつみていく
    for (int y = 0; y < MAIN_AXIS_COUNT; y++) {
      for (int x = 0; x < CROSS_AXIS_COUNT; x++) {
        // -1じゃなかったらすでにマッピングされているのでスルー
        if (indexMap[y][x] >= 0) continue;
        // 今の座標で下に広がれる最大値 <= 3 を取得。
        final int maxMainAxisExtent = math.min(MAX_EXTENT, MAIN_AXIS_COUNT - y);
        // 今の座標で右に広がれる最大値 <= 3 を取得。
        final int maxCrossAxisExtent = math.min(MAX_EXTENT, CROSS_AXIS_COUNT - x);
        // この座標の領域の高さをランダムで生成
        int mainAxisExtent = math.Random().nextInt(maxMainAxisExtent) + 1;
        // この座標の領域の幅をランダムで生成
        int crossAxisExtent = math.Random().nextInt(maxCrossAxisExtent) + 1;

        // 領域マップにIndexを書き込む
        for (int yy = 0; yy < mainAxisExtent; yy++) {
          for (int xx = 0; xx < crossAxisExtent; xx++) {
            if (indexMap[y + yy][x + xx] >= 0) {
              crossAxisExtent = xx;
              continue;
            }
            indexMap[y + yy][x + xx] = itemCount;
          }
        }
        // 座標と領域の大きさからGridBoundsを生成し配列へ
        boundsList.add(GridBounds(y, x, mainAxisExtent, crossAxisExtent));
        // Indexを表示するテキスト(背景青)
        children.add(Container(
          child: Text(itemCount.toString(),style: TextStyle(fontSize: 16.0, color: Colors.white),),
          alignment: Alignment.center,
          color: Color.fromARGB(255, 45, 120, 220),
        ));
        // Indexのインクリメント
        itemCount++;
      }
    }
    // GridBoundsListの生成
    GridBoundsList gridBoundsList = GridBoundsList(boundsList);
    // gridBoundsListのsortを使用して並べ替えたWidget配列を取得
    List<Widget> sortedChildren = gridBoundsList.sort(children);

    // 例のDelegateを使ってGridViewを生成
    return GridView(
      gridDelegate: SliverGridDelegateWithBounds(
        boundsList: gridBoundsList,
        childAspectRatio: 1.0,
        mainAxisSpacing: 1.0,
        crossAxisSpacing: 1.0,
      ),
      children: sortedChildren,
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: new Text("BoundsGridViewTest")),
      body: this.generateBody(context),
    );
  }
}

100 x 10 のGridの中に様々な大きさの領域(縦横共にMax 3個分)を生成するサンプルです。
実行結果はこんな感じ

result.png

やった!できた!スクロールしても問題ない!

_FirstPageWidgetState/generateBodyメソッド
    // GridBoundsListの生成
    GridBoundsList gridBoundsList = GridBoundsList(boundsList);
    // gridBoundsListのsortを使用して並べ替えたWidget配列を取得
    List<Widget> sortedChildren = gridBoundsList.sort(children);

    // 例のDelegateを使ってGridViewを生成
    return GridView(
      gridDelegate: SliverGridDelegateWithBounds(
        boundsList: gridBoundsList,
        childAspectRatio: 1.0,
        mainAxisSpacing: 1.0,
        crossAxisSpacing: 1.0,
      ),
      children: sortedChildren,
    );

しっかりとSliverGridDelegateWithBoundsを生成してGridViewに与えております。
さらにシンプルに生成するためにBoundsGridViewも作ってみました。

BoundsGridView
    return BoundsGridView(
      boundsList: gridBoundsList,
      childAspectRatio: 1.0,
      mainAxisSpacing: 1.0,
      crossAxisSpacing: 1.0,
      children: sortedChildren,
    );

こっちだとchildrenの並べ替え自分でやらなくても中でやってくれます。

あとがき

非常に勉強になって楽しかったです。Dartの書き方が性に合っているようです。
もっと順序よく書こうかと思ったのですが、実際の思考の順番に書いた方があとで見てもわかりやすいと思いました。
見づらかったかと思いますが、見ていただきましてありがとうございました!

今回のコードは
ins-Beer/flutter_bounds_grid_view
にあります。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away