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 重建导致布局信息变化引发的重绘,还有下列情况也会造成节点重绘:

  1. 对于子节点是 RenderObjectWidget 组件在执行 updateRenderObject 方法时,如果 renderObject 属性变化,则一般都会调用 markNeedsPaint() 重绘 。如“示例:共享 Layer 导致的重绘” 就是因为动画更新时(触发 listenable 回调)调用 setState,然后RenderTransform(本质是 RenderObject)执行 updateRenderObject,其属性 transform 发生变化,最终调用 markNeedsPaint() 重绘。
  2. 对于子节点是 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);
    }

posted on 2023-02-03 14:14  Lemo_wd  阅读(273)  评论(0编辑  收藏  举报

导航