【Flutter】FractionallySizedBox で本当にいいのか、サイズ比率を変える3つのウィジェットで考える
ウィジェットの比率を変える方法
ウィジェットのサイズの、例えば 70% を表示したいとき、Flutter の公式動画では、FcactionallySizedBox が紹介されています。
ウィジェットのサイズ比率を変える3つのウィジェット
-
FractionallySizedBox
- heightFactor, widthFactor の引数を持つ。
-
Align
- heightFactor, widthFactor の引数を持つ。
-
Transform.scale
- scale(scaleY, scaleY) の引数を持つ。
以下がざっくりとした特徴です。
ウィジェット | サイズ調整方法 | RenderObject | 注意点 |
---|---|---|---|
FractionallySizedBox | 親からの BoxConstraints の最大値から計算して調整 | RenderFractionallySizedOverflowBox | BoxConstraints の最大値が double.infinity だとエラー |
Align | 子から受け取ったサイズから計算して調整 | RenderPositionedBox |
ClipRect で囲まないと、はみ出てしまう |
Transform.scale | サイズはそのまま、子の要素のみのサイズなどの表示を調整 | RenderTransform | 変更されたサイズによって、レイアウトが調整されない |
RenderObject とは、ウィジェットのサイズ調整などの描画に関する部分が実際に記述されているクラスです。
描画に関する役割を持つウィジェットは、すべて何かしらの RenderObject をそれぞれ持っています(RenderObjectWidget)
FractionallySizedBox
使い方と仕組み
一つずつ見ていきます。
FractionallySizedBox
のサイズ調整方法には、実際に描画されいる Size ではなく、祖先から来た BoxConstraints
を利用します。
やっていることとしては、「BoxConstraints go down」の要領で祖先から来た BoxConstraints の最大値を捻じ曲げて子孫に送ります。
たとえば、height: 100
の中で、FractionallySizedBox(heightFactor: 0.5)
のウィジェットを設置すると、その子孫の高さは 50 になります。
SizedBox(
height: 100,
child: Row(
children: [
_SizedBox(color: Colors.indigo),
FractionallySizedBox(
heightFactor: 0.5,
child: _SizedBox(
color: Colors.green,
),
),
]
)
)
(_SizedBox
ウィジェットは、なんとなく察してください)
_SizedBox ウィジェットの中身
class _SizedBox extends StatelessWidget {
const _SizedBox({required this.color});
final Color color;
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 100,
child: ColoredBox(
color: color,
child: const Center(
child: Text('ABC'),
),
),
);
}
}
この時、子孫に送る BoxConstraints
は minHeight == maxHeight
となり、いわゆる tight な制約になります。
ソースコードを見るとそれがよくわかります。
/// FractionarySizedBox の RenderObject
class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
// ....
/// performLayout() で呼ばれる関数
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
double maxHeight = constraints.maxHeight;
// ...
if (_heightFactor != null) {
// * 最大値から計算
final double height = maxHeight * _heightFactor!;
// * 計算結果を、minHeight, maxHeight 両方に代入している
minHeight = height;
maxHeight = height;
}
return BoxConstraints(
minWidth: minWidth,
maxWidth: maxWidth,
minHeight: minHeight,
maxHeight: maxHeight,
);
}
そして、BoxConstraints を捻じ曲げるという強い言葉を使った理由は、以下のように祖先の制約を完全に無視したサイズ調整も可能です。
heightFactor に 2 を設定することで、2倍の大きさにしている
RenderFractionallySizedOverflowBox
の名の通り、OverflowBox
に似た挙動をします。
RenderBox
は、基本的に「隣に何のウィジェットがあるか」みたいなことは考えず、渡された Offset や BoxConstraints に従ってとりあえず描画します。
もし、ウィジェットの境界のハミ出しによって描画をさせないようにしたい場合は、Clip
の力を借ります。
SizedBox(
height: 100,
child: Row(
children: [
_SizedBox(color: Colors.indigo),
FractionallySizedBox(
heightFactor: 0.5,
child: _SizedBox(
color: Colors.green,
),
),
ClipRect(
child: FractionallySizedBox(
heightFactor: 2,
child: _SizedBox(
color: Colors.green,
),
),
),
]
)
)
注意点・適していないパターン
Column/Row で使う場合
たとえば、このような場合にはエラーになってしまいます。
Column(
children: [
// ここのウィジェットの高さだけ2分の1にしたい
FractionarySizedBox(
heightFactor: 0.5,
child: SizedBox(
height: 100,
),
),
],
)
Column
の children の BoxConstraints
は、すべて 0.0 < h < double.infinity
で扱われます。
そして、FractionarySizedBox
は、祖先の最大値をもとに子孫に tight な制約を与えるウィジェット。
これでは double.infinity
が与えられ[1]、Column
の children に高さ無限のものが置かれると、Column
のなかに ListView
を置いた時のようにRenderBox was not laid out
と怒られてしまいます。
~OverflowBox
なんて名乗っておきながらいざ無限の値を与えられるとエラーになってしまう、根性のないやつですね。
そういう場合は、Expanded/Flexible
を充てて最大値の制約を与えててやるのが定石ですが、それだと目一杯広がってしまいます。
FractionallySizedBox
はあくまで制約をいじるだけで、実際の Size を * 0.5
するわけではありません。
こういう場合は、次の Align/Transform.scale
を使いましょう。
Align
子ウィジェットの配置を定めるウィジェットですが、実はこいつも高さと幅を定めるパラメータが存在sます。
変数名は FractionallySizedBox
と同じです。
Align(
heighFactor: 0.5,
widthFactor: 0.25,
chid: // ...
)
しかし、こいつは少し奇妙な挙動をします。確認してみましょう。
まず、横に並んだ2つの矩形を用意します。
Row(
children: [
_SizedBox(color: Colors.indigo),
_SizedBox(color: Colors.amber),
],
)
では、右のウィジェットに、Align
を設けて、widthFactor: 0.5
で半分の幅にしてみましょう。
const Row(
children: [
_SizedBox(color: Colors.indigo),
Align(
widthFactor: 0.5,
child: _SizedBox(color: Colors.amber),
),
],
),
結果
....
なんか変な見た目になりましたね。
試しに、右の黄色い矩形に透明度を与えてみると、重なっていることがわかります。
どういうことかというと、この Align ウィジェットのサイズ調整のキモは、子ウィジェットの制約に影響を与えない という点です。
FractionallySizedBox
とは正反対な感じしますね。
Align
が持つ RenderObject
サイズを決定する部分、 performLayout()
関数を見てみましょう
class RenderPositionedBox extends RenderAligningShiftedBox {
void performLayout() {
final BoxConstraints constraints = this.constraints;
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
if (child != null) {
// 子ウィジェットのレイアウト
child!.layout(constraints.loosen(), parentUsesSize: true);
// 子ウィジェットのサイズをもとに、自身のサイズを決める
// その時、widthFactor/heightFactor の掛け算を行う
size = constraints.constrain(Size(
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
));
alignChild();
} else {
size = constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
}
FractionaySizedBox
の widthFactor/heightFactor
との大きな違いは、子ウィジェットレイアウト計算には関わらない点です。
子ウィジェットのレイアウトをした後に、その親である自身のサイズ決めるのは 「Size go up」 なので普通なのですが、その時 widthFactor/heightFactor
でさらにサイズに変更を与える計算を加えています。
変なヤツですね。
そして、さっき書いた通り、RenderBox
は基本的に「隣に何のウィジェットがあるか」みたいなことは知らないので、ハミ出るとか考えずに、与えられた制約の通りに描画します。
なので、これもさっき書いた通り、Clip を使えばはみ出さずに描画することが可能です。
const Row(
children: [
_SizedBox(color: Colors.indigo),
ClipRect(
chid: Align(
widthFactor: 0.5,
child: _SizedBox(color: Colors.amber),
),
),
],
),
SizeTransition
ウィジェットでも、この仕組みが使われています。
Widget buid(BuildContext context) {
return ClipRect(
child: Align(
alignment: alignment,
heightFactor: axis == Axis.vertical ? math.max(sizeFactor.value, 0.0) : fixedCrossAxisSizeFactor,
widthFactor: axis == Axis.horizontal ? math.max(sizeFactor.value, 0.0) : fixedCrossAxisSizeFactor,
child: child,
),
);
}
Align
x ClipRect
で、実質的に子ウィジェットのサイズの比率をいじることができます。
これは、子ウィジェットをとりあえず表示して、かつその一部を比率で隠したい時に使用されます。
一定の比率で隠すことは少ないかもしれませんが、アニメーションで 0.0 => 1.0
にする場合はよくあるかもしれません。
そういう場合に SizeTransition
採用する形でこの Align
x ClipRect
を使うことがあると思います。
Transform.scale
最後は Transform
ウィジェットです。
このウィジェットは、子ウィジェットの表示を拡大/縮小します。
この時、Transform
ウィジェット自体の大きさには影響しない のがポイントです。
これはスクリーンショットを見るのがわかりやすいと思います。
Transform.scale(
scaleX: 0.5
) を与えている
scaleX
のみを渡しているので、その分縮小して縦に延びた見た目になっています。
ですが、子ウィジェットが小さくなった分Transform
ウィジェットが小さくなることはなく、隙間ができています。
これは、RenderTransform
が、サイズを決定する performLayout()
を override しておらず、描画する関数である paint()
でやってる体と思います多分。
まとめ
使い分けはこんな感じになると思います。
- 「ここの範囲で、ある比率のサイズのウィジェットを表示したい」
- BoxConstraints の最大値を利用する
FractionarySizedBox
を選択。
- BoxConstraints の最大値を利用する
- ウィジェットの一部を隠して、その分サイズを小さく(大きく)したい場合。
-
Align
xClipRect
、もしくはSizeTransition
。
-
- 「一部を隠す」ような場合や、アニメーションで子ウィジェットで出したり隠したりしたい。
- 非表示の場合は、サイズを 0 にしたい。
-
Align
xClipRect
、もしくはSizeTransition
。
-
- 非表示の場合も表示領域を確保、与えた比率の分 拡大/縮小したい。
-
Transform.scale
、もしくはScaleTransition
。
-
- 非表示の場合は、サイズを 0 にしたい。
少し抽象的な話になりましたが、ウィジェットのサイズ比率調節をしたくなった時の助けになれば幸いです。
-
print(double.infinity * 0.5) // double.infinity
↩︎
Discussion