preface

The Flutter native framework provides MaterialDesign and Cupertino styles of UI. By default, the Flutter native framework supports a wide range of styles, but customization is still required to create a personalized control.

Flutter, like Android, also provides a drawing API. In this Demo, we will familiarize ourselves with the process of customizing widgets and explore how they are drawn.

Ui-level framework

As we all know, the UI framework of Flutter has three levels of structure: Widget, Element and RenderObject. Element is the middle tier that maintains the creation and updating of the entire layout. Widgets correspond to Elements one by one, but elements don’t always have a RenderObejct.

Here I choose the simple LeafRenderObjectElement type to customize a RenderObject, the effect picture is as follows:

One is the RenderObejct implementation and the other is CustomPaint.

Customize the RenderObject implementation process

We need to rewrite Widget, Element, and RenderObject:

SixStarWidget

class SixStarWidget extends LeafRenderObjectWidget {
  final Color _paintColor;
  final double _starSize;

  SixStarWidget(this._paintColor, this._starSize);

  /// called in the updateChild method of the Element corresponding to the parent Widget
  @override
  LeafRenderObjectElement createElement() {
    return SixStarElement(this);
  }

  /// called in the mount method
  @override
  RenderObject createRenderObject(BuildContext context) {
    return SixStarObject(_paintColor, _starSize);
  }

  /// This method is executed when the widget is rebuilt
  /// The renderObject is reused. If the renderObject is not updated, the UI will not change
  @override
  voidupdateRenderObject(BuildContext context, SixStarObject renderObject) { renderObject .. paintColor = _paintColor .. starSize = _starSize; }}Copy the code

SixStarElement

/// leaves
class SixStarElement extends LeafRenderObjectElement {
  SixStarElement(LeafRenderObjectWidget widget) : super(widget);
}
Copy the code

SixStarObject

According to RenderObject’s notes, RenderObject does not define coordinate systems and various layout rules, so it is complicated to draw the layout by itself. However, RenderBox defines the same Cartesian coordinate system as Android and many other rules on which layout depends. Unless you don’t want to use cartesian coordinates, you should use RenderBox instead of RenderObject.

So we’re going to do what we’re told, and we’re going to start with RenderBox. See the code here

Process of UI update

Why would overriding the above methods update the layout? Also, how does Flutter enable responsive UI updates? Since setState is not a direct action to start the UI refresh, when will the UI refresh actually start?

The answer is Vsync when the Vsync signal arrives. The UI refresh process is divided into beforeDrawFrame and beginDrawFrame with Vsync signal as the dividing line. The following is an overview:

Refresh page setState (beforeDrawFrame)

void setState(VoidCallback fn) {
  final dynamic result = fn() as dynamic;
  if (result is Future) {
    throw FlutterError...
  }
  _element.markNeedsBuild();
}
Copy the code

The StatefulElement. MarkNeedsBuild element () will get marked as _dirty = true, then call BuildOwner. ScheduleBuildFor (this), the element to add to the list of dirty:

  void scheduleBuildFor(Element element) {
    _dirtyElements.add(element);
    element._inDirtyList = true;
  }
Copy the code

Remember the class BuildOwner, which is the bridge between the two processes. BeforeDrawFrame the beforeDrawFrame part is simple, just adding an Element to the dirtyList.

When the next Vsync signal arrives (beginDrawFrame)

The WidgetsBinding and drawFrame function is automatically called by the flutter engine layer upon arrival of the Vsync signal:

  void drawFrame() {
      ...
      buildOwner.buildScope(renderViewElement); // Key point 1
      super.drawFrame(); // Critical point 2
      buildOwner.finalizeTree(); // Keypoint 3. }Copy the code
The key point 1

BuildOwner. BuildScope (RenderViewElement) iterates over _dirtyElements and does Element. rebuild (RenderViewElement is RootRenderObjectEl) Ement, which is constructed in runApp) :

  void buildScope(Element context, [VoidCallback callback]) {
    if (callback == null && _dirtyElements.isEmpty) return;
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    try {
      while (index < dirtyCount) {
        _dirtyElements[index].rebuild(); // Execute element.performrebuild ()
        index += 1; }}finally {
      for (Element element in _dirtyElements) {
        element._inDirtyList = false; } _dirtyElements.clear(); }}Copy the code

The performRebuild method here overrides the two Element subclasses in the Element dependency diagram compiled above:

  • RenderObjectElement performs the updateRenderObject, while ComponentElement performs the updateChild. This method is at the heart of Flutter layout construction. This method is called the View-Diff algorithm. Its overall strategy is as follows:

    newWidget == null newWidget ! = null
    child == null Returns null. Returns new [Element].
    child ! = null Old child is removed, returns null. Old child updated if possible, returns child or new [Element].
  • See in the above process can be carried in multiple locations widget. UpdateRenderObject, we are very familiar, this method before SixStarWidget rewritten. In this method, we update the properties of the RenderObject and call the following methods in setter methods inside the RenderObject:

    1. The RenderObject itself is added to PipelineOwner at markNeedsPaint()_nodesNeedingPaintThe list of
    2. The RenderObject itself is added to PipelineOwner at markNeedsLayout()_nodesNeedingLayoutThe list of

Our UI update data is now available at PipelineOwner.

The key point 2

The WidgetsBinding super.drawFrame() in the WidgetsBinding class is the mixin RenderBinding method that executes:

@protected
void drawFrame() {
  assert(renderView ! =null);
  pipelineOwner.flushLayout(); // Iterate over the _nodesNeedingLayout list, execute performLayout()
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint(); // Iterate over the _nodesNeedingPaint list to paint(context, offset)
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
Copy the code
The key point 3

BuildOwner_inactiveElements. Add (child); deactivateChild(Child) executed in updateChild in key 1 above adds the node to be removed to BuildOwner’s list to be removed; _inactiveElements._unmountall () is then executed in buildowner.finalizetree () at key 3 to iterate over all elements to be removed:

    element.visitChildren((Element child) {
      assert(child._parent == element);
      _unmount(child);
    });
    / / perform in RenderObjectElement subclass widgets. DidUnmountRenderObject
    // Execute _state.dispose() in StatefulElement
    element.unmount();
Copy the code

This is the whole refresh process of Flutter. Add a flow chart

CustomPaint

CustomPaint can do the same thing, which rewrites the three-layer structure, but encapsulates it:

  • WidgetisCustomPaintItself, it inheritsSingleChildRenderObjectWidget
  • ElementisSingleChildRenderObjectElement
  • RenderObjectisRenderCustomPaintIt inheritsRenderProxyBox extends RenderBox with xx.

One big advantage of CustomPaint is that it is an auto-rebuild Widget, so you don’t have to worry about maintaining parameter updates, dealing with dirty tags, and so on, as RenderObject does. In general, customizing widgets with CustomPaint is a better choice.

The implementation code is here