布局约束
刚才所说的改变一个控件的高度,有时候并不像刚才所说只是改变一下属性就能起作用,这里涉及到一个布局约束规则。
直接看BoxConstraints的实现,这个类主要定义了minWidth和maxWidth,minHeight和maxHeight这些约束条件,child布局的时候可以根据parent给予的这些条件进行对应的布局。
简单介绍相关的一些术语:
- tightly,如果最小约束(minWidth)和最大约束(maxWidth)都是一样的
- loose,如果最小约束是0.0(不管最大约束);如果最小约束和最大约束都是0.0,就同时是tightly和loose
- bounded,如果最大约束不是infinite
- unbounded,如果最大约束是infinite
- expanding,如果最小约束和最大约束都是infinite
如果一个size满足BoxConstraints的约束,那么它就是constrained的。
既然是parent传递给child的约束条件,当然是在performLayout的时候调起child.layout方法:
void layout(Constraints constraints, { bool parentUsesSize: false }) {
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints
&& relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
if (sizedByParent) {
try {
performResize();
} catch (e, stack) {
_debugReportException('performResize', e, stack);
}
}
RenderObject debugPreviousActiveLayout;
try {
performLayout();
markNeedsSemanticsUpdate();
assert(() { debugAssertDoesMeetConstraints(); return true; }());
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
_needsLayout = false;
markNeedsPaint();
}
开始分析这段代码前一小半,决定relayoutBoundary的值也就是布局边界,一个RenderObject想要重新布局,应该从哪里开始。
parentUsesSize,如果为false也就是,parent的布局并不需要依赖child的布局结果,那么child如果要重新布局并不需要通知parent,布局的边界就是自身了,而parentUsesSize的默认值也是为false,也就是大部分时候也只需自身重新布局。
parentUsesSize,如果为true就是parent的布局要依赖child布局(或者parent的size依赖于child的size,想象一下两个div嵌套的情况),再看如果sizedByParent和constraints.isTight都为false,在这种情况之下relayoutBoundary要指向parent.relayoutBoundary,也就是说child如果要重新布局,必须从relayoutBoundary开始,在RenderObject.markNeedsLayout方法实现里面,最终只会把relayoutBoundary加入到_nodesNeedingLayout列表中,跟isRepaintBoundary处理是几乎一样的;
但是如果constraints.isTight为true,也就是minWidth和maxWidth(或者minHeight和maxHeight)值都一样child的size没有变化的空间,只能在限定死的约束空间中布局,这个时候relayoutBoundary也是指向自身。
最后就是sizedByParent这个属性,说实在看名字不太明白它的意图,但是sizedByParent却决定performResize这个方法会不会调起,但是看了RenderBox.size的setter方法:
set size(Size value) {
assert(!(debugDoingThisResize && debugDoingThisLayout));
assert(sizedByParent || !debugDoingThisResize);
assert(() {
if ((sizedByParent && debugDoingThisResize) ||
(!sizedByParent && debugDoingThisLayout))
return true;
assert(!debugDoingThisResize);
String contract, violation, hint;
if (debugDoingThisLayout) {
assert(sizedByParent);
violation = 'It appears that the size setter was called from performLayout().';
hint = '';
} else {
violation = 'The size setter was called from outside layout (neither performResize() nor performLayout() were being run for this object).';
if (owner != null && owner.debugDoingLayout)
hint = 'Only the object itself can set its size. It is a contract violation for other objects to set it.';
}
if (sizedByParent)
contract = 'Because this RenderBox has sizedByParent set to true, it must set its size in performResize().';
else
contract = 'Because this RenderBox has sizedByParent set to false, it must set its size in performLayout().';
throw new FlutterError(
'RenderBox size setter called incorrectly.\n'
'$violation\n'
'$hint\n'
'$contract\n'
'The RenderBox in question is:\n'
' $this'
);
}());
assert(() {
value = debugAdoptSize(value);
return true;
}());
_size = value;
assert(() { debugAssertDoesMeetConstraints(); return true; }());
}
大致可以总结一下,一般情况下都是都是根据parent给予的约束条件来计算size,而设置size只能在performResize或者performLayout中进行,如果设置sizedByParent为true,则只能在performResize中进行,否则就只能在performLayout中与child的布局同时进行。所以sizedByParent为true也意味着这个RenderObject的size不需要依赖于child的size,完全可以根据parent给予的约束条件可以确定(取最大或者最小的宽度和高度或者根据其他算法);但是为false,自身的size就要在performLayout决定,可能要在child的size和约束条件中计算出来,应该是更为复杂,根据注释sizedByParent默认为false永远都不会有问题的。所以这里sizedByParent为true时把relayoutBoundary指向自身,因为自身的size肯定符合约束的条件,也是提高布局效率的一个关键点。
举一个栗子,在RenderPadding中:
void performLayout() {
_resolve();
assert(_resolvedPadding != null);
if (child == null) {
size = constraints.constrain(new Size(
_resolvedPadding.left + _resolvedPadding.right,
_resolvedPadding.top + _resolvedPadding.bottom
));
return;
}
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);
child.layout(innerConstraints, parentUsesSize: true);
final BoxParentData childParentData = child.parentData;
childParentData.offset = new Offset(_resolvedPadding.left, _resolvedPadding.top);
size = constraints.constrain(new Size(
_resolvedPadding.left + child.size.width + _resolvedPadding.right,
_resolvedPadding.top + child.size.height + _resolvedPadding.bottom
));
}
RenderPadding先让child布局之后,根据child的size,来设置自身的size。这里还涉及到一个Offset的问题,因为layout只是获取了size,但是元素在哪里开始绘制,一般也是由parent控制,当parent设置好每个child的offset之后在绘制的过程中就可以在适当的位置中绘制了。
parentUsesSize & sizedByParent
个人觉得这两个名称是最令人迷惑,所以这里再总结一下:
- sizedByParent 顾名思义控件的大小完全在父控件的约束条件下,例如约束条件maxWidth=100,minWidth=0就意味着子控件的宽度只能在0到100的范围内。
- parentUsesSize 意味着父控件要依赖子控件的size,可能父控件的布局要根据子控件的size来做调整。
还有这两者是否是冲突的尼,能否都为true?
根据我的分析这两个应该是不会造成冲突的,在选定布局的边界情况下,刚才代码中:
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
如果parentUsesSize为true,布局边界毫无疑问指向了parent,也就是子控件要重新布局必须先从父控件开始,因为父控件需要用到子控件重新布局后的结果,所以在选定布局边界的问题上parentUsesSize起到决定性的作用。
如果sizedByParent和parentUsesSize都为true,例如父控件把maxWidth=100,minWidth=0这样的约束条件传递给子控件,意味着子控件布局的范围只能在0到100之间,假设子控件最终的size为50,父控件可能直接会使用子控件的size作为自身的size,这样一看好像现在父控件的布局范围现在应该是0到50,但其实是并不会影响一开始传递给子控件的约束条件0到100之间,所以不会触发一个循环布局。
Layer
继续PipelineOwner处理流程,在flushLayout之后就是flushCompositingBits方法,而flushCompositingBits目标是为每个RenderObject设置适当needCompositing值,这影响flutter最终会生成多少层Layer,而这些Layer会组成一棵Layer Tree并交由引擎最终composite成一帧画面。
总结之前的,现在我们可以得出以下一张的关系图:
再对比一下之前的Chromium文档里面的一张图:
这种关系不言而喻,Flutter确实就是一个super webview。
继续flushCompositingBits方法的深入:
void flushCompositingBits() {
Timeline.startSync('Compositing bits');
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits();
}
_nodesNeedingCompositingBitsUpdate.clear();
Timeline.finishSync();
}
跟之前flushLayout差不多,那么啥时候RenderObject会加入到PipelineOwner._needsCompositingBitsUpdate列表上尼?
扫了一下代码,发现除了框架初始化以外,一般都是在添加child和删除child的时候,调起RenderObject.markNeedsCompositingBitsUpdate方法。
继续_updateCompositingBits方法:
void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
visitChildren((RenderObject child) {
child._updateCompositingBits();
if (child.needsCompositing)
_needsCompositing = true;
});
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing)
markNeedsPaint();
_needsCompositingBitsUpdate = false;
}
举个栗子,最初可能是这样的,RenderObject添加一个新的child,而这个child是被设置为alwaysNeedsCompositing
经过_updateCompositingBits处理后:
而needsComposting这个属性会用在那里尼?同样扫一遍代码,都可以发现类似的处理:
if (needsCompositing) {
pushLayer(new ClipRectLayer(clipRect: offsetClipRect), painter, offset, childPaintBounds: offsetClipRect);
} else {
canvas
..save()
..clipRect(offsetClipRect);
painter(this, offset);
canvas
..restore();
}
如果needsCompositing为true,都会创建一个新的Layer,所以needsCompositing更多像一个暗示的作用,在clip,transform或者设置opacity都会创建一个Layer来处理,这样可以把一些经常变化的区域隔离开来,每次只需要绘制这部分区域来提高效率。
接着flushPaint方法:
void flushPaint() {
Timeline.startSync('Paint', arguments: timelineWhitelistArguments);
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// Sort the dirty nodes in reverse order (deepest first).
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
} finally {
Timeline.finishSync();
}
}
在repaintCompositedChild方法里面,会为RenderObject创建一个属于自己的Layer,其实也只限于isRepaintBoundary为true的RenderObject,因为只有这样的RenderObject才可以加入到_nodesNeedingPaint列表中:
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent: false }) {
if (child._layer == null) {
child._layer = new OffsetLayer();
} else {
child._layer.removeAllChildren();
}
final PaintingContext childContext = new PaintingContext._(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
childContext._stopRecordingIfNeeded();
}
接着就是创建PaintingContext,就像前端需要获取Canvas2D Context一样,_paintWithContext最终会调起RenderObject.paint方法,在paint方法里面我们就可以自由绘制了,但是一般情况下CustomPaint组件就可以满足我们的需求。
再看一下PaintingContext类中:
Canvas get canvas {
if (_canvas == null)
_startRecording();
return _canvas;
}
void _startRecording() {
_currentLayer = new PictureLayer(canvasBounds);
_recorder = new ui.PictureRecorder();
_canvas = new Canvas(_recorder, canvasBounds);
_containerLayer.append(_currentLayer);
}
void _stopRecordingIfNeeded() {
if (!_isRecording)
return;
_currentLayer.picture = _recorder.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
当我们获取canvas做绘制操作的时候,每次都会创建一个新的Canvas对象,并使用使用ui.PictureRecorder记录我们在canvas的操作,最后_stopRecordingIfNeeded会从recorder上获取到绘制的picture,感觉这里有涉及到底层解析得不太好。。。
好吧,当flushPaint完成后,Layer Tree也构建出来了,最后就是composite阶段了,回到RenderView.compositeFrame方法:
void compositeFrame() {
Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
try {
final ui.SceneBuilder builder = new ui.SceneBuilder();
layer.addToScene(builder, Offset.zero);
final ui.Scene scene = builder.build();
ui.window.render(scene);
scene.dispose();
} finally {
Timeline.finishSync();
}
}
Flutter会把所有的Layer都加入到ui.SceneBuilder对象中,然后ui.SceneBuilder会构建出ui.Scene(场景),交给ui.window.render方法去做最后真实渲染,之后就是底层引擎的工作内容,有机会再去更加深入去学习吧。
结束
大致把整个布局渲染流程梳理一遍,感觉越到后面越吃力,有点超出认知的范围,也证明自己知识面仍然有很多不足的地方,如果有什么错漏的地方希望大家能够指正。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。