flutter —— 深入理解 RenderObject 布局与绘制
参考文章:RenderObject 的布局与绘制
1. relayoutBoundary
重布局边界。该参数用于表示子节点布局变化是否影响父节点,如果为true,当子节点布局发生变化时父节点都会标记为需要重新布局,如果为false,则子节点布局发生变化后不会影响父节点。
标记重新布局(当节点需要重新布局时会调用此方法,如 setState 方法可能会触发重新布局)
void markNeedsLayout() {
// ...
assert(_relayoutBoundary != null);
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
...
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
该方法会从当前节点向父级查找,直到找到一个relayoutBoundary的节点,然后会将它添加到一个全局的nodesNeedingLayout列表中;如果直到根节点也没有找到relayoutBoundary,则将根节点添加到nodesNeedingLayout列表中。
节点的布局方法(如果当前节点被标记为需重新布局,则在渲染管线的 flushLayout 方法中会对此节点执行 performLayout 进行重新布局)
@override
void performLayout() {
// 如果当前节点有子节点,通常会对先对子节点进行layout,同时可以获取它的 size
child!.layout(
constraints.loosen(),
parentUsesSize: true,
);
}
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 (sizedByParent) {
performResize();
}
// 再次调用 performLayout。如果该节点又有子节点,performLayout 中会再次调用 layout 对子节点进行布局。这样递归后,等待所有子节点都布局完了,再进行当前节点的布局。
performLayout();
...
}
2. performResize()
当 sizedByParent 为true 时,节点的大小仅通过 parent 传给它的 constraints 就可以确定了,即该节点的大小与它自身的属性和其子节点无关,此时其大小在 performResize() 中就确定了,performLayout() 不能用于改变组件大小。
class _RenderCircleBox extends RenderProxyBox {
_RenderCircleBox();
@override
bool get sizedByParent => true;
@override
void performResize() {
super.performResize();
size = constraints.constrain(Size(constraints.maxWidth, 100));
}
@override
void performLayout() {
}
}
3. RepaintBoundary
独立绘制边界
1)设置边界的情况下,自定义 RenderBox 组件,默认布局,在绘制时 offset 是相对当前 Layer(可以理解为画布)进行偏移。
class RenderRepaintBoundary extends RenderProxyBox {
/// Creates a repaint boundary around [child].
RenderRepaintBoundary({ RenderBox child }) : super(child);
@override
void paint(PaintingContext context, Offset offset) {
// 参数列表中的 offset 是相对当前 layer 的偏移量。同时 drawCircle 的起始偏移也是相对于当前 layer 进行偏移。
// 如果 isRepaintBoundary 返回 true,节点自身就是 layer,那么参数列表中的 offset 等于零,
// drawCircle 则是相当于当前节点自身位置进行绘制。
context.canvas.drawCircle(Offset(10, 10), size.width / 2, Paint());
}
// 也可直接用 RepaintBoundary 对当前组件进行包装
@override
bool get isRepaintBoundary => true;
}
2)如果没有该边界,别的组件重绘(如执行动画、CustomPainter 中 shouldRepaint 返回 true 等)会导致整个 Layer 重绘,继而导致包含在同一个 Layer 的当前组件重绘;反之,当前组件重绘也会影响别的组件(如果有边界,在绘制时仅会重绘自身而无需重绘它的 parent)。
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
//.....
// 默认情况 RenderObject 不创建独立绘制边界,自定义组件可以重写该方法返回值
bool get isRepaintBoundary => false;
//......
}
示例:共享 Layer 导致的重绘
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
print('home dd');
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleBox(),
SquareBox(),
],
)),
);
}
}
class CircleBox extends LeafRenderObjectWidget {
const CircleBox({super.key});
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderCircleBox();
}
}
class _RenderCircleBox extends RenderProxyBox {
_RenderCircleBox();
@override
void performLayout() {
super.performLayout();
size = constraints.constrain(Size(100, 100));
}
@override
void paint(PaintingContext context, Offset offset) {
print('circle painting...');
context.canvas.drawCircle(offset.translate(50, 50), size.width / 2, Paint());
}
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
return false;
}
// 由于 isRepaintBoundary 返回 false,即自身不是重绘边界,会受到同一层 Layer 影响。故 SquareBox 点击时触发的动画导致 Layer 重绘,继而导致 CircleBox 重绘。
// 具体来说,SquareBox 节点在因为动画触发的回调 setState ,造成节点标记重绘(markNeedsPaint)的过程中,节点向上遍历将遇到结界之前所经过的每个节点都添加 _needsPaint 标记,将最终的绘制边界节点加入到 _nodesNeedingPaint 列表中。
// 最终在执行 flushPaint 过程中,遍历边界节点列表(即 _nodesNeedingPaint)并依次执行 repaintCompositedChild,将每个 layer 的所有子节点分别进行重绘。
@override
bool get isRepaintBoundary => false;
}
class SquareBox extends StatefulWidget {
const SquareBox({Key? key}) : super(key: key);
@override
State<SquareBox> createState() => _SquareBoxState();
}
class _SquareBoxState extends State<SquareBox> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
print('square build');
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_controller.forward(from: 0);
},
child: RotationTransition(
turns: Tween(begin: 0.0, end: 0.5).animate(_controller),
child: Container(
color: Colors.green,
height: 200,
width: 200,
),
),
);
}
}
关于组件重绘的补充说明
注1: 节点是否重绘,主要依据的是当前节点是否有被标记加入到 _nodesNeedingPaint,通常是调用 markNeedsPaint,而不是节点是否执行了 updateRenderObject 或 widget 是否执行 build 等方法。
// 标记重新绘制
void markNeedsPaint() {
if (_needsPaint) {
return;
}
// 将节点的 _needsPaint 置为 true,表示需要重绘
_needsPaint = true;
// If this was not previously a repaint boundary it will not have
// a layer we can paint from.
if (isRepaintBoundary && _wasRepaintBoundary) {
// 对绘制边界进行标记重绘
if (owner != null) {
owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate();
}
} else if (parent is RenderObject) {
final RenderObject parent = this.parent! as RenderObject;
// 向上递归调用
parent.markNeedsPaint();
assert(parent == this.parent);
} else {
// 根节点
if (owner != null) {
owner!.requestVisualUpdate();
}
}
}
该方法和 markNeedsLayout 方法功能类似,也会从当前节点向父级查找,直到找到一个isRepaintBoundary属性为true的父节点,然后将它添加到一个全局的nodesNeedingPaint列表中;由于根节点(RenderView)的 isRepaintBoundary 为 true,所以必会找到一个。查找过程结束后会调用buildOwner.requestVisualUpdate方法,该方法最终会调用scheduleFrame(),该方法中会先判断是否已经请求过新的frame,如果没有则请求一个新的frame。
注2:标记重绘后,具体的重绘操作发生在渲染管线流程中的 flushPaint 部分
以下内容参考 setState 的执行流程说明
渲染管线的基本流程 WidgetsBinding.drawFrame
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); //重新构建widget树(很多操作在这一步执行,如上面提到的 markNeedsLayout,还有子节点递归调用 updateRenderObject 时可能会发生的 markNeedsPaint)
pipelineOwner.flushLayout(); // 更新布局(如果有布局需要更新的话,更新布局的过程中会调用 markNeedsPaint)
pipelineOwner.flushCompositingBits(); //更新合成信息
pipelineOwner.flushPaint(); // 更新绘制
if (sendFramesToEngine) {
renderView.compositeFrame(); // 上屏,会将绘制出的bit数据发送给GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
关于 setState 中其 widget 树的重建这一流程的具体实现,后续再作补充说明。
问:setState 是否会造成节点重绘?
答:setState 执行的是 Element 的 markNeedsBuild,标记完了之后会触发新的 frame的绘制流程(WidgetsBinding.drawFrame,亦即管线渲染流程),管线渲染过程中会执行五步流程,其中包括 widget 重建(即 buildOwner!.buildScope(renderViewElement!),此过程中,对于 ComponentElement 组件,先执行其 rebuild 方法,再执行其 build 方法,最终会调用 StatelessWidget 或 State 的 build 方法),更新布局(即 flushLayout 流程)与更新绘制流程(即 flushPaint 流程),但不一定会导致节点重绘。具体来说, 当 widget 重建导致布局信息变化时,则可能会触发节点重绘(更新布局时会对每一个 renderObject 重新布局,调用其 layout 方法,layout 方法中会调用其 markNeedsPaint 方法,即标记重绘),待重绘的节点会被添加到 _nodesNeedingPaint 列表,在 flushPaint 流程中会遍历该列表,并调用节点 RenderObject 的 paint 方法进行重绘。
除了由 widget 重建导致布局信息变化引发的重绘,还有下列情况也会造成节点重绘:
- 对于子节点是 RenderObjectWidget 组件在执行 updateRenderObject 方法时,如果 renderObject 属性变化,则一般都会调用 markNeedsPaint() 重绘 。如“示例:共享 Layer 导致的重绘” 就是因为动画更新时(触发 listenable 回调)调用 setState,然后RenderTransform(本质是 RenderObject)执行 updateRenderObject,其属性 transform 发生变化,最终调用 markNeedsPaint() 重绘。
- 对于子节点是 CustomPaint(本质也是 RenderObjectWidget 组件),在执行 updateRenderObject 方法会调用 set painter 属性方法,该方法执行时判断 CustomPainter 组件的 shouldRepaint 方法是否返回 true,若返回 true 则会调用 markNeedsPaint() 重绘。
问:为什么 setState 会触发子节点执行 updateRenderObject 方法?
答:widget 树触发重建时,ComponentElement 在 rebuild 的流程中,会将 build 返回的 widget 传入 updateChild 方法,然后执行 update 方法(在 update 之前还会执行 hasSameSuperclass && Widget.canUpdate(child.widget, newWidget) 条件判断,如果是 false 则不执行 update 方法),(如果 widget 还有子节点的话,继续执行 updateChildren 方法,执行 updateChild 方法,再执行 update 方法。循环。)再执行 _performRebuild 方法,最终执行 (widget as RenderObjectWidget).updateRenderObject(this, renderObject) 方法。
小提示:组件 AnimatedSwitcher 的_AnimatedSwitcherState 的在执行 update方法后,会执行 didUpdateWidget,该方法中有判断当新旧组件不是同一个组件(canUpdate 返回 false)时才执行动画。
void didUpdateWidget(AnimatedSwitcher oldWidget) {
//......
if (hasNewChild != hasOldChild ||
hasNewChild && !Widget.canUpdate(widget.child!, _currentEntry!.widgetChild)) {
// Child has changed, fade current entry out and add new entry.
_childNumber += 1;
_addEntryForNewChild(animate: true);
}