【Flutter】DraggableScrollableSheetの最大高さを子要素のサイズに合わせたい!

2024/08/01 11:41公開
2024/08/29 17:41最終更新

今回はタイトルの通り、DraggableScrollableSheetにおけるmaxChildSizeを子要素の実際のサイズに合わせる方法を解説します。DraggableScrollableSheetのコンテンツの高さが場合によって変わる場合、展開したときの最大サイズをそのコンテンツの高さに合わせたい場合があると思います。何も指定ない状態では、以下のようにコンテンツの量に対してシートが多くの面積を占領してしまうケースがあります。

画面収録 2024-07-31 21.32.21.webp

これをどうにかしていきましょう。

※StatefulWidgetを使用したコードについては、今後追加予定です。

Table of Contents
  1. 実現のために必要な要素
    1. コンテンツ(子要素)の高さを取得する
    2. DraggableScrollableSheetが占領できる最大の高さを取得する
    3. 最大領域に対する子要素のサイズの割合を計算する
  2. maxChildSizeを、計算した値に設定する
  3. 実行結果

実現のために必要な要素

maxChildSizeを子要素の実際のサイズに合わせるには、子要素のサイズに合わせてmaxChildSizeを変えてやる必要があります。これを実現するためには、以下の4つ数値をどうにかして入手する必要があります。

  • 実際の子要素のサイズ
  • DraggableScrollableSheetの最大高さ
  • 子要素の最大サイズ割合 (maxChildSize) (サイズがこの値で頭打ちするように)
  • 初期サイズ割合 (initialChildSize) (この値より小さい値がmaxChildSizeに割り当てられることを防ぐため)

DraggableScrollableSheetmaxChildSizeプロパティは、DraggableScrollableSheetが展開できる最大サイズに対する割合 で取るため、子要素のサイズに加えてその最大サイズが分からないといけません。これらはプログラムで取得する必要があります。 子要素の最大サイズ割合は、子要素が十分に大きい際に頭打ちする値です。初期サイズ割合については、これより小さい値がmaxChildSizeに設定されてしまうとエラーが発生するため、これを防ぐために必要です。

コンテンツ(子要素)の高さを取得する

まず、コンテンツ(子要素)の高さを取得するため、当該の子要素にkeyをつけます。

Flutter Hooksを利用する場合

class MyBottomSheet extends HookWidget {
  const MyBottomSheet({super.key});

  @override
  Widget build(BuildContext context) {
    final contentsKey = useMemoized(() => GlobalKey()); // <=
    
    return DraggableScrollableSheet(
      expand: false,
      snap: true,
      maxChildSize: 0.9,
      builder: (context, scrollController) {
        return SingleChildScrollView(
          controller: scrollController,
          child: Column(
            key: contentsKey // <=
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              for (var i = 0; i < 15; i++)
                Card(
                  child: Text("Item $i"),
                ),
            ],
          ),
        );
      },
    );
  }
}
StatefulWidgetを利用する場合
class MyBottomSheetWoHooks extends StatefulWidget {
  const MyBottomSheetWoHooks({super.key});

  @override
  State<MyBottomSheetWoHooks> createState() => _MyBottomSheetWoHooksState();
}

class _MyBottomSheetWoHooksState extends State<MyBottomSheetWoHooks> {
  final contentsKey = GlobalKey(); // <=

  @override
  Widget build(BuildContext context) {
    return DraggableScrollableSheet(
      expand: false,
      snap: true,
      maxChildSize: 0.9,
      builder: (context, scrollController) {
        return SingleChildScrollView(
          controller: scrollController,
          child: Column(
            key: contentsKey, // <=
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              for (var i = 0; i < 15; i++)
                Card(
                  child: Text("Item $i"),
                ),
            ],
          ),
        );
      },
    );
  }
}

このkeyに対して、key.currentContext.findRenderObject()を通してWidgetの実際の描画サイズにアクセスできます。


【注意】SingleChildScrollViewにpaddingを適用している場合、その子要素のPaddingに対してkeyを設定する必要があり、SingleChildScrollViewのプロパティでpaddingを設定してはいけません。こうしないと、ScrollViewで見えている範囲の高さとなってしまい、正しいコンテンツの高さが取得できません。

builder: (context, scrollController) {
  return SingleChildScrollView(
    controller: scrollController,
    child: Padding(
      key: contentsKey, // <= ここに設定
      padding: const EdgeInsets.only(left: 16.0, bottom: 16.0, right: 16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          for (var i = 0; i < 15; i++)
            Card(
              child: Text("Item $i"),
            ),
        ],
      ),
    ),
  );
},

DraggableScrollableSheetが占領できる最大の高さを取得する

この高さは、要するにmaxChildSize: 1.0の場合のDraggableScrollableSheetの最大高さです。これを取得するには、LayoutBuilderを使います。

Flutter Hooksを利用する場合
class MyBottomSheet extends HookWidget {
  const MyBottomSheet({super.key});

  @override
  Widget build(BuildContext context) {
    final contentsKey = useMemoized(() => GlobalKey());
    final availableHeight = useRef<double?>(null); // <= 追加

    return LayoutBuilder( // <= 追加
      builder: (context, constraints) {
        availableHeight.value = constraints.maxHeight; // <= 追加

        return DraggableScrollableSheet(
          expand: false,
          snap: true,
          maxChildSize: 0.9,
          builder: (context, scrollController) {
            return SingleChildScrollView(
              controller: scrollController,
              child: Padding(
                key: contentsKey,
                padding: const EdgeInsets.only(left: 16.0, bottom: 16.0, right: 16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    for (var i = 0; i < 15; i++)
                      Card(
                        child: Text("Item $i"),
                      ),
                  ],
                ),
              ),
            );
          },
        );
      },
    ); // <= 追加
  }
}

※availableHeightはbuildメソッドの中で値を取得・設定しているため、値が変更されても再描画が走らないuseRefを使用しています。(buildメソッドの処理が終わっていないタイミングで再描画が走るとエラーになります)

最大領域に対する子要素のサイズの割合を計算する

DraggableScrollableSheetが展開できる最大サイズに対する、子要素のサイズの割合を計算します。

Flutter Hooksを利用する場合
class MyBottomSheet extends HookWidget {
  const MyBottomSheet({super.key});

  @override
  Widget build(BuildContext context) {
    final contentsKey = useMemoized(() => GlobalKey());
    final availableHeight = useRef<double?>(null);

    const maxChildSize = 0.9; // <= 追加
    const initialChildSize = 0.5; // <= 追加
    
    double calculateChildSizeRatio() { // <= 追加
      final renderBox = contentsKey.currentContext?.findRenderObject() as RenderBox?;
      if (renderBox == null || availableHeight.value == null) {
        return maxChildSize;
      }

      return max(initialChildSize, min(renderBox.size.height / availableHeight.value!, maxChildSize));
    }

    return LayoutBuilder(
      builder: (context, constraints) {
        availableHeight.value = constraints.maxHeight;

        return DraggableScrollableSheet(
          expand: false,
          snap: true,
          maxChildSize: maxChildSize, // <= 変更
          initialChildSize: initialChildSize, // <= 追加
          builder: (context, scrollController) {
            return SingleChildScrollView(
              controller: scrollController,
              child: Padding(
                key: contentsKey,
                padding: const EdgeInsets.only(left: 16.0, bottom: 16.0, right: 16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    for (var i = 0; i < 15; i++)
                      Card(
                        child: Text("Item $i"),
                      ),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }
}

実際に割合を計算しているのはこの部分ですね。あまり難しい計算はしていないです。

max(initialChildSize, min(renderBox.size.height / availableHeight.value!, maxChildSize));

maxChildSizeを、計算した値に設定する

いよいよ、計算した値を設定していきます。Hooksを使用している場合は、useEffectを用いて初回のレンダリングが終わったタイミングで設定します。

Flutter Hooksを利用する場合
class MyBottomSheet extends HookWidget {
  const MyBottomSheet({super.key});

  @override
  Widget build(BuildContext context) {
    final contentsKey = useMemoized(() => GlobalKey());
    final availableHeight = useRef<double?>(null);

    const maxChildSize = 0.9;
    const initialChildSize = 0.5;

    final currentMaxChildSize = useState(maxChildSize);  // <= 追加

    double calculateChildSizeRatio() {
      final renderBox = contentsKey.currentContext?.findRenderObject() as RenderBox?;
      if (renderBox == null || availableHeight.value == null) {
        return maxChildSize;
      }

      return max(initialChildSize, min(renderBox.size.height / availableHeight.value!, maxChildSize));
    }

    useEffect(() { // <= 追加
      WidgetsBinding.instance.addPostFrameCallback((_) {
        currentMaxChildSize.value = calculateChildSizeRatio();
      });
      return null;
    }, [],);

    return LayoutBuilder(
      builder: (context, constraints) {
        availableHeight.value = constraints.maxHeight;

        return DraggableScrollableSheet(
          expand: false,
          snap: true,
          maxChildSize: currentMaxChildSize.value, // <= 変更
          initialChildSize: initialChildSize,
          builder: (context, scrollController) {
            return SingleChildScrollView(
              controller: scrollController,
              child: Padding(
                key: contentsKey,
                padding: const EdgeInsets.only(left: 16.0, bottom: 16.0, right: 16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    for (var i = 0; i < 15; i++)
                      Card(
                        child: Text("Item $i"),
                      ),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }
}

実行結果

画面収録 2024-08-01 11.36.10.webp

子要素に合わせて展開サイズが変わるようになりました。パチパチ