🎨

FlutterのCustomMultiChildLayoutとCutomSingleChildLayoutの使い方

2023/03/21に公開

はじめに

通常のレイアウトシステムでは実現できない高度なレイアウトを作成する場合に、自前でRenderObjectWidgetのサブクラスを作ってゴリゴリすれば実現はできますが、開発コストが非常に高くなる傾向にあります。

CustomMultiChildLayoutCutomSingleChildLayoutはWidgetのレイアウトを制御し、独自のレイアウトを作成するための非常に便利なWidgetです。これらのウィジェットを使用することで、通常のレイアウトシステムでは実現できない高度なレイアウトを作成できるので、RenderObjectWidgetを自前で実装することなく所望のレイアウトを実現できる場合があります。

例えば次のようなレイアウトが実現できます🎉

本記事ではそれぞれの使い方を説明していきたいと思います💪

CustomMultiChildLayoutについて

このWidgetは、複数のWidgetを自由に配置するためのものであり、子要素の位置やサイズをお互いに依存させることができます。この特性によって、例えばプリビルドされたStack Widgetでは実現できないレイアウトが可能になります。

使い方

CustomMultiChildLayoutを使うには以下の2つの引数を与える必要があります。

  1. children: 全ての子要素はLayoutIdでラップされている必要があります。idは、レイアウト時にWidgetを一意に特定するために使われます。
    CustomMultiChildLayout(
      children: [
        LayoutId(
          id: 1,
          child: Container(
    	      color: Colors.blue,
          ),
        ),
        LayoutId(
          id: 2,
          child: Container(
    	      color: Colors.red,
          )
        ),
      ],
    )
    
  2. delegate: MultiChildLayoutDelegateのサブクラスを作成し、レイアウト部分を処理を記述する必要があります。
    class MyLayoutDelegate extends MultiChildLayoutDelegate {
      MyLayoutDelegate({required this.reverse});
      final bool reverse;
    }
    

MultiChildLayoutDelegateのサブクラスの作り方

まずはperformLayoutをオーバーライドして実際のレイアウトの処理を記述しましょう。実装にあたって以下の3つのメソッドを使うことが可能です。

  1. hasChild: 指定したidの子要素がchildrenに含まれているかどうか
  2. layoutChild: 指定したidの子要素をレイアウトしsizeを取得する
  3. positionChild: ポジションをOffset(0, 0)から任意に移動させることができる

これらを駆使して例えばこんな感じにレイアウトを記述できます。中身の詳細は追わなくて良くて、hasChildを使ってループ回して子要素の数を取得したり、layoutChild使って子要素のサイズを取得したり、positionChild使って子要素を任意のポジションに移動させたりしていることが確認できればOKです!


void performLayout(Size size) {
  int index = 0;
  while (hasChild(index)) { // ループ回して子要素の数を取得
    index++;
  }
  final childrenNum = index;

  index = 0;
  double sumSize = 0;
  while (hasChild(index)) {
    final childSize = layoutChild( // 子要素のサイズを取得
      index,
      BoxConstraints.tight(
          Size(size.width / childrenNum, size.height / childrenNum)),
    );
    assert(childSize.width == childSize.height);

    positionChild( // 子要素を任意のポジションに移動
      index,
      Offset(reverse ? size.width - sumSize - childSize.width : sumSize,
          sumSize),
    );
    sumSize += childSize.width;
    index++;
  }
}

次にshouldRepaintをオーバーライドしてperformLayoutをいつ呼び出すかを記述します。

class _MyLayoutDelegate extends MultiChildLayoutDelegate {
  _MyLayoutDelegate({required this.reverse});
  final bool reverse;

  
  void performLayout(Size size) {
  // 省略
  }

  
  bool shouldRelayout(covariant _MyLayoutDelegate oldDelegate) =>
      oldDelegate.reverse != reverse; // reverseが変わるとperformLayoutが呼ばれる
}

以上でCustomMultiChildLayoutを使ってレイアウトをカスタムすることができました🎉

備考

  • 子要素の大きさに応じてCustomMultiChildLayout自体の大きさを決めるのはできないみたい(参考)。この場合はMultiChildRenderObjectWidgetのサブクラスを自前で実装しないといけなさそう。
  • 上記の例ではidintを使ったがenumを使うやり方がベスト
  • relayoutを指定するとアニメーションにも対応できる

CustomSingleChildLayoutについて

このWidgetは、単一の子要素を持つ Widget のレイアウトを制御するために使用されます。子要素のサイズに応じたレイアウトが実現できます。Flutterでは、Widgetのサイズはレンダリングなしでは取得することができません(参考)。そのため、一度レンダリングしてから次のフレームでそのサイズを使用するという少し非効率な方法がありますが(参考)、このWidgetを使用することで、このような問題を回避できる場合があります。

使い方

CustomSingleChildLayoutを使うには以下の2つの引数を与える必要があります。

  1. child: 任意のWidget。一意に決まるためLayoutIdでラップする必要はありません。
  2. delegate: SingleChildLayoutDelegateのサブクラスを作成し、レイアウト部分を処理を記述する必要があります。

SingleChildLayoutDelegateのサブクラスの作り方

以下の4つのメソッドの中から必要に応じてオーバーライドします。

  1. getSize: 渡ってきたconstraintsに対してCustomSingleChildLayout自体のサイズを決める。
  2. getConstraintsForChild: 渡ってきたconstraintsに対して子要素に渡すconstarintsを決める。
  3. getPositionForChild: 子要素のポジションを決める。positionChildに対応する。
  4. shouldRelayout: 先ほどと同様。上記3つのメソッドをいつ呼び出すかを記述する。

今回は下3つをオーバーライドしました。詳細は追わなくてもいいですが、Multiの時と同様に子要素のサイズに応じてレイアウトを制御できていることが分かればOKです!!

class _MySingleChildLayoutDelegate extends SingleChildLayoutDelegate {
  _MySingleChildLayoutDelegate({required this.offset});

  final Offset? offset;

  
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
      constraints.loosen(); // 子要素に渡すconstraintsを指定

  
  Offset getPositionForChild(Size size, Size childSize) {
    if (offset == null) {
      return Offset(size.width / 2 - childSize.width / 2,
          size.height / 2 - childSize.height / 2);
    }

    final width = clampDouble(
        offset!.dx, childSize.width / 2, size.width - childSize.width / 2);
    final height = clampDouble(
        offset!.dy, childSize.height / 2, size.height - childSize.height / 2);

    return Offset(width - childSize.width / 2, height - childSize.height / 2); // 子要素のサイズに応じたポジションを返す
  }

  
  bool shouldRelayout(covariant _MySingleChildLayoutDelegate oldDelegate) =>
      oldDelegate.offset != offset; // offsetが変わると上記のメソッドが呼ばれる
}

以上でCustomSingleChildLayoutについてもレイアウトを制御することができました🎉

まとめ

  • CustomMultiChildLayoutは複数のWidgetを自由に配置するためのもので、子要素の位置やサイズをお互いに依存させることができる
  • CustomSingleChildLayoutは単一のWidgetを自由に配置するためのもので、子要素のサイズに応じたレイアウトが実現できる
  • これらのWidgetを必要とするケースに出くわすことはごく稀だが知っておくとイレギュラーなレイアウトパターンにも対処できるかも!

サンプルコードはこちらに置いておくのでぜひぜひ触ってみてください!

もし間違ってるところあればコメントいただけると助かります🙇

ちなみにNavigationToolbarの内部で使われているみたいです📝
https://twitter.com/_mono/status/1359847986361106436?s=20

リファレンス

記事を書くうえで参考にしたもの
https://www.youtube.com/watch?v=HqXNGawzSbY
https://stackoverflow.com/questions/59483051/how-to-use-custommultichildlayout-customsinglechildlayout-in-flutter

Discussion