阅读 232

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

前言

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

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

UI层级框架

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

element

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

在这里插入图片描述

其中一个是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
      ...
  }
复制代码
关键点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].
    /// 其中child是此Element持有的旧的子element,newWidget是通过Stateless、StatefulWidget的build方法构建出的
    Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
      ...
      // 第一列的两种情况
      if (newWidget == null) {
        if (child != null) {
          // 移除child: child.detachRenderObject()
          deactivateChild(child);
        }
        return null;
      }
      // 第二列第二排情况: 都不为null, 需要更新
      if (child != null) {
        // 如果新旧为同一个widget时, 更新下位置即可
        if (child.widget == newWidget) {
          if (child.slot != newSlot) {
            // 如果持有MultiChild的Element内child的位置有变化时更新位置
            updateSlotForChild(child, newSlot);
          }
          return child;
        }
        // 通过runtimeType和key来判断(这里就是为什么要给widget添加key的原因)
        if (Widget.canUpdate(child.widget, newWidget)) {
          if (child.slot != newSlot) {
            updateSlotForChild(child, newSlot); 
          }
          // element更新widget, 由两个子类重写:
          // RenderObjectElement会在子类中执行 !!updateRenderObject!!
          // ComponentElement会在子类中执行rebuild方法,最终调用到子Element的updateRenderObject
          child.update(newWidget);
          ...
          return child;
        }
        // 如果不是上面的两种情况,说明不能更新,就把旧的子element放入_inactiveElements,然后从render tree移除
        deactivateChild(child);
        assert(child._parent == null);
      }
      // 第二列第一排情况: old == null, new != null
      // 调用element.mount方法,两个子类的表现为:
      // RenderObjectElement会在子类中执行 !!createRenderObject!!
      // ComponentElement仍会执行rebuild方法
      return inflateWidget(newWidget, newSlot);
    }
    复制代码
  • 看到在上面流程中会在多个位置执行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.
}
复制代码

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

在这里插入图片描述

CustomPaint

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

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

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

实现代码请戳这里

关注下面的标签,发现更多相似文章
评论