Layout(布局)过程主要是确定每一个组件的布局信息(大小和位置),Flutter 的布局过程如下:
- 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。
- 子节点根据约束信息确定自己的大小(size)。
- 父节点根据特定布局规则(不同布局组件会有不同的布局算法)确定每一个子节点在父节点布局空间中的位置,用偏移 offset 表示。
- 递归整个过程,确定出每一个节点的大小和位置。
可以看到,组件的大小是由自身决定的,而组件的位置是由父组件决定的。
Flutter 中的布局类组件很多,根据孩子数量可以分为单子组件和多子组件。
单子组件布局示例(CustomCenter)
实现一个单子组件 CustomCenter,功能基本和 Center 组件对齐,通过这个实例演示一下布局的主要流程。
首先,定义组件,为了介绍布局原理,不采用组合的方式来实现组件,而是直接通过定制 RenderObject 的方式来实现。因为居中组件需要包含一个子节点,所以直接继承 SingleChildRenderObjectWidget。
class CustomCenter extends SingleChildRenderObjectWidget {
const CustomCenter2({Key? key, required Widget child})
: super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCustomCenter();
}
}
接着实现 RenderCustomCenter,重写performLayout,在该函数中实现子节点居中算法即可
class RenderCustomCenter extends RenderShiftedBox {
RenderCustomCenter({RenderBox? child}) : super(child);
@override
void performLayout() {
//1. 先对子组件进行layout,随后获取它的size
child!.layout(
constraints.loosen(), //将约束传递给子节点
parentUsesSize: true, // 因为我们接下来要使用child的size,所以不能为false
);
//2.根据子组件的大小确定自身的大小
size = constraints.constrain(Size(
constraints.maxWidth == double.infinity
? child!.size.width
: double.infinity,
constraints.maxHeight == double.infinity
? child!.size.height
: double.infinity,
));
// 3. 根据父节点子节点的大小,算出子节点在父节点中居中之后的偏移,然后将这个偏移保存在
// 子节点的parentData中,在后续的绘制阶段,会用到。
BoxParentData parentData = child!.parentData as BoxParentData;
parentData.offset = ((size - child!.size) as Offset) / 2;
}
}
1.在对子节点进行布局时, constraints 是 CustomCenter 的父组件传递给自己的约束信息,传递给子节点的约束信息是constraints.loosen()
BoxConstraints loosen() {
return BoxConstraints(
minWidth: 0.0,
maxWidth: maxWidth,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
很明显,CustomCenter 约束子节点最大宽高不超过自身的最大宽高
- 子节点在父节点(CustomCenter)的约束下,确定自己的宽高;此时CustomCenter会根据子节点的宽高确定自己的宽高,上面代码的逻辑是,如果CustomCenter父节点传递给它最大宽高约束是无限大时,它的宽高会设置为它子节点的宽高。注意,如果这时将CustomCenter的宽高也设置为无限大就会有问题,因为在一个无限大的范围内自己的宽高也是无限大的话,那么实际上的宽高到底是多大,它的父节点会懵逼的!屏幕的大小是固定的,这显然不合理。如果CustomCenter父节点传递给它的最大宽高约束不是无限大,那么是可以指定自己的宽高为无限大的,因为在一个有限的空间内,子节点如果说自己无限大,那么最大也就是父节点的大小。所以,简而言之,CustomCenter 会尽可能让自己填满父元素的空间。
3.CustomCenter 确定了自己的大小和子节点大小之后就可以确定子节点的位置了,根据居中算法,将子节点的原点坐标算出后保存在子节点的 parentData 中,在后续的绘制阶段会用到,具体怎么用,看一下RenderShiftedBox中默认的 paint 实现:
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
//从child.parentData中取出子节点相对当前节点的偏移,加上当前节点在屏幕中的偏移,
//便是子节点在屏幕中的偏移。
context.paintChild(child!, childParentData.offset + offset);
}
}
performLayout 流程
可以看到,布局的逻辑是在 performLayout 方法中实现的。梳理一下 performLayout 中具体做的事:
- 如果有子组件,则对子组件进行递归布局。
- 确定当前组件的大小(size),通常会依赖子组件的大小。
- 确定子组件在当前组件中的起始偏移。