美文网首页
Flutter系列七:Flutter布局和绘制流程浅析

Flutter系列七:Flutter布局和绘制流程浅析

作者: chonglingliu | 来源:发表于2021-04-01 15:16 被阅读0次

    我们前面介绍了StatelessWidgetStatefulWidget,它们只是对其他Widget进行组合,不具备自定义绘制的能力。在需要绘制内容的场景下,我们要使用RenderObjectWidget,因为RenderObjectWidget创建的RenderObject负责布局和绘制的功能。

    本文将以RenderObject为起点,梳理下Flutter的布局和绘制流程的逻辑。

    RenderObject

    RenderObject渲染树Render Tree的一个节点,主要负责布局绘制

    Flutter设计了重要的三棵树Widget Tree - Element Tree - RenderObject Tree。示例如下:

    image
    图片引用来源

    RenderObjectWidget实例化的RenderObjectElement会创建 RenderObject, 所有的RenderObject会组成一颗RenderObject Tree

    abstract class RenderObject extends AbstractNode implements HitTestTarget {}
    

    RenderObject继承自AbstractNode, AbstractNode是对树的节点的抽象:

    class AbstractNode {
        // 1
        int get depth => _depth;
        int _depth = 0;
        void redepthChild(AbstractNode child) {}
        
        // 2
        Object? get owner => _owner;
        Object? _owner;
        
        void attach(covariant Object owner) {
            _owner = owner;
        }
        void detach() {
            _owner = null;
        }
        
        // 3
        AbstractNode? get parent => _parent;
        AbstractNode? _parent;
      
        void adoptChild(covariant AbstractNode child) {
            child._parent = this;
            if (attached)
              child.attach(_owner!);
            redepthChild(child);
        }
        
        void dropChild(covariant AbstractNode child) {
            child._parent = null;
            if (attached)
              child.detach();
        }
    }
    
    • AbstractNode提供了三个属性和几个重要的方法:
    1. 节点深度depth属性和计算节点深度redepthChild()方法;
    2. owner和对应的关联attach()和取消关联detach()方法;
    3. parent父节点;
    4. 挂载子节点adoptChild()和卸载子节点dropChild()方法。
    abstract class RenderObject extends AbstractNode implements HitTestTarget {
        // 1
        ParentData? parentData;
        
        // 2
        Constraints _constraints;
        
        // 3
        RenderObject? _relayoutBoundary;
        
        // 众多方法...
    }
    
    • RenderObject自身也有几个重要的属性:
    1. parentData父节点的插槽,父节点的一些信息可以放置在这里面供子节点使用;
    2. _constraints为父节点提供的约束;
    3. _relayoutBoundary是需要重新布局的边界。
    • RenderObject的方法和Android的View非常类似:
    功能 RenderObject View
    布局 performLayout() measure()/measure()
    绘制 paint() draw()
    请求布局 markNeedsLayout() requestLayout()
    请求绘制 markNeedsPaint() invalidate()
    父节点/View parent getParent()
    添加子节点/View adoptChild() addView()
    移除子节点/View dropChild() removeView()
    关联owner/Window attach() onAttachedToWindow()
    取消关联owner/Window detach() onDetachedFromWindow()
    事件 hitTest() onTouch()
    屏幕旋转 rotate() onConfigurationChanged()
    参数 parentData mLayoutParams
    • RenderObject还有一个特点 --- 它定义了布局/绘制协议,但并没定义具体布局/绘制模型

    定义了布局/绘制协议就是指继承RenderObject的子类必须要实现一些方法,譬如performLayoutpaint等;没定义具体布局/绘制模型是指没有限定使用什么坐标系,子节点可以有0个、1个还是多个等。

    • Flutter提供了RenderBoxRenderSlive两个子类,他们分别对应简单的2D笛卡尔坐标模型和滚动模型。
    RenderObject子类 Constraints ParentData
    RenderBox BoxConstraints BoxParentData
    RenderSlive SliverConstraints SliverLogicalParentData

    一般情况下我们不需要直接使用RenderObject,使用RenderBoxRenderSlive这两个子类就能满足需求。

    SchedulerBinding.handleDrawFrame()

    我们介绍这个方法是为了介绍每次刷新的工作流程,这样有助于我们更好的理解RenderObject的相关内容。

    Flutter启动流程分析那篇文章中,我们提到过window.scheduleFrame()Native platform发起一个刷新视图的请求后,Flutter Engine会在适当的时候调用SchedulerBinding_handleDrawFrame方法。

    void handleDrawFrame() {
        try {
          // PERSISTENT FRAME CALLBACKS
          _schedulerPhase = SchedulerPhase.persistentCallbacks;
          for (final FrameCallback callback in _persistentCallbacks)
            _invokeFrameCallback(callback, _currentFrameTimeStamp!);
        } finally {
        }
    }
    

    handleDrawFrame中执行了回调函数数组persistentCallbacks中所有的回调函数。其中就包括RendererBinding中的_handlePersistentFrameCallback方法:

    <!-- RendererBinding -->
    void _handlePersistentFrameCallback(Duration timeStamp) {
        drawFrame();
        _scheduleMouseTrackerUpdate();
    }
    

    这里的drawFrame方法是调用的父类WidgetsBinding的方法:

    <!-- WidgetsBinding -->
    void drawFrame() {
        try {
          // 1
          if (renderViewElement != null)
            buildOwner!.buildScope(renderViewElement!);
          // 2
          super.drawFrame();
          // 3
          buildOwner!.finalizeTree();
        } finally {
         
        }
    }
    

    此方法代表的含义:

    1. buildOwner!.buildScope(renderViewElement!)执行的是Widgetbuild任务,这其中就包括StatelessWidgetStatefulWidgetRenderObjectWidget
    2. 调用WidgetsBindingdrawFrame方法;
    3. 卸载非激活状态的Element

    WidgetsBindingdrawFrame方法中则执行了布局和绘制等操作。

    void drawFrame() {
        assert(renderView != null);
        pipelineOwner.flushLayout();
        pipelineOwner.flushCompositingBits();
        pipelineOwner.flushPaint();
        if (sendFramesToEngine) {
          renderView.compositeFrame(); // this sends the bits to the GPU
          pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
          _firstFrameSent = true;
        }
    }
    
    flow

    上面的buid/layout/paint等都和RenderObject息息相关,我们将在接下来的章节中详细介绍。

    Build

    接下来我们就来看看buildScope方法触发的RenderObjectWidget的构建过程。

    Element inflateWidget(Widget newWidget, dynamic newSlot) {
        // 1
        final Element newChild = newWidget.createElement();
        // 2
        newChild.mount(this, newSlot);
        return newChild;
    }
    

    inflateWidget方法主要作用:

    1. 先通过createElement方法根据Widget创建对应的Element
    2. 然后新建的Element调用mount方法,将自己挂载到Element Tree上,位置是父ElementnewSlot这个插槽。

    createElement

    abstract class RenderObjectWidget extends Widget {
        @factory
        RenderObjectElement createElement();
    }
    

    RenderObjectWidgetcreateElement方法是工厂方法,真正的实现方法在子类里面。

    RenderObjectWidget的子类对应的Element总结:

    分类 Widget Element
    根节点 RenderObjectToWidgetAdapter RootRenderObjectElement
    具有多个子节点 MultiChildRenderObjectWidget MultiChildRenderObjectElement
    具有一个子节点点 SingleChildRenderObjectWidget SingleChildRenderObjectElement
    叶子节点 LeafRenderObjectWidget LeafRenderObjectElement

    代码如下:

    abstract class LeafRenderObjectWidget extends RenderObjectWidget {
      @override
      LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
    }
    
    abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
      @override
      SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
    }
    
    abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
      @override
      MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
    }
    
    class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
      @override
      RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
    
      @override
      RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
    }
    

    mount

    我们来看RenderObjectElementmount方法实现:

    void mount(Element? parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        _renderObject = widget.createRenderObject(this);
        attachRenderObject(newSlot);
        _dirty = false;
    }
    
    1. super.mount的作用主要是记录下parent,slotdepth等值;
    2. widget.createRenderObject创建了一个renderObject
    3. attachRenderObject就是将这个parentData挂载到RenderObject Tree上,并且更新RenderObjectparentData
    void attachRenderObject(dynamic newSlot) {
        _slot = newSlot;
        _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
        _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
        final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
        if (parentDataElement != null)
          _updateParentData(parentDataElement.widget);
    }
    

    insertRenderObjectChild

    renderObject通过insertRenderObjectChild方法挂载到RenderObject Tree上,那具体的实现是如何实现的呢?

    能实现挂载RenderObject的只能是SingleChildRenderObjectElementMultiChildRenderObjectElement。我们分别来看看:

    SingleChildRenderObjectElement
    void insertRenderObjectChild(RenderObject child, dynamic slot) {
        final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
        renderObject.child = child;
    }
    

    child进行赋值:

    set child(ChildType? value) {
        if (_child != null)
          dropChild(_child!);
        _child = value;
        if (_child != null)
          adoptChild(_child!);
    }
    

    如果已经有_child先将其从卸载,然后将新的Child挂载上。

    void dropChild(RenderObject child) {
        child._cleanRelayoutBoundary();
        child.parentData!.detach();
        child.parentData = null;
        super.dropChild(child);
        markNeedsLayout();
        markNeedsCompositingBitsUpdate();
        markNeedsSemanticsUpdate();
    }
    
    void adoptChild(RenderObject child) {
        setupParentData(child);
        markNeedsLayout();
        markNeedsCompositingBitsUpdate();
        markNeedsSemanticsUpdate();
        super.adoptChild(child);
    }
    

    这两个方法主要是对_childparentData重新赋值,然后通过markNeedsLayoutmarkNeedsCompositingBitsUpdatemarkNeedsSemanticsUpdate标记需要重新布局,需要合成和语义的更新。

    MultiChildRenderObjectElement
    void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
        final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;
        renderObject.insert(child, after: slot.value?.renderObject);
    }
    
    void insert(ChildType child, { ChildType? after }) {
        adoptChild(child);
        _insertIntoChildList(child, after: after);
    }
    

    MultiChildRenderObjectElement中的实现方式类似,只是这次不是简单的赋值,而是将child添加到Render Tree中去,然后进行各种标记。

    _insertIntoChildList方法添加的逻辑如下:

    • 依附的兄弟节点为空,插入在第一个子节点;
    • 依附的兄弟节点没有相关联的下一个兄弟节点,插入在兄弟节点队尾;
    • 依附的兄弟节点有相关联的下一个兄弟节点,插入在兄弟节点中间。

    inflateWidget递归

    由于SingleChildRenderObjectWidgetMultiChildRenderObjectWidget含有子节点,所以需要对子Widget进行构建。

    <!-- SingleChildRenderObjectWidget -->
    void mount(Element? parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        _child = updateChild(_child, widget.child, null);
    }
    
    Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) {
        final Element newChild;
        // ... 省略Widget更新的逻辑
        newChild = inflateWidget(newWidget, newSlot);
        return newChild;
    }
    
    <!-- MultiChildRenderObjectElement -->
    void mount(Element? parent, dynamic newSlot) {
        super.mount(parent, newSlot);
        final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
        Element? previousChild;
        for (int i = 0; i < children.length; i += 1) {
          final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
          children[i] = newChild;
          previousChild = newChild;
        }
        _children = children;
    }
    

    这样,接下来的操作就进入了递归流程了,和上面介绍的流程内容一模一样了。

    流程示意图:

    RenderObjectWidget Build

    markNeedsLayout

    我们上面看到了,adoptChildadoptChild的方法中都调用了markNeedsLayout的相关内容:

    abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
    
        bool _needsLayout = true;
    
        RenderObject? _relayoutBoundary;
        
        void markNeedsLayout() {
            // 1
            if (_needsLayout) {
              return;
            }
            // 2
            if (_relayoutBoundary != this) {
              markParentNeedsLayout();
            } else {
              // 3
              _needsLayout = true;
              if (owner != null) {
                owner!._nodesNeedingLayout.add(this);
                owner!.requestVisualUpdate();
              }
            }
        }
    }
    

    RenderObject_needsLayout属性,标记是否需要重新布局,还有一个_relayoutBoundary布局边界属性,表示开始重新布局的节点,这样就不需要每次整个渲染树的节点都进行重新布局。

    markNeedsLayout代表的含义:

    1. 如果已经标记_needsLayout,直接返回;
    2. 如果_relayoutBoundary布局边界不是自身,让父节点递归调用markNeedsLayout方法;
    3. 如果_relayoutBoundary布局边界是自身,标记_needsLayout, 并将自身加入到PipelineOwner_nodesNeedingLayout列表中,等待PipelineOwner进行重新布局;
    4. 请求PipelineOwner进行更新。

    您可能会有疑问_relayoutBoundary是在什么时候赋值的?有两个地方赋值:

    1. 第一次布局的时候,_relayoutBoundary会被标记为RenderView,即自身,然后从根节点进行布局;
    void scheduleInitialLayout() {
        _relayoutBoundary = this;
        owner!._nodesNeedingLayout.add(this);
    }
    
    1. layout()方法中RenderObject也会重新标记_relayoutBoundary,一般情况下也是自身。
    void layout(Constraints constraints, { bool parentUsesSize = false }) {
        // ...
        _relayoutBoundary = relayoutBoundary;
    }
    

    markNeedsCompositingBitsUpdate

    bool _needsCompositingBitsUpdate = false;
    
    void markNeedsCompositingBitsUpdate() {
        if (_needsCompositingBitsUpdate)
          return;
        _needsCompositingBitsUpdate = true;
        if (parent is RenderObject) {
          final RenderObject parent = this.parent! as RenderObject;
          if (parent._needsCompositingBitsUpdate)
            return;
          if (!isRepaintBoundary && !parent.isRepaintBoundary) {
            parent.markNeedsCompositingBitsUpdate();
            return;
          }
        }
        if (owner != null)
          owner!._nodesNeedingCompositingBitsUpdate.add(this);
    }
    

    RenderObject_needsCompositingBitsUpdate属性,标记是否需要合成。

    markNeedsCompositingBitsUpdate的逻辑如下:

    1. 如果已经标记_needsCompositingBitsUpdate,直接返回;
    2. 如果未标记_needsCompositingBitsUpdate先标记,然后标记父节点或者向父类递归调用markNeedsCompositingBitsUpdate直到标记成功为止;
    3. 将自身加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表中。

    结果就是将isRepaintBoundary这个节点下的所有节点都标记为_needsCompositingBitsUpdate,然后加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表中。

    flushLayout

    前面所有的逻辑只能算是buildScope方法触发的Build阶段。接下来我们就进入了Layout阶段了。

    <!-- PipelineOwner -->
    void flushLayout() {
        try {
          while (_nodesNeedingLayout.isNotEmpty) {
            final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
            _nodesNeedingLayout = <RenderObject>[];
            for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
              if (node._needsLayout && node.owner == this)
                node._layoutWithoutResize();
            }
          }
        } finally {
        }
    }
    

    PipelineOwnerflushLayout其实很简单,让_nodesNeedingLayout中的所有RenderObject按照广度优先遍历调用_layoutWithoutResize方法。

    <!-- RenderObject -->
    void _layoutWithoutResize() {
        try {
          performLayout();
        } catch (e, stack) {
        }
        _needsLayout = false;
        markNeedsPaint();
    }
    

    我们来看看_ScaffoldLayout中的实现:

    void performLayout() {
        size = _getSize(constraints);
        delegate._callPerformLayout(size, firstChild);
    }
    
    void _callPerformLayout(Size size, RenderBox? firstChild) {
        performLayout(size);
    }
    
    void performLayout(Size size) {
        layoutChild(_ScaffoldSlot.body, bodyConstraints);
        positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
    }
    
    Size layoutChild(Object childId, BoxConstraints constraints) {
        child!.layout(constraints, parentUsesSize: true);
        return child.size;
    }
    
    void positionChild(Object childId, Offset offset) {
        final MultiChildLayoutParentData childParentData = child!.parentData! as MultiChildLayoutParentData;
        childParentData.offset = offset;
    }
    

    根据一系列调用,生成一个BoxConstraints传递给每个子节点,子节点调用layout() 进行测量和布局。

    void layout(Constraints constraints, { bool parentUsesSize = false }) {
        // 1
        RenderObject? relayoutBoundary;
        if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
          relayoutBoundary = this;
        } else {
          relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
        }
        
        // 2
        if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
          return;
        }
        // 3
        _constraints = constraints;
        if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
          visitChildren(_cleanChildRelayoutBoundary);
        }
        _relayoutBoundary = relayoutBoundary;
        
        // 4
        if (sizedByParent) {
          try {
            performResize();
            
          } catch (e, stack) {
          }
          
        }
        try {
          // MultiChildLayoutDelegate --- performLayout & _callPerformLayout & performLayout & child!.layout
          // 5 
          performLayout();
          markNeedsSemanticsUpdate();
          
        } catch (e, stack) {
        }
     
        _needsLayout = false;
        markNeedsPaint();
    }
    
    1. 首先根据!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject的条件进行_relayoutBoundary的计算,一般情况下会指向自身;

    parentUsesSize表示是否父节点的大小依赖子节点,sizedByParent表示大小由父类决定,constraints.isTight表示大小是固定的。

    1. 根据!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary确实定是否需要重新布局,不需要直接返回;
    2. 记录下_constraints;
    3. 如果依赖父节点的大小,则根据_constraints计算出size尺寸, ;
    4. performLayout根据根据_constraints计算出size尺寸,然后调用子类的layout方法。
    总结:

    performLayout的逻辑就是通过layout方法将Constraints逐步往下传递,得到Size逐步向上传递,然后父节点通过给parentData赋值确定对子节点的位置摆放。

    flushLayout

    flushCompositingBits

    void flushCompositingBits() {
    
        _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
        for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
          if (node._needsCompositingBitsUpdate && node.owner == this)
            node._updateCompositingBits();
        }
        _nodesNeedingCompositingBitsUpdate.clear();
      }
    

    遍历_nodesNeedingCompositingBitsUpdate中的每个RenderObject,然后调用_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;
    }
    

    这个方法就是找到isRepaintBoundarytrue的节点及其父节点,将它们的_needsCompositing为true设置为true;

    isRepaintBoundary

    上面提到的isRepaintBoundaryRenderObject的一个属性,默认是false。表示的是否需要独立渲染。

    <!-- RenderObject -->
    bool get isRepaintBoundary => false;
    

    如果需要独立渲染则需要覆盖这个值为true,例如RenderView的值就为true

    <!-- RenderView -->
    @override
    bool get isRepaintBoundary => true;
    

    flushPaint

    按照逻辑flushPaint之前应该先会调用markNeedsPaint,我们回过头来看看发现确实如此,有很多地方都频繁的调用的markNeedsPaint,譬如_layoutWithoutResize,layout,_updateCompositingBits等方法中都有出现,只是前面我们特意忽略了这个逻辑。

    void markNeedsPaint() {
        if (_needsPaint)
          return;
        _needsPaint = true;
        if (isRepaintBoundary) {
          if (owner != null) {
            owner!._nodesNeedingPaint.add(this);
            owner!.requestVisualUpdate();
          }
        } else if (parent is RenderObject) {
          final RenderObject parent = this.parent! as RenderObject;
          parent.markNeedsPaint();
        } else {
          if (owner != null)
            owner!.requestVisualUpdate();
        }
    }
    
    1. 如果isRepaintBoundarytrue, 则加入到_nodesNeedingPaint数组中,然后请求界面更新;
    2. 如果isRepaintBoundaryfalse,则向父节点遍历;
    3. 如果到了根节点,就直接请求界面更新;

    我们接下来看看flushPaint的代码:

    void flushPaint() {
        try {
          final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
          _nodesNeedingPaint = <RenderObject>[];
          for (final 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 {
        }
    }
    

    从下往上遍历_nodesNeedingPaint数组,然后从上往下进行绘制。

    接下来我们看看是如何绘制的:

    static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
        _repaintCompositedChild(
          child,
          debugAlsoPaintedParent: debugAlsoPaintedParent,
        );
    }
    
    static void _repaintCompositedChild(
        RenderObject child, {
        bool debugAlsoPaintedParent = false,
        PaintingContext? childContext,
      }) {
        OffsetLayer? childLayer = child._layer as OffsetLayer?;
        if (childLayer == null) {
          child._layer = childLayer = OffsetLayer();
        } else {
          childLayer.removeAllChildren();
        }
        childContext ??= PaintingContext(child._layer!, child.paintBounds);
        // 重点
        child._paintWithContext(childContext, Offset.zero);
    
        childContext.stopRecordingIfNeeded();
    }
    

    PaintingContext的类方法repaintCompositedChild接收了RenderObject对象,最后结果是这个RenderObject对象调用_paintWithContext方法,参数是PaintingContext对象和偏移量Offset

    void _paintWithContext(PaintingContext context, Offset offset) {
        
        if (_needsLayout)
          return;
    
        _needsPaint = false;
        try {
          paint(context, offset);
        } catch (e, stack) {
          
        }
    }
    
    <!-- PaintingContext -->
    Canvas? _canvas;
    

    _paintWithContext方法调用的是RenderObject子类对象的paint(PaintingContext context, Offset offset)进行绘制,绘制在PaintingContextCanvas上。

    总结

    本文主要分析了RendObjectBuildLayoutPaint等相关内容,后续继续分析其他相关内容。

    相关文章

      网友评论

          本文标题:Flutter系列七:Flutter布局和绘制流程浅析

          本文链接:https://www.haomeiwen.com/subject/ugptkltx.html