1 2 3 4 5 6 7 8 9 10 11 12 13
| class RenderBinding { void drawFrame() { pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); if (sendFramesToEngine) { renderView.compositeFrame(); pipelineOwner.flushSemantics(); _firstFrameSent = true; } } }
|
过程
标记阶段
markNeedsLayout方法会将当前节点标记为需要Layout。markNeedsLayout方法在很多地方都会被触发,比如UI的高宽发生变化,字体属性发生变化等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class RenderObject { void markNeedsLayout() { if (_needsLayout) { return; } if (_relayoutBoundary == null) { _needsLayout = true; if (parent != null) { markParentNeedsLayout(); } return; } if (_relayoutBoundary != this) { markParentNeedsLayout(); } else { _needsLayout = true; if (owner != null) { owner!._nodesNeedingLayout.add(this); owner!.requestVisualUpdate(); } } }
void markParentNeedsLayout() { _needsLayout = true; final RenderObject parent = this.parent! as RenderObject; if (!_doingThisLayoutWithCallback) { parent.markNeedsLayout(); } else { } }
}
|
Flush阶段
Layout开始于drawFrame的flushLayout,Layout过程也跟Build类似,先标记再处理。Layout是相对Render Tree而言的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| class PipelineOwner { void flushLayout() { try { while (_nodesNeedingLayout.isNotEmpty) { final List<RenderObject> dirtyNodes = _nodesNeedingLayout; _nodesNeedingLayout = <RenderObject>[]; dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth); for (int i = 0; i < dirtyNodes.length; i++) { if (_shouldMergeDirtyNodes) { _shouldMergeDirtyNodes = false; if (_nodesNeedingLayout.isNotEmpty) { _nodesNeedingLayout.addAll(dirtyNodes.getRange(i, dirtyNodes.length)); break; } } final RenderObject node = dirtyNodes[i]; if (node._needsLayout && node.owner == this) { node._layoutWithoutResize(); } } _shouldMergeDirtyNodes = false; }
for (final PipelineOwner child in _children) { child.flushLayout(); } } finally { _shouldMergeDirtyNodes = false; } }
}
class RenderObject { void _layoutWithoutResize() { RenderObject? debugPreviousActiveLayout; try { performLayout(); markNeedsSemanticsUpdate(); } catch (e, stack) { _reportException('performLayout', e, stack); } _needsLayout = false; markNeedsPaint(); } }
|
_nodesNeedingLayout是需要重新构建的列表,存储所有需要重新布局的节点,一般会通过markNeedsLayout将自己添加到待重新layout列表中。
总结:
drawFrame–>flushLayout–>performLayout–>markNeedsPaint
具体示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| class RenderView { void performLayout() { _size = configuration.size; if (child != null) { child!.layout(BoxConstraints.tight(_size)); } }
}
class RenderObject { void layout(Constraints constraints, { bool parentUsesSize = false }) { if (!kReleaseMode && debugProfileLayoutsEnabled) { Map<String, String>? debugTimelineArguments; } final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject; final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!; if (!_needsLayout && constraints == _constraints) { if (relayoutBoundary != _relayoutBoundary) { _relayoutBoundary = relayoutBoundary; visitChildren(_propagateRelayoutBoundaryToChild); } return; } _constraints = constraints; if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) { visitChildren(_cleanChildRelayoutBoundary); } _relayoutBoundary = relayoutBoundary; if (sizedByParent) { try { performResize(); } catch (e, stack) { } } RenderObject? debugPreviousActiveLayout; try { performLayout(); markNeedsSemanticsUpdate(); } catch (e, stack) { _reportException('performLayout', e, stack); } _needsLayout = false; markNeedsPaint(); } }
|
为了性能考虑,layout过程会使用_relayoutBoundary来优化性能。
布局
布局过程
- 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。
- 子节点根据约束信息确定自己的大小。
- 父节点根据特定布局规则确定每一个子节点在父节点布局控件中的位置,用偏移offset表示。
- 递归整个过程,确定出每一个节点的大小和位置。
layout流程
- 确定当前组件的布局边界。
- 判断是否需要重新布局,如果没必要会直接返回,反之才需要重新布局。不需要布局要同时满足以下三个条件:
- 当前组件没有被标记为需要重新布局。
- 父组件传递的约束没有发生变化。
- 当前组件的布局边界没有发生变化。
- 调用performLayout进行布局,其内部会调用子组件的layout方法。
- 请求绘制。
- 如果有子组件,则对子组件进行递归布局。
- 确定当前组件的大小,通常会依赖子组件的大小。
- 确定子组件在当前组件中的起始偏移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| void layout(Constraints constraints, { bool parentUsesSize = false }) { RenderObject? relayoutBoundary; if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) { relayoutBoundary = this; } else { relayoutBoundary = (parent! as RenderObject)._relayoutBoundary; } if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) { return; } _constraints = constraints; _relayoutBoundary = relayoutBoundary;
if (sizedByParent) { performResize(); } performLayout(); _needsLayout = false; markNeedsPaint(); }
|
命中测试
命中测试用来判断某个组件是否需要响应一个点击事件,其入口是RenderObject Tree的根节点RenderView的hitTest函数
1 2 3 4 5 6
| bool hitTest(HitTestResult result, { Offset position }) { if (child != null) child.hitTest(BoxHitTestResult.wrap(result), position: position); result.add(HitTestEntry(this)); return true; }
|
查看RenderView源码
1 2 3 4 5 6 7 8 9
| bool hitTest(BoxHitTestResult result, { @required Offset position }) { if (_size.contains(position)) { if (hitTestChildren(result, position: position) || hitTestSelf(position)) { result.add(BoxHitTestEntry(this, position)); return true; } } return false; }
|
如果点击事件位置处于RenderObject之内,如果在其内,并且hitTestSelf或者hitTestChildren返回true,则表示RenderObject通过了命中测试,需要响应事件,此时需要将被点击的RenderObject加入BoxHitTestResult列表,同时点击事件不再向下传递。否则认为没有通过命中测试,事件继续向下传递。其中,hitTestSelf函数表示节点是否通过命中测试,hitTestChildren表示子节点是否通过命中测试。
- 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。
- 子节点根据约束信息确定自己的大小(size)。
- 父节点根据特定布局规则(不同布局组件会有不同的布局算法)确定每一个子节点在父节点布局空间中的位置,用偏移 offset 表示。
- 递归整个过程,确定出每一个节点的大小和位置。