Flutter自定义View以及响应式UI框架原理

2,313 阅读4分钟

前言

Flutter原生框架提供了MaterialDesign和Cupertino两种风格的UI,默认支持了非常多的样式,不过想做个性化的控件仍然需要我们进行自定义。

Flutter像android一样也提供了一套画图API,下面我们就自己动手做一个简单的Demo,熟悉下自定义Widget的流程,然后探究下界面绘制的原理。

UI层级框架

我们都知道,Flutter的UI框架有三级结构:Widget,Element,RenderObject。Element作为中间层负责维护整个布局的创建和更新,Widget和Element一一对应,但Element并不一定都会持有一个RenderObejct。

element.png

这里我选择比较简单的LeafRenderObjectElement类型自定义了一个RenderObject,效果图如下:

screen.gif

其中一个是RenderObejct实现,另一个是CustomPaint

自定义RenderObject实现过程

需要重写Widget,Element,RenderObject三部分:

SixStarWidget

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

  SixStarWidget(this._paintColor, this._starSize);

  /// 在其父Widget对应的Element的updateChild方法中调用
  @override
  LeafRenderObjectElement createElement() {
    return SixStarElement(this);
  }

  /// 在mount方法中调用
  @override
  RenderObject createRenderObject(BuildContext context) {
    return SixStarObject(_paintColor, _starSize);
  }

  /// 在widget重建时会执行此方法
  /// 这里的renderObject是复用的,如果这里不更新RenderObject, 那么UI不会改变
  @override
  void updateRenderObject(BuildContext context, SixStarObject renderObject) {
    renderObject
      ..paintColor = _paintColor
      ..starSize = _starSize;
  }
}

SixStarElement

/// 叶子节点
class SixStarElement extends LeafRenderObjectElement {
  SixStarElement(LeafRenderObjectWidget widget) : super(widget);
}

SixStarObject

根据RenderObject的注释,RenderObject没有定义坐标系以及各类布局规则,自行实现布局绘制较为复杂,而RenderBox定义了与android相同的笛卡尔直角坐标系以及布局所依赖的其他多种规则。除非不想使用直角坐标系,应该用RenderBox替换RenderObject

所以这里我们还是乖乖听话,直接选择从RenderBox入手,代码请戳这里查看

UI更新时的处理流程

为什么重写上面的方法就可以实现布局的更新呢?还有,Flutter可以实现响应式更新UI的原理是什么呢?既然setState不是开启界面刷新的直接动作,那什么时候才会真正开始刷新UI呢?

答案就是Vsync垂直同步信号到来时。以获取到Vsync信号为分界线,UI刷新流程分为beforeDrawFrame和beginDrawFrame两部分,下面整体介绍一下:

在刷新页面setState时(beforeDrawFrame)

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

其中StatefulElement.markNeedsBuild()会把element标记为_dirty = true,然后调用BuildOwner.scheduleBuildFor(this),把element添加到dirty列表中:

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

记住这个类BuildOwner,它是承接前后两个流程的桥梁。 beforeDrawFrame这一部分很简单,只做了把element添加到dirtyList一件事。

下一个Vsync信号到达后(beginDrawFrame)

Vsync信号到达后,flutter-engine层会自动调用到framework层的WidgetsBinding.drawFrame

  void drawFrame() {
      ...
      buildOwner.buildScope(renderViewElement); // 关键点1
      super.drawFrame(); // 关键点2
      buildOwner.finalizeTree(); // 关键点3
      ...
  }
关键点1

BuildOwner.buildScope(RenderViewElement)方法会遍历_dirtyElements执行element.rebuild(这里的RenderViewElement就是根节点RootRenderObjectElement,它在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(); // 执行了element.performRebuild()
        index += 1;
      }
    } finally {
      for (Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
    }
  }

这里的performRebuild方法在上面整理的Element依赖图中的两个Element子类被分别重写:

  • RenderObjectElement会执行updateRenderObject,而ComponentElement中会执行updateChild,这个方法是Flutter布局构建的核心,也就是我们平时所说的view-diff算法所在,它的总策略如下:

    - 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].
  • 看到在上面流程中会在多个位置执行widget.updateRenderObject,这个方法我们很眼熟,之前SixStarWidget里重写过。在这个方法中,我们更新了RenderObject的相关属性,在RenderObject内部的setter方法中调用了以下方法:

    1. 在 markNeedsPaint() 时会把RenderObject自身添加到 PipelineOwner 的 _nodesNeedingPaint 列表
    2. 在 markNeedsLayout()时会把RenderObject自身添加到 PipelineOwner 的 _nodesNeedingLayout列表

现在我们的UI更新数据已经来到了PipelineOwner

关键点2

关键点2处WidgetsBinding类内的super.drawFrame()是执行的mixin RenderBinding混入类的方法:

@protected
void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout(); // 遍历_nodesNeedingLayout列表,执行performLayout()
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint(); // 遍历_nodesNeedingPaint列表,执行到paint(context, offset)
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
关键点3

上面关键点1中updateChild中执行的deactivateChild(child)中会把要移除的节点添加到BuildOwner的待移除列表中BuildOwner_inactiveElements.add(child); 然后在关键点3处的buildOwner.finalizeTree()中会执行_inactiveElements._unmountAll(),遍历所有待移除element:

    element.visitChildren((Element child) {
      assert(child._parent == element);
      _unmount(child);
    });
    // 在RenderObjectElement子类中执行widget.didUnmountRenderObject
    // 在StatefulElement中执行_state.dispose()
    element.unmount();

这就是Flutter的整个刷新流程,补充一张流程图

flutter-flow.png

CustomPaint

CustomPaint也可以做到类似效果,这种方式也是重写了三层结构,不过进行了封装:

  • Widget就是CustomPaint自身,它继承了SingleChildRenderObjectWidget
  • ElementSingleChildRenderObjectElement
  • RenderObjectRenderCustomPaint,它继承了RenderProxyBox extends RenderBox with xx

CustomPaint的一个很大优势在于它是一个会自动重建的Widget,所以不用像RenderObject一样要考虑维护参数更新、处理繁琐的标记dirty等。一般情况下,使用CustomPaint自定义widget是更好地选择。

实现代码请戳这里