今回はタイトルの通り、DraggableScrollableSheet
におけるmaxChildSize
を子要素の実際のサイズに合わせる方法を解説します。DraggableScrollableSheet
のコンテンツの高さが場合によって変わる場合、展開したときの最大サイズをそのコンテンツの高さに合わせたい場合があると思います。何も指定ない状態では、以下のようにコンテンツの量に対してシートが多くの面積を占領してしまうケースがあります。
これをどうにかしていきましょう。
※StatefulWidgetを使用したコードについては、今後追加予定です。
Table of Contents
- 実現のために必要な要素
- コンテンツ(子要素)の高さを取得する
- DraggableScrollableSheetが占領できる最大の高さを取得する
- 最大領域に対する子要素のサイズの割合を計算する
- maxChildSizeを、計算した値に設定する
- 実行結果
実現のために必要な要素
maxChildSize
を子要素の実際のサイズに合わせるには、子要素のサイズに合わせてmaxChildSize
を変えてやる必要があります。これを実現するためには、以下の4つ数値をどうにかして入手する必要があります。
- 実際の子要素のサイズ
DraggableScrollableSheet
の最大高さ- 子要素の最大サイズ割合 (maxChildSize) (サイズがこの値で頭打ちするように)
- 初期サイズ割合 (initialChildSize) (この値より小さい値が
maxChildSize
に割り当てられることを防ぐため)
DraggableScrollableSheet
のmaxChildSize
プロパティは、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"),
),
],
),
);
},
);
}
}
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
を使います。
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
が展開できる最大サイズに対する、子要素のサイズの割合を計算します。
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を用いて初回のレンダリングが終わったタイミングで設定します。
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"),
),
],
),
),
);
},
);
},
);
}
}
実行結果
子要素に合わせて展開サイズが変わるようになりました。パチパチ