Analysis of Flutter Framework

Analysis of the Flutter Framework (I) — Overview and Window

Analysis of the Flutter Framework (II) — Initialization

Analysis of the Flutter Framework (iii) — Widget, Element and RenderObject

Analysis of The Flutter Framework (IV) — The Operation of the Flutter Framework

Analysis of the Flutter Framework (5) — Animation

Analysis of the Flutter Framework (VI) — Layout

Analysis of the Flutter Framework (VII) — Drawing

preface

Previous articles introduced the Animate and build phases of the Flutter rendering pipeline. This article introduces the layout phase of the rendering pipeline with the Flutter source code.

An overview of the

As with Android, iOS, H5 and other frameworks, the framework needs to determine the position and size of each element on the page before drawing it. For an element on the page, if it contains child elements, the layout is complete by knowing the size of the child elements and having the parent element determine the location of the child elements within the page. So once the size and location of the child elements are determined, the layout is complete. The layout of the Flutter framework adopts a Box constraints model. Its layout process is shown in the figure below:

RenderObject
Constraits
maxWidth
minWidth
maxHeight
minHeight
size
size
width
height
size

Analysis of the

The engine calls the window’s onDrawFrame() function after the arrival of the vsync signal. This function runs the “PERSISTENT FRAME CALLBACKS” of Flutter. The build, layout, and paint phases of the rendering pipeline are all in this callback, WidgetsBinding. DrawFrame (). This function is added to the “Persistent” callback when RendererBinding is initialized.

void drawFrame() {
   try {
    if(renderViewElement ! =null)
      buildOwner.buildScope(renderViewElement);
    super.drawFrame();
    buildOwner.finalizeTree();
  } finally{... }}Copy the code

This line of buildOwner. BuildScope (renderViewElement) in the code is the build phase of the rendering pipeline. This part is explained in “Flutter Framework Analysis (4) — The Operation of Flutter Framework”. The next function super.drawframe () goes to RendererBinding.

void drawFrame() {
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

Copy the code

The first call pipelineOwner. FlushLayout () is this article to the layout of the stage. All right, let’s go from here. Take a look at piplineowner.flushLayout ().

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

This is going to iterate over the dirtyNodes array. This array contains the RenderObject that needs to be relaid. The dirtyNodes array is sorted by its depth in the Render Tree before traversal. The sort here is the same as the sort we encountered for Element Tree in the Build phase. The upper-layer nodes are processed first after sorting. Because the child nodes are processed recursively during layout, if the upper level nodes are processed first, the lower level nodes are not rearranged later. Renderobject._layoutwithoutresize () is then called to let the node do its own layout.

void _layoutWithoutResize() {
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {
      ...
    }
    _needsLayout = false;
    markNeedsPaint();
  }
Copy the code

In RenderObject, the function performLayout() is implemented by subclasses. Because there are a variety of layouts, you need to subclass personalize your layout logic. After the layout is complete, it sets its own _needsLayout flag to false. Looking back at the previous function, in the body of the loop, _layoutWithoutResize() is only called if _needsLayout is true. We know that layout and rendering of Flutter is done by RenderObject. Most page elements use box constraints. RenderObject has a subclass called RenderBox that deals with this layout. Most widgets in Flutter are ultimately rendered by RenderBox subclasses. RenderBox is defined in a comment in the source code

A render object in a 2D Cartesian coordinate system.

Render object in 2d Cartesian coordinate system. Each box has a size attribute. Contains height and width. Each box has its own coordinate system, the top left corner is 0,0. The lower right corner coordinates are (width, height).

abstract class RenderBox extends RenderObject {... Size _size; . }Copy the code

When we write a Flutter app to set the component size, we always pass the size or configuration such as center into the Widget when creating the Flutter app. For example, with the following Widget, we specify a size of 100×100;

Container(width: 100, height: 100);Copy the code

Because the layout is done in the RenderObject, more specifically the RenderBox. So how does this 100×100 size get passed to RenderBox? How does RenderBox do layout? Container is a StatelessWidget. It doesn’t correspond to any RenderObject by itself. Depending on the parameters passed in when the Container is constructed, the Container eventually returns the Widget made up of the Align, Padding, ConstrainedBox, and so on:

  Container({
    Key key,
    this.alignment,
    this.padding,
    Color color,
    Decoration decoration,
    this.foregroundDecoration,
    double width,
    double height,
    BoxConstraints constraints,
    this.margin,
    this.transform,
    this.child, }) : decoration = decoration ?? (color ! =null ? BoxDecoration(color: color) : null), constraints = (width ! =null|| height ! =null)
          ? constraints?.tighten(width: width, height: height)
            ?? BoxConstraints.tightFor(width: width, height: height)
          : constraints,
       super(key: key);
       
  final BoxConstraints constraints;

  @override
  Widget build(BuildContext context) {
    Widget current = child;

    if (child == null && (constraints == null| |! constraints.isTight)) { current = LimitedBox( maxWidth:0.0,
        maxHeight: 0.0,
        child: ConstrainedBox(constraints: const BoxConstraints.expand()),
      );
    }

    if(alignment ! =null)
      current = Align(alignment: alignment, child: current);

    final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
    if(effectivePadding ! =null)
      current = Padding(padding: effectivePadding, child: current);

    if(decoration ! =null)
      current = DecoratedBox(decoration: decoration, child: current);

    if(foregroundDecoration ! =null) {
      current = DecoratedBox(
        decoration: foregroundDecoration,
        position: DecorationPosition.foreground,
        child: current,
      );
    }

    if(constraints ! =null)
      current = ConstrainedBox(constraints: constraints, child: current);

    if(margin ! =null)
      current = Padding(padding: margin, child: current);

    if(transform ! =null)
      current = Transform(transform: transform, child: current);

    return current;
  }
Copy the code

In this case, a ConstrainedBox is returned.

class ConstrainedBox extends SingleChildRenderObjectWidget {
  
  ConstrainedBox({
    Key key,
    @required this.constraints,
    Widget child,
  }) : assert(constraints ! =null),
       assert(constraints.debugAssertIsValid()),
       super(key: key, child: child);

  /// The additional constraints to impose on the child.
  final BoxConstraints constraints;

  @override
  RenderConstrainedBox createRenderObject(BuildContext context) {
    return RenderConstrainedBox(additionalConstraints: constraints);
  }

  @override
  voidupdateRenderObject(BuildContext context, RenderConstrainedBox renderObject) { renderObject.additionalConstraints = constraints; }}Copy the code

The Widget creates the RenderConstrainedBox. The actual layout work is done by this, and as you can see from the code above, the 100×100 dimensions are within constraints.

class RenderConstrainedBox extends RenderProxyBox {
  
  RenderConstrainedBox({
    RenderBox child,
    @required BoxConstraints additionalConstraints,
  }) : 
       _additionalConstraints = additionalConstraints,
       super(child);

  BoxConstraints _additionalConstraints;

  @override
  void performLayout() {
    if(child ! =null) {
      child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
      size = child.size;
    } else{ size = _additionalConstraints.enforce(constraints).constrain(Size.zero); }}}Copy the code

RenderConstrainedBox inherits from RenderProxyBox. RenderProxyBox in turn inherits from RenderBox.

Here we see the implementation of performLayout(). When there are child nodes, child.layout() is called to ask the child node to do the layout. The call is passed in the constraints for the child node. This is going to pass in the 100×100 constraint. Set your size to the size of the child node after the child node layout is complete. When there is no child node, set the constraint to size for yourself.

Let’s take a look at child.layout(). This function is in the RenderObject class:

void layout(Constraints constraints, { bool parentUsesSize = false }) {
    RenderObject relayoutBoundary;
    if(! parentUsesSize || sizedByParent || constraints.isTight || parentis! RenderObject) {
      relayoutBoundary = this;
    } else {
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }

    if(! _needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {return;
    }
    _constraints = constraints;
    _relayoutBoundary = relayoutBoundary;

    if (sizedByParent) {
      try {
        performResize();
      } catch(e, stack) { ... }}try {
      performLayout();
      markNeedsSemanticsUpdate();
     
    } catch (e, stack) {
      ...
    }
    _needsLayout = false;
    markNeedsPaint();
  }
Copy the code

This one is longer and more critical. The first thing to do is to determine the relayoutBoundary. There are several conditions:

  1. parentUsesSize: Whether the parent component needs the size of the child component. This is the input parameter at call timefalse.
  2. sizedByParent: it’s aRenderObjectProperty representing the currentRenderObjectThe layout is only subject to the parentRenderObjectThe constraining influence of giving. The default isfalse. Subclasses can return if desiredtrue. Such asRenderErrorBox. When our Flutter app goes wrong, it renders the screen with yellow words on a red background.
  3. constraints.isTight: indicates whether the constraint is strict. That is, whether only one size is allowed.
  4. The final condition is whether the parent node isRenderObject. When any of the above conditions are met,relayoutBoundaryIt is itself, otherwise take the parent noderelayoutBoundary.

Next is another judgment. If the current node does not need to be rearranged, the constraint is not changed, and relayoutBoundary is not changed, then the relayoutBoundary is returned directly. This means that there is no need to rearrange the nodes from this node, including the children below it. There will be a performance improvement.

And then another judgment, if sizedByParent is true, performResize() is called. This function calculates the size of the current RenderObject based solely on constraints. When this function is called, the following performLayout() function cannot normally change the size.

PerformLayout () is where most nodes are laid out. Different RenderObjects have different implementations.

Finally, flag that the current node needs to be redrawn. This is how the layout process works recursively. Different constraints are stacked from top to bottom. The child node calculates its size according to the constraints. If necessary, the parent node gets the size of the child node for further processing after the child node layout is completed. That’s what we said at the beginning.

When we call layout(), we need to pass in a constraint, so let’s see how this constraint works:

abstract class Constraints {
  bool get isTight;

  bool get isNormalized;
}
Copy the code

This is an abstract class with only two getters. IsTight means tight as we said before. Because a Flutter is primarily a box constraint. So let’s look at the subclass of Constraints: BoxConstraints

BoxConstraints

class BoxConstraints extends Constraints {
  const BoxConstraints({
    this.minWidth = 0.0.this.maxWidth = double.infinity,
    this.minHeight = 0.0.this.maxHeight = double.infinity,
  });
  
  final double minWidth;
  
  final double maxWidth;

  final double minHeight;

  final doublemaxHeight; . }Copy the code

The box constraint has four attributes, maximum width, minimum width, maximum height, and minimum height. Different combinations of these four attributes form different constraints.

When the maximum and minimum constraints are the same in an axial direction, the axial direction is considered to be tightly constrained.

BoxConstraints.tight(Size size)
    : minWidth = size.width,
      maxWidth = size.width,
      minHeight = size.height,
      maxHeight = size.height;

const BoxConstraints.tightFor({
    double width,
    doubleheight, }) : minWidth = width ! =null ? width : 0.0, maxWidth = width ! =null ? width : double.infinity, minHeight = height ! =null ? height : 0.0, maxHeight = height ! =null ? height : double.infinity;
    
BoxConstraints tighten({ double width, double height }) {
    return BoxConstraints(minWidth: width == null ? minWidth : width.clamp(minWidth, maxWidth),
                              maxWidth: width == null ? maxWidth : width.clamp(minWidth, maxWidth),
                              minHeight: height == null ? minHeight : height.clamp(minHeight, maxHeight),
                              maxHeight: height == null ? maxHeight : height.clamp(minHeight, maxHeight));
  }

Copy the code

When the minimum constraint is 0.0 in an axis direction, that axis direction is considered loose.

  BoxConstraints.loose(Size size)
    : minWidth = 0.0,
      maxWidth = size.width,
      minHeight = 0.0,
      maxHeight = size.height;
      

  BoxConstraints loosen() {
    assert(debugAssertIsValid());
    return BoxConstraints(
      minWidth: 0.0,
      maxWidth: maxWidth,
      minHeight: 0.0,
      maxHeight: maxHeight,
    );
  }
Copy the code

The axial constraint is finite when the maximum constraint is less than double. Infinity.

 bool get hasBoundedWidth => maxWidth < double.infinity;
 
 bool get hasBoundedHeight => maxHeight < double.infinity;
Copy the code

When the maximum constraint on an axis is equal to double. Infinity, the axis constraint is unbounded. If the maximum and minimum constraints are double. Infinity, the axial constraint is exbanding.

  const BoxConstraints.expand({
    double width,
    doubleheight, }) : minWidth = width ! =null ? width : double.infinity, maxWidth = width ! =null ? width : double.infinity, minHeight = height ! =null ? height : double.infinity, maxHeight = height ! =null ? height : double.infinity;
Copy the code

Finally, nodes need to convert constraints to dimensions at layout time. The size obtained here is considered to satisfy the constraint.

  Size constrain(Size size) {
    Size result = Size(constrainWidth(size.width), constrainHeight(size.height));
    return result;
  }
  
  double constrainWidth([ double width = double.infinity ]) {
    return width.clamp(minWidth, maxWidth);
  }

  double constrainHeight([ double height = double.infinity ]) {
    return height.clamp(minHeight, maxHeight);
  }
Copy the code

Layout example

We know that the root node of render Tree is RenderView. RendererBinding Creates RenderView with a configuration parameter of type ViewConfiguration:

void initRenderView() {
   assert(renderView == null);
   renderView = RenderView(configuration: createViewConfiguration(), window: window);
   renderView.scheduleInitialFrame();
 }
Copy the code

ViewConfiguration is defined as follows, with a size property and a device pixel ratio property:

@immutable
class ViewConfiguration {

  const ViewConfiguration({
    this.size = Size.zero,
    this.devicePixelRatio = 1.0});final Size size;

  final double devicePixelRatio;
}
Copy the code

The ViewConfiguration instance is created by createViewConfiguration() :

ViewConfiguration createViewConfiguration() {
    final double devicePixelRatio = window.devicePixelRatio;
    return ViewConfiguration(
      size: window.physicalSize / devicePixelRatio,
      devicePixelRatio: devicePixelRatio,
    );
  }
Copy the code

As you can see, the size is the physical pixel size of the window divided by the ratio of device pixels. On the Nexus5, the physical pixel size of a full-screen window (window.physicalSize) is 1080×1776. The devicePixelRatio (window.devicepixelratio) is 3.0. The final ViewConfiguration size property is 360×592.

So let’s look at how RenderView does layout:

@override
  void performLayout() {
    _size = configuration.size;
    if(child ! =null)
      child.layout(BoxConstraints.tight(_size));
  }

Copy the code

The root node generates a strict box constraint based on the configured size, which, in the case of the Nexus5, is 360 for both maximum and minimum width, and 592 for both maximum and minimum height. This strict constraint is passed when the layout() of the child node is called.

Suppose we wanted to center the screen with a 100×100 rectangle, the code would look like this:

runApp(Center(child: Container(width: 100, height: 100, color: Color(0xFFFF9000))));Copy the code

The render Tree structure is as follows:

RenderView’s child node is a RenderPositionedBox. The layout function is as follows:

@override
  void performLayout() {

    if(child ! =null) {
      child.layout(constraints.loosen(), parentUsesSize: true);
      size = constraints.constrain(Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.infinity,
                                            shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.infinity)); alignChild(); }}Copy the code

The constraints here come from the root RenderView. As we analyzed earlier, this is a 360×592 tight constraint. A call to layout() gives the child node a new constraint that has been loosened from its own strict constraints. That is, the child node is [0-360]x[0-592]. ParentUsesSize is set to true.

The RenderConstrainedBox child is then laid out:

 @override
  void performLayout() {
    if(child ! =null) {
      child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
      size = child.size;
    } else{ size = _additionalConstraints.enforce(constraints).constrain(Size.zero); }}Copy the code

The layout function for the child node RenderDecoratedBox is called. What are the constraints on the child node? _additionalConstraints comes from the 100×100 size we gave us in the Container. From the analysis above, this is a strict constraint. The parent gives us [0-360]x[0-592]. Create a new constraint by calling enforce() :

BoxConstraints enforce(BoxConstraints constraints) {
    return BoxConstraints(
      minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
      maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
      minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
      maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
    );
  }
Copy the code

As you can see from the above code, the new constraint is the strict 100×100 constraint. Finally, we come to the layout of the RenderDecoratedBox:

 @override
  void performLayout() {
    if(child ! =null) {
      child.layout(constraints, parentUsesSize: true);
      size = child.size;
    } else{ performResize(); }}Copy the code

Since it is a leaf node, it has no children, so it goes the else branch and calls performResize() :

@override
  void performResize() {
    size = constraints.smallest;
  }
Copy the code

The default layout without children is to make yourself as small as possible within the current constraints. So this is going to be 100×100;

The “click” of the layout process is now complete. This process is that the parent node generates constraints for its children based on its own configuration, and then asks the children to make layouts based on the constraints of the parent node.

Once done, it’s time to go up. Back to the parent of the leaf, RenderConstrainedBox:

child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
      size = child.size;
Copy the code

Nothing. I set the baby’s size to my own, so I’ll be as old as the baby is. Above that, we get to the RenderPositionedBox:

child.layout(constraints.loosen(), parentUsesSize: true);
      size = constraints.constrain(Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.infinity,
                                            shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.infinity));
      alignChild();
Copy the code

ShrinkWrapWidth and shrinkWrapHeight are both false. And the constraints are 360×592 strict constraints, so the resulting size is 360×592. And the child node is 100×100, so you need to know where to place the child node inside yourself, so call alignChild()

  void alignChild() {
    _resolve();
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
  }
Copy the code

The Alignment of child nodes within parent nodes is determined by Alignment.

class Alignment extends AlignmentGeometry {
  const Alignment(this.x, this.y)
  
  final double x;

  final double y;

  @override
  double get _x => x;

  @override
  double get _start => 0.0;

  @override
  double get _y => y;

  /// The top left corner.
  static const Alignment topLeft = Alignment(1.0.1.0);

  /// The center point along the top edge.
  static const Alignment topCenter = Alignment(0.0.1.0);

  /// The top right corner.
  static const Alignment topRight = Alignment(1.0.1.0);

  /// The center point along the left edge.
  static const Alignment centerLeft = Alignment(1.0.0.0);

  /// The center point, both horizontally and vertically.
  static const Alignment center = Alignment(0.0.0.0);

  /// The center point along the right edge.
  static const Alignment centerRight = Alignment(1.0.0.0);

  /// The bottom left corner.
  static const Alignment bottomLeft = Alignment(1.0.1.0);

  /// The center point along the bottom edge.
  static const Alignment bottomCenter = Alignment(0.0.1.0);

  /// The bottom right corner.
  static const Alignment bottomRight = Alignment(1.0.1.0);

Copy the code

It contains two floating-point coefficients. The combination of these two coefficients defines common Alignment patterns, such as Alignment(-1.0, -1.0) in the upper left corner. The top center is an Alignment(0.0, -1.0). In the upper right corner is Alignment(1.0, -1.0). The Alignment(0.0, 0.0) that we used is centered vertically and horizontally. So how do you calculate the offset from Alignment? AlongOffset (size-child.size) is called with the Alignment. AlongOffset (size-child-size) we saw above.

Offset alongOffset(Offset other) {
    final double centerX = other.dx / 2.0;
    final double centerY = other.dy / 2.0;
    return Offset(centerX + x * centerX, centerY + y * centerY);
  }
Copy the code

The input parameter is the size of the parent minus the size of the child, which is the free space of the parent. You take the width and the width and divide by 2 to get the median. The offset is then obtained by multiplying each median by the coefficient of Alignment. Isn’t that clever? Our example is centered vertically and horizontally, where x and y are 0. So the offset obtained is [130,246].

Back to alignChild(), after taking the offset, the parent saves the offset with the child by setting childparentData.offset. This offset will be used later in the drawing process.

Finally, we return to the root node, RenderView. At this point, “on” of the layout process is completed. It can be seen that in the second half of the process, the parent node may decide its own size according to the size of the child node, and the position of the child node within the process may also be determined according to the size of the child node and its own size.

conclusion

This article introduces the layout stage of Flutter rendering pipeline. The layout stage is mainly to master the process of “down and up”, which is the transfer of constraints layer by layer and the transfer of dimensions layer by layer. This article doesn’t cover the details of the various layouts, but you need to understand the layout process and how it is implemented by referring to the source code of the RenderObject.