preface

In these two articles in front, said a Flutter startup is how to construct a Widget. The Element, RenderObject node tree.

Then in this article, we will analyze the layout flow of Flutter and the call flow of hitTest

The basic layout flow code is handled in the RenderObjcet class, but this is the most basic flow and does not contain specific coordinate systems, sizes, etc. In mobile development, cartesian coordinates are often used.

RenderBox inherits RenderObjcet and implements a cartesian layout.

This paper analyzes the basic flow of Flutter layout and the call flow of hitTest from the perspective of source code. But because there’s something to refer to, you can refer to it

Widgets, Element, RenderObject tree to build and update process

The startup process of Flutter App

RenderObject

basis

RenderObject can be understood as information about a node, which describes the Layout, Layer, and Paint information of the node.

As mentioned in this article,RenderObject is created by widgets. When the Widget tree is built, the RenderObject tree is also created.

If a Widget is related to UI information, the base class is RenderObjectWidget, the corresponding Element’s base class is RenderObjectElement, and there will be a RenderObject for each Widget.

Requesting layout updates

When a Widget is updated, the Update method of RenderObjectElement is called

. The update is as follows

 @override
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget); . widget.updateRenderObject(this, renderObject); . _dirty =false;
  }

Copy the code

When Wiget is a RenderObjectWidget, the Update method of RenderObjectElement is called. The update method in turn calls the updateRenderObject method of RenderObjectWidget.

The Widget then processes the RenderObject in updateRenderObject. If the layout needs to be updated, the markNeedsLayout method of RenerObject is called to request the layout update. The markNeedsLayout implementation is as follows

void markNeedsLayout() {
   ...
    if(_relayoutBoundary ! =this) {
      // If the current node is not a layout boundary, the layout of the node affects the parent layout
      //markParentNeedsLayout recursively calls the markNeedsLayout() method up until the parent node is the layout boundary
      markParentNeedsLayout();
    } else {
      _needsLayout = true; .// Owner is PipelineOwner, which manages layouts, layers, and drawingsowner! ._nodesNeedingLayout.add(this); owner! .requestVisualUpdate(); }}}Copy the code

When markNeedsLayout is called, instead of making immediate changes to the UI, the changes are recorded. In the next interface update, change all the changes at once

Layout update request processing

Like BuildOwner in the Widget build process mentioned earlier, there is also a dispatch center PipelineOwner. He is responsible for the layout of the RenderObject tree, layer updates, and the drawing process.

When a node needs a Layout update, the markNeedsLayout() method is called to add it to the list in _nodesNeedingLayout in PipelineOwner.

This is followed by a call to the PipelineOwner requestVisualUpdate method, which registers a callback that will be called when the frame is signaled. The RenderBinding drawFrame method is called when the callback is executed. For details about this RenderBinding and how to call it, see the startup process of the Flutter App.

The method is as follows

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; }}Copy the code

The PipelineOwner method flushLayout() is called to update the layout information on the interface when the GPU frame signal is sent out, and then submitted to the GPU for rendering.

PS: This article focuses on the layout. The processing process of layers and drawing is roughly similar to that of layout, so it focuses on the process of flushLayout. To achieve the following

void flushLayout() {
    ...
       while (_nodesNeedingLayout.isNotEmpty) {
        final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
        _nodesNeedingLayout = <RenderObject>[];
        for (final RenderObject node indirtyNodes.. sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {if (node._needsLayout && node.owner == this) node._layoutWithoutResize(); }}... }Copy the code

This takes out _nodesNeedingLayout, which is all the nodes that need to update the layout, and calls the _layoutWithoutResize() method on each node. From this step, the node layout process begins.

The layout process

The _layoutWithoutResize() method looks like this

void_layoutWithoutResize() { ... performLayout(); . _needsLayout =false;
    markNeedsPaint();
  }
Copy the code

As you can see, you are basically just calling the performLayout() and markNeedsPaint() methods

PerformLayout () is responsible for figuring out the location and size of the node itself. RenderObject does not define an implementation of performLayout(); subclasses do.

And of course, when the layout changes, it needs to be redrawn, so there’s a markNeedsPaint() tag node that needs to be redrawn.

If we subclass RenderObjct, we need to implement performLayout() to implement our layout method. If you have multiple child nodes. Then we also need to call the layout(Constraints Constraints, {bool parentUsesSize = false} method of the child node. The layout method is passed to the child node constraint. After calling the layout method of the child node, we can know the size of the child node. To set the layout of the node

Layout method

This Layout method is defined in the RenderObject method. The following

 void layout(Constraints constraints, { bool parentUsesSize = false{...}) RenderObject? relayoutBoundary;// is the layout boundary, that is, does the child layout change to affect the parent layout
    if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject) {
      // The node is a layout boundary if the following conditions are met
      //1 The parent node determines the size of the child node
      //2 The parent node does not need the size of the child node
      //3 A given constraint can determine a unique size
      // the parent node is not a RenderObject
      relayoutBoundary = this;
    } else {
      // Otherwise, relayoutBoundary is the parent node's layout boundary relayoutBoundary
      relayoutBoundary = (parent! asRenderObject)._relayoutBoundary; }...if(! _needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) { ...// If the layout boundary is not changed, the constraint is not changed, and is not marked _needsLayout, the end is immediate
      return;
    }
    // Update node constraints
    _constraints = constraints;
    if(_relayoutBoundary ! =null&& relayoutBoundary ! = _relayoutBoundary) {// If the boundaries of the layout change, clear the boundaries of all child nodes and mark _needsLayout to true
      // When the layout of the node changes, the layout of the child node also changes
      visitChildren(_cleanChildRelayoutBoundary);
    }
   / / update the _relayoutBoundary_relayoutBoundary = relayoutBoundary; .if (sizedByParent) {
      ...
        // If the parent node determines the size of the child node, the method is called,
        //performResize is the size of the processing node
        // If sizedByParent is true, the size is determined in performResize, not performLayout
        //performResize determines the size based on the _constraints constraintperformResize(); . . }...try {
      // Call the performLayout() methodperformLayout(); . }... _needsLayout =false; markNeedsPaint(); . }Copy the code

The Layout method does a few things

  1. Handle layout boundary _relayoutBoundary
  2. If sizedByParent is true, the performResize method is called to determine the size
  3. Call the performLayout method
Layout boundary _relayoutBoundary_

The first step is to determine the layout boundary _relayoutBoundary. This is actually very important. Combined with the markNeedsLayout method above, When the markNeedsLayout method is called, the _relayoutBoundary is used to determine whether the markNeedsLayout method needs to be called all the way up. The more markNeedsLayout calls are made, the more nodes will be affected and the slower the UI will be updated. Therefore, from the perspective of interface optimization, adding _relayoutBoundary can optimize the interface fluency.

Specific can be through the following condition to start

! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject
Copy the code

In general, this is about reducing the level of the Widget tree and using it as much as possible

  1. Widgets that do not affect the parent node.

  2. A Widget whose size is determined by the parent node

  3. A Widget whose size can be uniquely determined by constraints.

This depends on the specific Widget implementation.

performResize

In the second step, determine whether to call the performResize method based on the value of the sizedByParent field. If sizedByParent is true, it means that the size of the node is only relevant to the constraints provided when the parent node calls layout. The performResize() method is called to determine the node size. In general, we use the performLayout() method to determine the size of a node. However, if performResize() is called, you should not change the node size in performLayout()

performLayout

In step 3, we see that the performLayout() method is called. Combining the previous flow, you can see that the method is called as follows

Parent node performLayout -> child node Layout -> child node performLayout -> child child node layout -> child child node performLayout ->.......Copy the code

When a node is being laid out, if there are child nodes, it will call the layout method of the child nodes and pass constraints, and the child nodes will carry out the layout. The process is then repeated all the way to the leaf node

After looking at the flow of Flutter layout, I often come across a picture online.

The parent node provides constraints to the child nodes, the child nodes layout according to the constraints, and then return to the parent node to complete the layout process. In fact, this is the process described in step 3.

So far, the general layout process is like this, as shown in the picture below

Layout flow chart

The layout flow above is based on RenderObjct, but it defines a basic flow for building a layout from top to bottom. However, the specific coordinate system and node size are not involved. This means that the basic layout process alone cannot determine where and how much of a Widget is displayed on the interface.

Flutter provides a layout based on cartesian product called RenderBox. RenderBox is inherited from RenderObjct. Cartesian coordinates, node size and hit test are expanded in RednderObjct layout process. Most renderObjects in Flutter inherit from RenderBox.

If you need to customize the layout of your coordinate system, you can inherit RenderObject. Otherwise, inheriting RenderBox is the best option.

The main layout, RenderBox

Size and location

Dart defines BoxConstraints and BoxParentData. Inherited from Constraints and ParentData, respectively. _constraints and parentData in RenderBox are two of these types.

BoxConstraints is defined as follows

class BoxConstraints extends Constraints {...final double minWidth;// Minimum width
  final double maxWidth;// Maximum width
  final doubleminHeight; .// Minimum height
  final double maxHeight;// Maximum height. }Copy the code

BoxParentData is defined as follows

class BoxParentData extends ParentData { ... Offset offset = Offset.zero; // Based on the starting point of the Cartesian product... }Copy the code

BoxConstraints determines the size of the node, and BoxParentData determines the starting point of the node.

Each node receives the parent node passing BoxConstraints and BoxParentData, and then follows the layout process above, so that the node’s starting point and size are determined.

Calculate the size

RenderBox provides several unimplemented methods, and subclasses need to provide implementations

double computeMinIntrinsicWidth(double height) // Calculate the minimum width
double computeMaxIntrinsicWidth(double height) // Calculate the maximum width
double computeMinIntrinsicHeight(double width) // Calculate the minimum height
double computeMaxIntrinsicHeight(double width) // Calculate the maximum height
Size computeDryLayout(BoxConstraints constraints) // Calculate the size of the constraint sub-node given by the parent node
Copy the code

Through these methods, the node can be calculated to occupy the size. It is not recommended to call these methods directly in Flutter. Instead, you need to get them by calling the following methods

double getMinIntrinsicWidth(double height) // Get the minimum width
double getMaxIntrinsicWidth(double height) // Get the maximum width
double getMinIntrinsicHeight(double width) // Get the minimum height
double getMaxIntrinsicHeight(double width) // Get the maximum height
Size getDryLayout(BoxConstraints constraints) // Get the size of the constraint sub-node given by the parent node
Copy the code

In the previous Layout procedure, the performLayout stage calls the Layout method of the child node, which then determines the size of the child node. GetMinIntrinsicxxx or getDryLayout to get the width and height of the child node. After getting the dimensions of the child node, the layout of the child node can be done.

By the way, the xxxDryLayout method is a post-Flutter 2.0 method that replaces the performResize method. That is, if the size of a node is determined only by the constraints of the parent node, the size of the node should not be calculated in the performLayout method, but should be calculated in the computeDryLayout method.

The xxxDryLayout method, on the other hand, works out how big a node should be without changing the rest of the RenderObjct state. The Dry method in DryLayout is the same as the Dry method in normal layout, which changes the boundary layout and constraints.

hitTest

After the layout is complete, the UI is displayed completely. When the user clicks on a Widget, how is the click event delivered? Here, click events are taken as an example to illustrate the process of event delivery

As mentioned in the previous article, a series of bindings are initialized at App startup, one of which is a GestureBinding. When the click event occurs, the GestureBinding _handlePointerDataPacket method is called, After the event the operations will eventually call _handlePointerEventImmediately (PointerEvent event) method, called process is as follows

_handlePointerEventImmediately as follows

void _handlePointerEventImmediately(PointerEvent event) {
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
     ...
      hitTestResult = HitTestResult();// Store hitTest results
      hitTest(hitTestResult, event.position);//进行hitTest
      if (event isPointerDownEvent) { _hitTests[event.pointer] = hitTestResult; }... }else if (event is PointerUpEvent || event is PointerCancelEvent) {
      hitTestResult = _hitTests.remove(event.pointer);
    } else if (event.down) {
      ...
      hitTestResult = _hitTests[event.pointer];
    }
   ...
    }());
    if(hitTestResult ! =null ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      assert(event.position ! =null);
      dispatchEvent(event, hitTestResult);// Distribute events}}Copy the code

As you can see, there are two main steps here

  1. HitTest hitTest
  2. DispatchEvent Event distribution

HitTest hitTest

Because of the Binding mixin design, the hitTest method here goes to the RenderBinding hitTest method, as follows

@override void hitTest(HitTestResult result, Offset position) { ... renderView.hitTest(result, position: position); Super. hitTest(result, position); super.hitTest(result, position); }Copy the code

The renderView.hittest (result, position: position) method is called. RenderView is the root node of the RenderObjct tree at App startup. It is RenderView type, inheritance in RenderObject, mixins RenderObjectWithChildMixin. Its hitTest method is as follows

bool hitTest(HitTestResult result, { required Offset position }) {
    if(child ! =null) child! .hitTest(BoxHitTestResult.wrap(result), position: position); result.add(HitTestEntry(this));
    return true;
  }
Copy the code

Because a mixIn RenderObjectWithChildMixin, so when the child nodes of hitTest method, will reach the RenderBox hitTest method. The following

bool hitTest(BoxHitTestResult result, { required Offset position }) {
    ...
    if(_size! .contains(position)) {if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));
        return true; }}return false;
  }
Copy the code

Here hitTest calls the hitTestChildren and hitTestSelf methods. These methods return false by default and should be implemented by specific subclasses.

The hitTestChildren method is used to handle whether a child node matches a test, and hitTestSelf determines whether the node itself responds to a hit test. If it hits, the node is added to the hit test result.

In general, the hitTestChildren method will call the hitTest method of the child node and pass

hitTest -> hitTestChildren -> hitTest -> hitTestChildren -> .... 
Copy the code

This process, will endure to all accord with the results of the test of the hitTestResult GestureBinding _handlePointerEventImmediately method, that is to say, in

DispatchEvent Event distribution

Once hitTestResult is obtained, the dispatchEvent method is executed as follows

void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
  ...
    / / the result of convenience
    for (final HitTestEntry entry in hitTestResult.path) {
      ...
        // Process and distributeentry.target.handleEvent(event.transformed(entry.transform), entry); . }}Copy the code

Because it involves a lot of event distribution processing, the margin is large, so it is not discussed here.

The flow chart of hitTest

conclusion

This article mainly analyzes the layout process, but there are no detailed examples (otherwise, the article will be long), but readers can read the source code together with specific examples. It is recommended to look at Stack implementation, because the layout calculation of this Widget is relatively simple.