布局过程
Layout(布局)过程主要是确定每一个组件的布局信息(大小和位置),Flutter 的布局过程如下:
- 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。
- 子节点根据约束信息确定自己的大小(size)。
- 父节点根据特定布局规则(不同布局组件会有不同的布局算法)确定每一个子节点在父节点布局空间中的位置,用偏移 offset 表示。
- 递归整个过程,确定出每一个节点的大小和位置。
可以看到,组件的大小是由自身决定的,而组件的位置是由父组件决定的。
下面是官网的一张图,它用三句话描述了 Flutter 布局过程的精髓:
Flutter 中的布局类组件很多,根据孩子数量可以分为单子组件和多子组件,下面我们先通过分别自定义一个单子组件和多子组件来直观理解一下Flutter的布局过程,之后会介绍一下布局更新过程和 Flutter 中的 Constraints(约束)。
单子组件布局示例
我们实现一个单子组件 CustomCenter
,功能基本和 Center
组件对齐,通过这个实例我们演示一下布局的主要流程。
首先,我们定义组件,为了介绍布局原理,我们不采用组合的方式来实现组件,而是直接通过定制 RenderObject
的方式来实现。因为居中组件需要包含一个子节点,所以我们直接继承 SingleChildRenderObjectWidget
。
class CustomCenter extends SingleChildRenderObjectWidget {
const CustomCenter2({
Key? key, required Widget child})
: super(key: key, child: child);
RenderObject createRenderObject(BuildContext context) {
return RenderCustomCenter();
}
}
接着实现 RenderCustomCenter
。这里直接继承 RenderObject
会更接近底层一点,但这需要我们自己手动实现一些和布局无关的东西,比如事件分发等逻辑。为了更聚焦布局本身,我们选择继承自RenderShiftedBox
,它是RenderBox
的子类(RenderBox
继承自RenderObject
),它会帮我们实现布局之外的一些功能,这样我们只需要重写performLayout
,在该函数中实现子节点居中算法即可。
class RenderCustomCenter extends RenderShiftedBox {
RenderCustomCenter({
RenderBox? child}) : super(child);
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;
}
}
布局过程请参考注释,在此需要额外说明有3点:
- 在对子节点进行布局时,
constraints
是CustomCenter
的父组件传递给自己的约束信息,我们传递给子节点的约束信息是constraints.loosen()
,下面看一下loosen
的实现源码:
BoxConstraints loosen() {
return BoxConstraints(
minWidth: 0.0,
maxWidth: maxWidth,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
很明显,CustomCenter
约束子节点最大宽高不超过自身的最大宽高。
-
子节点在父节点(
CustomCenter
)的约束下,确定自己的宽高;此时CustomCenter
会根据子节点的宽高确定自己的宽高,上面代码的逻辑是,如果CustomCenter
父节点传递给它最大宽高约束是无限大时,它的宽高会设置为它子节点的宽高。注意,如果这时将CustomCenter
的宽高也设置为无限大就会有问题,因为在一个无限大的范围内自己的宽高也是无限大的话,那么实际上的宽高到底是多大,它的父节点会懵逼的!屏幕的大小是固定的,这显然不合理。如果CustomCenter
父节点传递给它的最大宽高约束不是无限大,那么是可以指定自己的宽高为无限大的,因为在一个有限的空间内,子节点如果说自己无限大,那么最大也就是父节点的大小。所以,简而言之,CustomCenter
会尽可能让自己填满父元素的空间。 -
CustomCenter
确定了自己的大小和子节点大小之后就可以确定子节点的位置了,根据居中算法,将子节点的原点坐标算出后保存在子节点的parentData
中,在后续的绘制阶段会用到,具体怎么用,我们看一下RenderShiftedBox
中默认的paint
实现:
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),通常会依赖子组件的大小。
- 确定子组件在当前组件中的起始偏移。
在Flutter组件库中,有一些常用的单子组件比如 Align、SizedBox、DecoratedBox
等,都可以打开源码去看看其实现。
下面我们看一个多子组件的例子。
多子组件布局示例
实际开发中我们会经常用到贴边左-右布局,现在我们就来实现一个 LeftRightBox
组件来实现左-右布局,因为LeftRightBox
有两个孩子,用一个 Widget 数组来保存子组件。
首先我们定义组件,与单子组件不同的是多子组件需要继承自 MultiChildRenderObjectWidget
:文章来源:https://www.toymoban.com/news/detail-485222.html
lass LeftRightBox extends MultiChildRenderObjectWidget {
LeftRightBox({
Key? key,
required List<Widget> children,
}) : assert(children.length == 2, "只能传两个children"),
super(key: key, children: children);
RenderObject createRenderObject(BuildContext context) {
return RenderLeftRight();
}
}
接下来需要实现 RenderLeftRight
,在其 performLayout
中我们实现实现左-右布局算法:文章来源地址https://www.toymoban.com/news/detail-485222.html
class LeftRightParentData extends ContainerBoxParentData<RenderBox> {
}
class RenderLeftRight extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, LeftRightParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, LeftRightParentData> {
// 初始化每一个child的parentData
void setupParentData(RenderBox child) {
if (child.parentData is! LeftRightParentData)
child.parentData = LeftRightParentData();
}
void performLayout() {
final BoxConstraints constraints = this.constraints;
RenderBox leftChild = firstChild!;
LeftRightParentData childParentData =
leftChild.parentData! as LeftRightParentData;
RenderBox rightChild = childParentData.nextSibling!;
//我们限制右孩子宽度不超过总宽度一半
rightChild.layout(
constraints.copyWith(maxWidth: constraints.maxWidth / 2),
parentUsesSize
到了这里,关于Flutter 笔记 | Flutter 核心原理(三)布局(Layout )过程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!