RenderObject was mentioned in Widget. What is RenderObject? Let’s play a game!

The official explanation

An object in the render tree.

An object in the Render tree.

There are four trees in front of Flutter: the Widget tree, the Element tree, the Render tree and the Layer tree.

If the Widget tree is a drawing, then the Render tree is the assembly line for that drawing. RenderObject is an operator on this assembly line.

After we configure the Widget tree at the top level, the Render tree is responsible for rendering what you want on the phone screen.

Read the source code

A good day should start with reading the source code!

abstract class RenderObject extends AbstractNode 
with DiagnosticableTreeMixin 
implements HitTestTarget {}
Copy the code

As you can see, the RenderObject is also a linked list structure, incorporating the DiagnosticableTreeMixin tree feature and implementing a hit test abstract class.

Remove debug and Assert code, and the structural functionality of the RenderObject is clear.

Layout module

abstract class RenderObject {
  / / LAYOUT module
  // The storage container that passes information to child is usually Offset
  ParentData parentData;

  // This method passes parentData to the child before it is added to the child list
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! ParentData)
      child.parentData = ParentData();
  }

  // Add a Render Object as your child
  @override
  void adoptChild(RenderObject child) {
    / / set parentData
    setupParentData(child);
    // Mark the update
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
  }

  // Delete a Render Object as its own child
  @override
  void dropChild(RenderObject child) {
    // Clear boundaries
    child._cleanRelayoutBoundary();
    // The parentData access permission is lost
    child.parentData.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
  }

  / / traverse the children
  void visitChildren(RenderObjectVisitor visitor) { }

  // Render Tree manager
  @override
  PipelineOwner get owner => super.owner;

  // Tell its owner to insert it into the Render Tree and the first markup needs to compute the layout and redraw it
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    if(_needsLayout && _relayoutBoundary ! =null) {
      _needsLayout = false;
      markNeedsLayout();
    }
    if (_needsCompositingBitsUpdate) {
      _needsCompositingBitsUpdate = false;
      markNeedsCompositingBitsUpdate();
    }
    if(_needsPaint && _layer ! =null) {
      _needsPaint = false;
      markNeedsPaint();
    }
    if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
      _needsSemanticsUpdate = false; markNeedsSemanticsUpdate(); }}// Whether layout is required
  bool _needsLayout = true;

  // Where is the record layout boundary
  RenderObject _relayoutBoundary;

  // Parent constraints
  @protected
  Constraints get constraints => _constraints;
  Constraints _constraints;

  // The markup needs to be rearranged
  void markNeedsLayout() {
    // If it is not a boundary, let parent. MarkNeedsLayout handle push all the way to the boundary
    if(_relayoutBoundary ! =this) {
      markParentNeedsLayout();
    } else {
      // The owner adds the dirty table to the layout and asks for a refresh
      _needsLayout = true;
      if(owner ! =null) {
        owner._nodesNeedingLayout.add(this); owner.requestVisualUpdate(); }}}This method is called to clear the boundary cache when the boundary changes
  void _cleanRelayoutBoundary() {
    if(_relayoutBoundary ! =this) {
      _relayoutBoundary = null;
      _needsLayout = true; visitChildren((RenderObject child) { child._cleanRelayoutBoundary(); }); }}// Only layout does not remeasure
  void _layoutWithoutResize() {
      performLayout();
      markNeedsSemanticsUpdate();
    _needsLayout = false;
    markNeedsPaint();
  }

  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    RenderObject relayoutBoundary;
    // Determine the boundary
    // If one of the following four conditions is met, the boundary is itself
    // parentUsesSize parent Is concerned about its own size
    // sizedByParent specifies the size by parent
    // constraints. IsTight is constrained
    // parent is not the root node of RenderObject
    if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject) {
      relayoutBoundary = this;
    } else {
      // Otherwise use the parent boundary
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
    _constraints = constraints;
    // If the boundary changes, the traversal clears all recorded boundaries to reset
    if(_relayoutBoundary ! =null&& relayoutBoundary ! = _relayoutBoundary) { visitChildren((RenderObject child) { child._cleanRelayoutBoundary(); }); } _relayoutBoundary = relayoutBoundary;// performResize is called when sizedByParent is true
    // Otherwise, set the size in performLayout
    if (sizedByParent) {
      performResize();
    }
    / / layout
      performLayout();
      markNeedsSemanticsUpdate();
    _needsLayout = false;
    markNeedsPaint();
  }

  // size is determined only by parent constraints. Default to false is never wrong
  // When true, the work of determining size is computed in performResize
  @protected
  bool get sizedByParent => false;

  // Subclass overrides this method to calculate its own size based on constraints
  // You can't call this method directly, you can call the Layout method indirectly only if sizedByParent is true
  @protected
  void performResize();

  // Subclasses override this method
  // This method cannot be called directly, but indirectly by calling the Layout method
  Function 1: When sizedByParent is false, calculate its size according to the adaptive child
  Function 2: Iterate over child.layout to determine the size and offsets of the child
  @protected
  void performLayout();
}
Copy the code

Lay down the key points:

  • RenderObjectAs aRender TreeBut you can’t directly change the tree yourself. Every move needs to be communicated toPipelineOwnerThe manager executes.
  • PipelineOwnerManage several dirty notebooks, record nodes that need to be changed, and actually update according to these notebooksRender Tree. whenRenderObjectWhen changes need to be made, notify the owner to write himself to the small book (marked as dirty). It can be understood that in the process of military training, any problems should be reported to the drillmaster, and the drillmaster will make unified arrangements.
  • parentthroughsetupParentDataMethod, transferparentData, usuallylayout offset.
  • performResizeMethods need to be subclassed. Calculate its own size
  • performLayoutMethods need to be subclassed.
  1. Calculate size when sizedByParent is false.
  2. 2. Iterate over child.layout to determine the size and offsets of the child.
  • The core approach islayout.
Determine '_relayoutBoundary' layout boundary -> Call the 'performResize' and 'performLayout' methods to calculate size and position -> redrawCopy the code

Since this process works for most scenarios, when we actually develop, we only care about rewriting the implementation of performResize and performLayout, not the Layout method.

Paint module

abstract class RenderObject {
  / / PAINTING module
  RenderObject() {
    Whether you need / / mixed layer = is redrawing borders | | always mixed layer
    _needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
  }

  // Whether to redraw the boundaries currently
  bool get isRepaintBoundary => false;

  // Always create a new layer and merge it to the original layer
  @protected
  bool get alwaysNeedsCompositing => false;

  // Cache layer
  ContainerLayer _layer;

  // Whether the value of _needsCompositing needs to be set
  bool _needsCompositingBitsUpdate = false;

  // The flag _needsCompositing needs to be reset
  void markNeedsCompositingBitsUpdate() {
    if(owner ! =null)
      owner._nodesNeedingCompositingBitsUpdate.add(this);
  }

  // Whether to draw on a new layer and merge to the ancestor layer
  // true: Draw on a new layer but the new layer will preferentially use the cache layer to improve performance
  // false: do not use the new layer
  // currently repaintBoundary _needsCompositing=true and the cache layer is automatically assigned to the new OffsetLayer after drawing on this layer to merge into the parent layer
  bool _needsCompositing;

  // Go through the Settings using layer blending
  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;
  }

  // Whether to draw
  bool _needsPaint = true;

  // The mark needs to be redrawn
  void markNeedsPaint() {
    _needsPaint = true;
    // If it is currently repaintBoundary inform the owner that it needs to be redrawn
    if (isRepaintBoundary) {
      if(owner ! =null) {
        owner._nodesNeedingPaint.add(this); owner.requestVisualUpdate(); }}// If the node is not root, let parent determine
    else if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      parent.markNeedsPaint();
    }
    // If the current node is root, directly notify the owner to redraw
    else {
      if(owner ! =null) owner.requestVisualUpdate(); }}// See [RenderView] to initialize the root node.
  void scheduleInitialPaint(ContainerLayer rootLayer) {
    _layer = rootLayer;
    owner._nodesNeedingPaint.add(this);
  }

  // Replace rootLayer which is called only by root
  // This method may be called when the pixel ratio of the device changes
  void replaceRootLayer(OffsetLayer rootLayer) {
    _layer.detach();
    _layer = rootLayer;
    markNeedsPaint();
  }

  // Subclasses override this method to do the actual drawing at the corresponding offset
  // Instead of calling this method directly, use the markNeedsPaint flag and let the owner handle it
  / / if you just want to draw a child with PaintingContext. PaintChild interface to operate Avoid direct manipulation render the object
  void paint(PaintingContext context, Offset offset) { }
}
Copy the code

The most important point in this module is the variable needsCompositing. This variable determines whether to draw on the new layer. As you can see in the constructor, it is determined by isRepaintBoundary and alwaysNeedsCompositing. IsRepaintBoundary This can be changed to true with the RepaintBoundary wrap. AlwaysNeedsCompositing can be modified by subclasses.

Therefore we can use the RepaintBoundary control to partially redraw to improve performance.

The SEMANTICS module and the HIT TESTING module

Semantic Semantics is mainly the interface provided to screen reading software, and it is also the basis to realize auxiliary functions. Through semantic interface, the machine can understand the content on the page, and users with visual impairment can use screen reading software to understand the UI content. Unless there is a special need, generally not accessible.

The result of the hit test is processed in the following way.

// Override this method to handle events
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }
Copy the code

There are two more important methods in RenderObject that I’ll mention as well.

Void reassemble() {// Mark for relayout markNeedsLayout(); / / tag markNeedsCompositingBitsUpdate reset whether to use a new layer (); // Mark needs to be redrawn markNeedsPaint(); // Tag semantics need to update markNeedsSemanticsUpdate(); VisitChildren ((RenderObject child) {child-.reassemble (); }); Void showOnScreen({RenderObject descendant, Rect Rect, Duration Duration = duration.zero, Curve curve = Curves.ease, }) { if (parent is RenderObject) { final RenderObject renderParent = parent; renderParent.showOnScreen( descendant: descendant ?? this, rect: rect, duration: duration, curve: curve, ); }}Copy the code

PipelineOwner

The pipeline owner manages the rendering pipeline.

The manager of the entire rendering process is embodied in the following methods:

  1. flushLayout

To iterate over the Render Object node that needs Relayout, call _layoutWithoutResize() to rewrite the computed layout

  1. flushCompositingBits

Iterate over the nodes that require CompositingBitsUpdate and call the _updateCompositingBits() method to update needsCompositing. This is usually executed between flushLayout and flushPaint.

  1. flushPaint

Iterate over the node that needs repaint, triggering the paint painting by calling the _paintWithContext method of the child Render Object with PaintingContext.

  1. flushSemantics

Update semantics

RenderBox

RenderBox is a subclass of RenderObject, which is a further encapsulation of RenderObject in 2D Cartesian coordinates. It mainly encapsulates the following function points:

  1. Parentdata isBoxParentDataonlyoffsetProperty defaults toOffset.zero
  2. useBoxConstraintsAs its constraint
  3. usesizeRecord its size
  4. Measure its maximum, minimum width and height
  5. The measurement baseline
  6. Implement the default hit test scheme
  7. Mixed withRenderObjectWithChildMixinImplementation of single child ->SingleChildRenderObjectWidget
  8. Mixed withContainerRenderObjectMixinImplementation of multiple children ->MultiChildRenderObjectWidget

The development App draws in Cartesian coordinates, so inheriting RenderBox will suffice for most scenarios.

conclusion

This section describes the main functions and methods of RenderObject. Understanding these will help us better understand the underlying principles of Flutter UI and help us to implement our own custom Flutter controls. For details on how to draw the layout to the screen, I will use examples below.

It hurts my hair to understand how it works!