1. Constraints
初学者在接触 Flutter 时,经常会有一些疑问,为什么我使用的 widget 没有表现出我期望的样子。比如 Container(width: 200, height: 200, color: Colors.red) 这样一个简单的 Container 为什么不是 200 * 200 而是屏幕大小?
这种问题的答案在于 Widgets 的大小并不是只是由自身决定的,它还会依据父 Widget 的约束(Constraints)来修正自身大小,并最终由父 Widget 确定其位置。
Constraints 有两个直接子类,BoxConstraints 和 RenderSliverConstraints,分别约束 RenderBox 和 RenderSliver。我们说下 BoxConstraints 吧, RenderSliverConstraints 略有些复杂,读者需要可以自行查阅源码去学习。
BoxConstraints 有四个属性 minWidth、maxWidth、minHeight、maxHeight,根据这四个属性我们可以创建”松的“、”紧的“、”无限制的“ 等类型的约束。Widget 布局过程中就会根据具体的约束和它期望的大小确定自己最终的尺寸。
2. Constraints go down. Sizes go up. Parent sets position
Constraints go down. Size go up. Parent sets position,这是 Flutter 布局过程的一个基本原则。如果你在编写 flutter 代码时,发现 Widget 的大小、位置没有展示出如预期般表现,记得这个规则然后重新思考你的布局流程。
3. layout 过程
Flutter 框架执行流程 这篇文章介绍到 drawFrame 方法会调用 pipelineOwner.flushLayout 方法,这里就开始了 Widget 的布局过程。
我们以 rootWidget 为 Container(width: 200, height: 200, color: Colors.red) 为例说明下为什么屏幕中没有出现一个 200 * 200 的红色方块,而是一个全屏的红色背景。
@overridevoid performLayout() {
assert(_rootTransform != null);
_size = configuration.size;
assert(_size.isFinite);
if (child != null)
child!.layout(BoxConstraints.tight(_size));
}
这是 renderView 的 performLayout 方法,其中 configuration 是创建 renderView 时传递给它的参数,其 size 属性被指定为屏幕尺寸。方法最后调用 child.layout 时,创建了一个”紧“的约束,表明它希望子 Widget 是这个大小。我们先看下位于 RenderObject 中的 layout 方法。
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject? relayoutBoundary; ...
if (sizedByParent) // 简化源码
performResize();
...
performLayout();
...
}
这里有几个地方需要讲一下。
relayoutBoundary,这个是布局边界,当 RenderObject 大小或位置等发生变化时,并不会重新 layout 整棵树,而是从 relayoutBoundary 代表的 RenderObject 开始 layout。sizedByParent 表示自己的尺寸是否由 parent 决定,对于 RenderBox 来说,如果这个属性为 true 则由 parent renderObject 根据约束返回最小尺寸。最后调用 performLayout。所以布局流程就这样从 layout 到 performLayout 从上到下一级一级的遍历布局。
Container 初始化时,先根据 width、height 创建了 constraints 对象。
class Container extends StatelessWidget {
Container({ ...
width,
height,
...
}) : constraints = (width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key); ...
}
然后根据条件在 Widget 子树上插入了两个 Widget。
@overrideWidget build(BuildContext context) {
Widget current = child;
...
if (color != null)
current = ColoredBox(color: color, child: current);
...
if (constraints != null)
current = ConstrainedBox(constraints: constraints, child: current);
...
return current;
}
这两个 Widget 对应的 RenderObject 是 RenderConstraintBox 和 _RenderColorBox。
到这里我们直接看 RenderConstraintBox 的 performLayout 吧。
@overridevoid performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
这里 child.layout 传递了 enforce 后的约束,这里会以父节点传递的 constraints 为合并约束,因为父节点为 renderView 它需要子节点的大小(约束)为屏幕大小,所以这里生成的新 constraints 是正好满足屏幕大小的。
叶子节点 RenderObject 是 _RenderColorBox。
@overridevoid performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
performResize();
}
}
@overridevoid performResize() {
// default behavior for subclasses that have sizedByParent = true
size = constraints.smallest;
assert(size.isFinite);
}
可见 performLayout 直接调用 performResize 获取了 constraints 表示约束的最小尺寸,因为这里的约束是个”紧“的约束,就是屏幕尺寸,所以这里的 _renderColorBox 的大小就是屏幕尺寸。
到这里,为什么指定大小的 Container 没有表现预期效果的问题就明确了,同时也能看出 Constraints go down 了吧。
我们再看一眼 _RenderColorBox 的 performLayout 方法。如果 child 不为 null 时,调用 child.layout 进行 child 布局,然后用 child.size 决定自己的 size。这不就是 Sizes go up 吗。那么 Parent sets position 什么时候体现呢,这个在 RenderObject paint 过程中由 offset 属性做偏移指定了。