Flutter 必知必会系列 —— Render 树的布局绘制

1,047 阅读8分钟

这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战

往期精彩

👉  Flutter 必知必会系列——三颗树到底是什么

👉  Flutter 必知必会系列—— Element 的更新复用机制

👉  Flutter 必知必会系列 —— Element 更新实战

👉  深入理解Flutter布局约束

从之前的文章中,我们知道了 Widget、Element 和 RenderObject 的分工,其中 RenderObject 负责布局测量。

下面我们就看一下整个布局过程。这里主要解决三个问题:1、深入理解 RenderObject   2、从哪里开始布局的   3、怎么布局的

深入理解 RenderObject

在前面的三棵树文章中,我们知道了 RenderObject 的概念,为了更好的理解布局绘制过程,我们再深邃的看一眼 RenderObject,毕竟它是整个布局的载体。

官方文档中写了大量的篇幅来介绍,我先看它是怎么介绍的。

未命名1.001.jpeg

  从文档的描述我们可以知道以下几点:

RenderObject 是渲染的核心

RenderObject 实现了布局和绘制协议,但是具体子类去计算如何布局。

RenderObject 持有父节点,持有父节点关心的数据。

下面我们继续看:

未命名2.002.jpeg

上面我们可以知道以下几点:

想要自定义的话,大多数情况下,继承自RenderBox即可

除了定义含义之外,文档中还介绍了,布局的协议。

未命名3.001.jpeg

上面我们可以知道以下几点:

布局就是输入+输出。RenderBox 输入的盒子约束,输出的是尺寸。

**子节点具体布局是 performLayout方法 **

并且 约束是可以沿着渲染树从上到下的传递,Flutter APP 的根结点是 RenderView。布局过程就是两个方法:performLayoutlayout

小结

上面我们介绍了渲染对象,知道了渲染对象的基本信息。 RenderObject 是整个渲染的核心,定义了基本的布局协议,RenderBox 实现了我们熟知的盒子约束,输入的是盒子约束,输出的是Size。并且约束自顶向下传递,传递到 layout 方法中。实现布局的方法主要有两个 performLayout 和 layout 方法。

从哪里开始布局的 

上面我们介绍了布局就是两个方法:performLayout 和 layout 方法,那么起点是谁呢?

关系是这样的,在一个渲染对象中。 performLayout 方法会调用子节点的layout方法,然后子节点的layout 方法中,会继续调用本节点的 performLayout 方法

类似于这样的:

performLayout.png

Flutter 的根节点是 RenderView,所以布局的起点是 RenderView 的 performLayout 方法。

按照上面的图,应该是 RenderVie w的父节点来发起 RenderView 的布局,但是 RenderView没有父节点,那是谁来发起的呢?

这就是根的作用,在 Flutter 创建三棵树的根节点的时候。会调用下面的代码(RenderView)后面我们讲 binding 的时候会详细讲:

image.png

其中的 markNeedsLayout 就是发起了布局的流程。

下面我们看 RenderView 的 performLayout 方法:

@override
void performLayout() {
  _size = configuration.size;
  if (child != null)
    child!.layout(BoxConstraints.tight(_size));
}

做了两件非常重要的事:

确定了尺寸 size。这个 size 就是 window 的宽高,一般是** FlutterView 的宽高**

发起子节点的布局流程,并且约束是 tight 的,强制子节点就是 window 宽高

小结

布局的 起点是 RenderView 的 performLayout,在 performLayout 中确定了整个 Flutter App 的尺寸,发起了子孙节点的布局流程。

下面,我们看复杂的布局流程。

怎么布局的

上面我们知道了,布局的作用就是:根据输入明确输出,输入的是盒子约束,输出的是 尺寸 size

并且布局的流程是从 layout 方法开始的,在内部调用 performLayout 方法。下面我们看这两个方法是怎么确定输入和输出的。

定义概念的 layout 方法

布局描述

老规矩我们先看方法的描述

Compute the layout for this render object.
为 render object 执行 layout

This method is the main entry point for parents to ask their 
children to update their layout information. The parent passes 
a constraints object, which informs the child as to which
layouts are permissible. The child is required to obey the 
given constraints.
parent通过此方法来让子节点更新布局信息,并且 parent 会传递一个约束对象,
子需要遵守约束。

If the parent reads information computed during the child's 
layout, the parent must pass true for `parentUsesSize`. In 
that case, the parent will be marked as needing layout 
whenever the child is marked as needing layout because the 
parent's layout information depends on the child's layout 
information. If the parent uses the default value (false) for
`parentUsesSize`, the child can change its layout information 
(subject to the given constraints) without informing the 
parent.
parentUsesSize 表示父 Widget 是否要依赖子的布局信息。默认是 false
     如果parentUsesSize为 true,则表明父依赖子。那么当子被标记需要
        layout的时候(markNeedsLayout,那么父也会被标记需要 layout。
     为 false(父不依赖子),子节点要重新布局的时候并不需要通知 parent,
        布局的边界就是自身了。

Subclasses should not override [layout] directly. Instead, 
they should override [performResize] and/or [performLayout]. The [layout] method delegates the actual work to
[performResize] and [performLayout].
子类最好不直接覆写 layout 方法。相反,应该覆写 performResize和 
performLayout方法。

The parent's [performLayout] method should call the [layout] 
of all its children unconditionally. It is the [layout] 
method's responsibility (as implemented here) to return early 
if the child does not need to do any work to update its layout 
information.
父节点的 performLayout 方法需要调用每一个字节点的 layout 方法。layout
可以尽早的返回布局信息。

上面描述我们知道以下几点:

layout 方法的作用是执行布局过程

执行布局过程,需要两个参数:约束和 parentUsesSize

具体的布局计算,应该在 performResize 和 performLayout 中,有点类似 Android 的 onLayout 和 layout 方法

布局边界

在讲解布局之前,我们先了解一个概念:布局的边界

当我们布局 A节点 的时候,如果 A节点 的父节点依赖 A节点 的布局信息,那么 A节点 的父节点就是 A节点 的上界,当 A节点 标记为需要布局的时候,A节点 的上界节点们,也会被标记为需要布局。

image.png

这个查找过程是什么呢?就是之前介绍的 RenderObject持有父节点的引用。 也就是 parent 属性。

那么判断是否是上界的标准是什么?父节点是否依赖子节点的信息。(这句话貌似是废话,听君一席话,如听一席话)

Flutter 的渲染节点中,只要边界不是自己,那么边界就是上层。
边界是自己的:

parentUsesSize 参数为 false

节点的大小只和自己有关系,sizedByParent 为 true,约束类型是constraints.isTight

节点是根节点

除了这三种情况,剩下的边界都是上层

layout 布局流程

介绍了前置铺垫之后,下面我们看具体的布局流程。整个流程也就 50 行代码不到

image.png

整个流程可以分为两部分:找到布局上界发起布局绘制

先看第一部分

if (!parentUsesSize || sizedByParent || constraints.isTight || 
parent is! RenderObject) {
  relayoutBoundary = this;
} else {
  relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}

如果parentUsesSize是false,那么绘制上界就是自己。
如果sizedByParent是true,绘制边界也是自己。
如果约束是紧凑的,constraints.isTight。对于盒子约束来说就是宽高是确定的。
父节点不是渲染节点, 就是根节点。

else 里面的就不是这四种,边界就是 父边界

sizeByParent 的意思是,自己的大小是 父节点 确定的。这个值一般是false,只有几种特殊的节点才是 true。

比如:我们熟悉的 Offstage 组件,offstage 属性是 bool 值,offstage是true,Offstage不显示,绘制边界就是自己。

 还有 RenderViewport 节点,它的大小是父布局确定的,比如ListView的大小就是 200*300

接着往下看:

if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
  return;
}

如果新旧两次的信息都是一样的,说明不用布局了。

节点没有标记为 “脏“
约束是一样的
边界是一样的

再往下看:

_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
  visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;

走到这里说明确实需要布局了,那就 :

  • 保存约束信息 constraints
  • 清空子节点的上界信息
  • 保存上界 relayoutBoundary 信息

上面已经完成了布局上界的查找过程。

image2021-11-25_14-44-39.png

再先看第二部分

第一个流程判断点:是否是 sizedByParent

sizedByParent 是一个属性,默认是 false。
只有下面的这几类型的节点是 true,其他的全是 false

image.png

if (sizedByParent) {
  try {
    performResize();
  } catch (e, stack) {
  }
}

如果宽高是 父节点 确定的,就可以直接确定下来 Size

比如:
Offstage 不显示子组件的时候,size 就是最小约束 0*0 image2021-11-25_16-51-19.png

Viewport 的 size,就是最大的 size,也就是父布局的最大空间。
Viewport 是承载 Listview 的渲染节点,所以我们不能把 ListView 放到 不能确定宽高的容器里。

image2021-11-25_16-53-5.png

接下来在看,最终的布局

try {
  performLayout();
  markNeedsSemanticsUpdate();
} catch (e, stack) {
}
_needsLayout = false;
markNeedsPaint();

就是执行 performlayout 方法,上面我们讲了,performlayout 方法就是具体的布局

比如,我们熟知的 LayoutBuilder。它在 performLayout阶段 等child布局之后,才具体确定自己的宽高 Size。并且它的布局就是子节点的布局

image2021-11-25_17-15-48.png

布局之后标识位设置为 false,并且发起在一个绘制的流程。这就是完整的布局流程。

image.png

小结

我们梳理了完整的布局协议,从布局边界的确定到发起绘制。其中大部分流程是统一的,各个子类只需要实现 performLayout 方法。

在方法中实现自己的布局行为,比如显示多大啊,有没有位移之类的。

总结 

布局是 Flutter 的渲染核心,宏观流程集中在 RenderObject 的 layout 方法中,处理了输入(约束)与输出(size)的关系。

子节点可以在 performLayout 方法中,实现自己的布局行为。这一篇较为理论,下一篇根据具体的例子具体分析不同场景下的布局行为。