Layout
原文地址:www.flutterinternals.org/rendering/l…
什么是 Relayout Boundary?
- Relayout boundary 是 render tree 中节点之间的逻辑划分点。边界之下的节点永远不会使边界之上的节点的布局失效。
- Relayout boundary 表示为满足一组条件的 render object(
RenderObject._relayoutBoundary
),允许其独立于其父节点进行布局。也就是说,以 relayout boundary 为跟节点的子树无法影响树的其他部分。
- 如果一个 render object 节点的父节点忽略了它的 size(
!parentUseSize
)、完全定义该节点的 size(sizedByParent
)、传入严格约束或它不是一个 render object(例如 RenderView
),那该节点就定义为一个 relayout boundary(即无法影响父节点的布局)。
- 后两种情况表明,无论子树发生什么,子节点都无法更改大小。
parentUsesSize
参数可防止 render object 成为 relayout boundary。因此,将 render object 标记为脏时,所有祖先节点(包括父节点)都将标记为脏,直到最接近的 relayout boundary。
- 当 render object 需要重新布局时,relayout boundary 限制了有多少祖先节点必须标记为脏的 。遍历会在最近的祖先节点 relayout boundary 处停止,因为超出该点的节点不受子孙节点布局的影响。
- 实际布局的时候,relayout boundary 会被忽略。Render object 始终会 layout 子节点。但是,当子节点没有被标记为脏或接收到相同的约束(约束不变自然不需要 layout)时遍历就会停止。
RenderObject.layout
负责执行此逻辑。RenderObject._layoutWithoutResize
无条件执行 layout,因为 PipelineOwner.flushLayout
已经跳过了干净的节点。
布局如何标记 dirty?
PipelineOwner
维护一个脏节点列表(PipelineOwner._nodeNeedingLayout
),通过 PipelineOwner.flushLayout
方法每一帧清空一次该列表。
- 将 render objects 标记为需要布局是可以批量操作的(即,可以在一次遍历中处理布局,而不必多次布局节点)。
- 当通过
RenderObject.markNeedsLayout
设置 RenderObject._needsLayout
来把一个 render object 标记为脏的时候,在最近的 relayout boundary 之下的所有的祖先节点(包括 relayout boundary)也会被标记为 dirty(通过 RenderObject.markParentNeedsLayout
)。
- 只有最近的闭区间中的 relayout boundary 会被添加到
PipelineOwner._nodeNeedingLayout
中。所有子孙节点会遍历执行布局。同时这也会请求下一帧的回调(通过PipelineOwner.requestVisualUpdate
)来让布局变更得以应用(通过 PipelineOwner.flushLayout
)。
- 当处理较早加入 dirty list 的节点的时候,比较晚加入的节点可能会变为 clean;
PipelineOwner.flushLayout
忽略 clean 节点。
- 如果
sizedByParent
标记发生更改,则必须通知 render object 及其父节点(通过 RenderObject.markNeedsLayoutForSizedByParentChange
)。更新此标志可以在渲染树中添加或删除 relayout boundary(更改随后由 RenderObject.layout
提交)。大多数 render object 不会动态更新此标记。
- Dirty list 中的节点是使用
RenderObject._layoutWithoutResize
进行布局的,这不会更新 relayout boundary,因此必须把父节点标记为 dirty。因此,由于子节点的状态可能会改变,因此也必须 layout 其父节点。
- 更改此标志位还意味着子节点将采用新尺寸,从而可能使父节点的布局无效。
约束是如何传递的?
- 从视图配置(
ViewConfiguration.size
)读取时,RenderView
提供了最顶层的约束(通过RenderView.performLayout
)。这以逻辑像素点来表示整个可视界面的尺寸。
- 标记为 dirty 的 render object 使用之前缓存的约束(
RenderObject.constraints
)执行布局。这些约束不能是无效的。否则,该树上方的节点将被标记为脏节点。
- Render object 的尺寸(由
RenderObject.performResize
或 RenderObject.performLayout
计算)可能会影响提供给子节点的约束。约束也可能是完全任意的(例如OverflowBox)。这些约束将转发给任何子节点,然后递归地重复该过程。
- 约束会缓存并用于
RenderObject.layout
。
Layout 是如何执行的?
PipelineOwner.flushLayout
会按照深度递增的顺序触发 dirty list(PipelineOwner._nodesNeedingLayout
)中的所有节点进行 layout。此列表中的节点相当于 relayout boundary, 它们调用 RenderObject._layoutWithoutResize
而不是 RenderObject.layout
进行 layout。
- Dirty List 中的节点永远不需要调用
RenderObject.performResize
。考虑一个由父节点确定 size 的节点。
- 如果父节点不是脏的,传入的 constraints 没有变更。因此,resize 是不必要的。
- 如果父节点被标记为脏,由于节点是从上到下进行清理的,因此将首先对其进行布局。这最终将布局原来的节点(现在可能会重新计算大小),从而有效地将其从脏列表中删除。
RenderObject._layoutWithoutResize
不会更新缓存的 constraints(RenderObject.constraints
)或者 relayout boundary (RenderObject._relayoutBoundary
)。
- 由于祖先节点尚未标记为脏,因此传入的约束不会改变。
- 由于此节点和父节点之间的关系没有改变,因此 Relayout boundary 状态不会变更。如果改变了,父节点也会被标记为脏(通过
RenderObject.markNeedsLayoutForSizedByParentChange
)。
- 当一个子节点被标记为脏、接收到新约束或刚刚成为 relayout boundary 时,将执行布局(通过
RenderObject.layout
)。否则,它将标记为 clean 并中断遍历。
- 注意,布局优化是在标记阶段而不是布局阶段中应用的。当一个子代被标记为脏的节点时,Relayout boundaries 提供了一个限制但没有其他直接的影响。
RenderObject.layout
缓存新的约束 (RenderObject.constraints
) ,并将 relayout boundary 字段 (RenderObject._relayoutBoundary
) 的任何变更传递给受影响的子孙节点 (通过 RenderObject._cleanChildRelayoutBoundary
)。
- 由其父节点调整 size(
RenderObject.sizedByParent
)的 Render objects 仅通过传入的约束来确定新的 size(通过 RenderObject.performResize
)。所有 render objects(包括已调整 size 的)都继续进行 layout (通过 RenderObject.performLayout
).
- 这个方法负责布局所有子节点(通过
RenderObject.layout
)和更新所有子节点的 parent data。
- 子节点有可能在父节点之前或之后进行布局,这取决于父节点如何执行 layout(例如,当父节点的 size 被子节点影响 size 时,必须首先布局它的子节点 )。
- 那些不是由父节点决定 size 的 render object 必须在
RenderObject.performLayout
中确定一个新的 size。
- 完成后,
RenderObject.layout
将节点标记为需要语义化(semantics)和绘制(painting),但不再需要布局。
如何优化布局?
- 某些条件下可以避免布局子节点:
- 如果子节点不是脏的且约束条件不变,则无需布局,因此可中断遍历。
- 如果父节点没有使用子节点的 size,则它无需于子节点一起布局;子节点必然符合原始传入的约束,因此夫节点不会发生任何改变。
- 如果父节点传递了严格(tight)约束给子节点,则它无需于子节点一起布局;子节点不能改变大小。
- 如果子节点为
SizeByParent
(给定的约束完全决定其 size),并且再次传递相同的约束,则父节点无需与子节点一起布局;子节点不能改变大小。
- 沿着树向下布局时会维护最近的上游 relayout boundary。执行布局时(通过
RenderObject.performLayout
),render object 确定其是否满足上述任何优化标准。如果符合,它将自身设置为最近的 relayout boundary。否则,它将采用父节点的 relayout boundary。
- 更新 relayout boundary 后会继续进行布局,即使节点是 clean 的且传入的约束不变。
- 所有子节点必须更新它们所知道的最近 relayout boundary。这是通过递归访问以清除 relayout boundary 并标记 render object 为需要进行布局(通过
RenderObject._cleanRelayoutBOundary
)来实现的。遍历会在他们自己划定的 relayout boundary 的后代处停止(即 RenderObject._relayoutBoundary == this
)。
- 布局可能已无效的节点(即,可能导致布局被跳过的假设不再成立)必须被标记为脏。布局还负责沿受影响的 render object 树向下传递 relayout boundary。
如何构建 UI 以响应布局?
RenderObject.invokeLayoutCallback
允许在布局过程中调用 buidler callback。只能对仍然为脏的子树进行操作,以确保将节点精确地布局一次。这样,可以在布局过程中按需构建子节点(viewports 的需要)和移动节点(进行 global key 重定)。
- 以下几个不变量确保这是有效的:
- 构建是单向的 -- 信息仅沿树向下流动,一次访问一个节点。
- 布局一次性访问每个脏节点。
- 当前子节点下方的构建不能使先前的布局失效;构建信息只会沿着树向下流动。
- 在布局节点后进行够能不能使该节点无效;节点永远不会被重新访问。
- 因此,在以给定的 render object 为根的未访问子树中进行构建是安全的。