理论上,某个组件的布局变化后,就可能会影响其他组件的布局,所以当有组件布局发生变化后,最笨的办法是对整棵组件树 relayout(重新布局)!但是对所有组件进行 relayout 的成本还是太大,所以需要探索一下降低 relayout 成本的方案。实际上,在一些特定场景下,组件发生变化后只需要对部分组件进行重新布局即可(而无需对整棵树 relayout )。
1. 布局边界(relayoutBoundary)
假如有一个页面的组件树结构如图所示:
假如 Text3 的文本长度发生变化,则会导致 Text4 的位置和 Column2 的大小也会变化;又因为 Column2 的父组件 SizedBox 已经限定了大小,所以 SizedBox 的大小和位置都不会变化。所以最终需要进行 relayout 的组件是:Text3、Column2,这里需要注意:
- Text4 是不需要重新布局的,因为 Text4 的大小没有发生变化,只是位置发生变化,而它的位置是在父组件 Column2 布局时确定的。
- 很容易发现:假如 Text3 和 Column2 之间还有其他组件,则这些组件也都是需要 relayout 的。
在本例中,Column2 就是 Text3 的 relayoutBoundary (重新布局的边界节点)。每个组件的 renderObject 中都有一个 _relayoutBoundary
属性指向自身的布局边界节点,如果当前节点布局发生变化后,自身到其布局边界节点路径上的所有的节点都需要 relayout。
那么,一个组件是否是 relayoutBoundary 的条件是什么呢?这里有一个原则和四个场景,原则是“组件自身的大小变化不会影响父组件”,如果一个组件满足以下四种情况之一,则它便是 relayoutBoundary :
- 当前组件父组件的大小不依赖当前组件大小时;这种情况下父组件在布局时会调用子组件布局函数时并会给子组件传递一个 parentUsesSize 参数,该参数为 false 时表示父组件的布局算法不会依赖子组件的大小。
- 组件的大小只取决于父组件传递的约束,而不会依赖后代组件的大小。这样的话后代组件的大小变化就不会影响自身的大小了,这种情况组件的 sizedByParent 属性必须为 true(具体后面会讲)。
- 父组件传递给自身的约束是一个严格约束(固定宽高,下面会讲);这种情况下即使自身的大小依赖后代元素,但也不会影响父组件。
- 组件为根组件;Flutter 应用的根组件是 RenderView,它的默认大小是当前设备屏幕大小。
对应的代码实现是:
// parent is! RenderObject 为 true 时则表示当前组件是根组件,因为只有根组件没有父组件。
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
_relayoutBoundary = this;
} else {
_relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
代码中 if 里的判断条件和上面的 4 条 一一对应,其中除了第二个条件之外( sizedByParent 为 true ),其他的都很直观
2. markNeedsLayout
当组件布局发生变化时,它需要调用 markNeedsLayout
方法来更新布局,它的功能主要有两个:
- 将自身到其 relayoutBoundary 路径上的所有节点标记为 “需要布局” 。
- 请求新的 frame;在新的 frame 中会对标记为“需要布局”的节点重新布局。
看看其核心源码:
void markNeedsLayout() {
_needsLayout = true;
if (_relayoutBoundary != this) { // 如果不是布局边界节点
markParentNeedsLayout(); // 递归调用前节点到其布局边界节点路径上所有节点的方法 markNeedsLayout
} else {// 如果是布局边界节点
if (owner != null) {
// 将布局边界节点加入到 pipelineOwner._nodesNeedingLayout 列表中
owner!._nodesNeedingLayout.add(this);
owner!.requestVisualUpdate();//该函数最终会请求新的 frame
}
}
}
3. flushLayout()
markNeedsLayout 执行完毕后,就会将其 relayoutBoundary 节点添加到 pipelineOwner._nodesNeedingLayout
列表中,然后请求新的 frame,新的 frame 到来时就会执行 drawFrame
方法:
void drawFrame() {
pipelineOwner.flushLayout(); //重新布局
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
...
}
flushLayout() 中会对之前添加到 _nodesNeedingLayout
中的节点重新布局,看一下其核心源码:
void flushLayout() {
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
//按照节点在树中的深度从小到大排序后再重新layout
for (final RenderObject node in dirtyNodes..sort((a,b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize(); //重新布局
}
}
}
看一下 _layoutWithoutResize
实现
void _layoutWithoutResize() {
performLayout(); // 重新布局;会递归布局后代节点
_needsLayout = false;
markNeedsPaint(); //布局更新后,UI也是需要更新的
}