Flutterでは主にWidgetを用いてサイズの指定、制約などを定義してレイアウトを記述します。iOS開発におけるAutoLayoutやAndroidにおけるConstraintLayoutなどでレイアウトを組むのではなくViewをどのように配置するか、どのサイズで表示するかも含めてWidgetで定義が可能です。
しかし、実際にFlutterではどのような仕組みでWidgetのサイズを決め、そしてどこに配置するのかを計算しているのでしょうか?今回はFlutterにおけるBox Constraintsの仕組みについてざっとまとめてみようかと思います。
WidgetとElementとRenderObject
特にiOSのAutoResizingにおいてはView自体が座標・サイズを持ちそれをもとにレンダリングされていましたが、FlutterにおいてWidgetは自身の座標やサイズは持たず、テキストや画像URLなどサイズに影響する値しか持たない軽量オブジェクトです。各Widgetが持つcreateElement()メソッドによって対応するElementが作られ、さらにそのElementがRenderObjectElementの場合、mountのタイミングでrenderObjectを生成しています。このRenderObjectが基本的なレイアウト処理を担当しています。Widget, Element, RenderObjectの関係性は、@mono0926さんの記事がめちゃわかりやすいのでご参照ください1。
わかりやすい例として、自身にサイズの値を渡して子Widgetのサイズを制約するSizedBox Widgetは、createRenderObjectの際にRenderConstrainedBoxというRenderObjectを生成しています。
class SizedBox extends SingleChildRenderObjectWidget {
final double width;
final double height;
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(
additionalConstraints: _additionalConstraints,
);
}
BoxConstraints get _additionalConstraints {
return BoxConstraints.tightFor(width: width, height: height);
}
}
SizedBox自体はwidth/heightを持っていますが、それをレンダリング時に直接利用する事はせず、BoxConstraintsにもたせています。レンダリング自体はこのWidgetで行われていないのがわかるかと思います。
WidgetにRenderObjectを生成するメソッドが定義されてはいますが、これはSizedBox Widgetに対応したElementであるRenderObjectElement(SingleChildRenderObjectElement)から呼び出され、生成されたRenderObjectがattachされています。
abstract class RenderObjectElement extends Element {
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
}
}
このRenderObjectを生成しているときにわたしているのがBoxConstraintsです。基本的にWidgetは、このBoxConstraintsによってサイズの制約を定義しています。
BoxConstraintsとは
BoxConstraintsとは、Constraintsクラスを継承して作られている、最小・最大の高さ・幅を持っているクラスです。
class BoxConstraints extends Constraints {
final double minWidth;
final double maxWidth;
final double minHeight;
final double maxHeight;
}
この制約に合わせて、minWidth/minHeightより大きく、maxWidth/maxHeightよりも小さくなるように制約が作られ、子Widget及び自分自身のサイズに影響します。
これらのdouble値に指定可能な値は0からdouble.infinityまであり、infinityの場合は可能な限り自身のWidgetのサイズを大きくする、という指定になります。
SizedBoxなど厳密にこのサイズにしたい!という制約を決めたい場合や、RenderObjectのRootであるRootViewはWindowと同じサイズになるよう、BoxConstraints.tightFor(_size)を用いて、上記min/maxが同じ値が設定されている場合もあります。
このConstraintsをもとにTextやImageなどの最終的にCanvas上にpaintされるRenderObjectのsizeが決定され、paint処理が実行されます。
多くのRenderObjectはレイアウト計算時に呼ばれるperformLayout()にて自身のサイズ計算や、子への制約を渡しています。
SizedBox, Center, Imageを例に取って、performLayout()の処理を見ていきましょう。
SizedBox
SizedBoxに対応するRenderConstrainedBoxの場合、レイアウト計算する際に呼ばれるperformLayout()は以下のようになっています。
class RenderConstrainedBox extends RenderProxyBox {
@override
void performLayout() {
if (child != null) {
child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
}
_additionalConstraintsが親から渡されている親のconstraintsに収まるように計算され、子Objectのレイアウト処理が実行されています。parentUsesSizeは子のサイズの変化を親が検知する必要があるかどうかを表しており、今回は親が子のサイズを指定しており、子のサイズによって変化するためtrueを指定しています。これを指定する事で子の再レイアウトが走ったときに親のレイアウトも走ります。
Center
Centerの場合、child widgetを中心に置くためにCenter自身のサイズは親のサイズに合わせてなるべく大きくなろうとします。Centerは配置系Widgetがsuperclassとして用いるAlign Widgetを継承しており、Align WidgetはRenderPositionedBoxと対応しています。
class Center extends Align { ... }
class Align extends SingleChildRenderObjectWidget {
@override
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.of(context),
);
}
}
RenderPositionedBoxは自身の親のサイズがinfinityで指定されていたり、子のサイズから指定させようとしない限りdouble.infinityが自身のサイズとして指定されています。これにより、Centerは親のサイズに合わせて自身のサイズを決めるようになります。
class RenderPositionedBox extends RenderAligningShiftedBox {
@override
void performLayout() {
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);
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));
}
}
Imageの場合
Imageは子Widgetを持たないLeaf Widgetなので、子のサイズを気にする必要はありません。ですが、自分の画像サイズやその他指定されたサイズによって自身のサイズを変形させるWidgetです。Image自身はStatefulWidgetなのでbuildメソッドは持ちませんが、Stateに定義されているbuild()によって生成されるRawImage WidgetはRenderImageというRenderObjectと対応しています。
class RenderImage extends RenderBox {
Size _sizeForConstraints(BoxConstraints constraints) {
// Folds the given |width| and |height| into |constraints| so they can all
// be treated uniformly.
constraints = BoxConstraints.tightFor(
width: _width,
height: _height,
).enforce(constraints);
if (_image == null)
return constraints.smallest;
return constraints.constrainSizeAndAttemptToPreserveAspectRatio(Size(
_image.width.toDouble() / _scale,
_image.height.toDouble() / _scale,
));
}
@override
void performLayout() {
size = _sizeForConstraints(constraints);
}
}
前述した通り子のRenderObjectに影響させるようなものではないため、performLayout()では自分自身のサイズのみ指定しています。_sizedForConstraintsでは、親の制約をもとにアスペクト比を維持しつつ、シンプルに自分のサイズを決めているのがわかります。
まとめ
今回は取り急ぎBoxConstraintsを用いたサイズ計算のみに絞って紹介しました。親や子の影響によって自身のサイズが決まっていくプロセスはシンプルでありながらとても柔軟にできているという印象です。
今回は時間の都合で難しかったですが、Flex Widgetのサイズ計算やポジショニングについても調べてみたいと思います。