前言
Flutter原生框架提供了MaterialDesign和Cupertino两种风格的UI,默认支持了非常多的样式,不过想做个性化的控件仍然需要我们进行自定义。
Flutter像android一样也提供了一套画图API,下面我们就自己动手做一个简单的Demo,熟悉下自定义Widget的流程,然后探究下界面绘制的原理。
UI层级框架
我们都知道,Flutter的UI框架有三级结构:Widget,Element,RenderObject。Element作为中间层负责维护整个布局的创建和更新,Widget和Element一一对应,但Element并不一定都会持有一个RenderObejct。
这里我选择比较简单的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
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方法中调用了以下方法:- 在 markNeedsPaint() 时会把RenderObject自身添加到 PipelineOwner 的
_nodesNeedingPaint列表 - 在 markNeedsLayout()时会把RenderObject自身添加到 PipelineOwner 的
_nodesNeedingLayout列表
- 在 markNeedsPaint() 时会把RenderObject自身添加到 PipelineOwner 的
现在我们的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的整个刷新流程,补充一张流程图
CustomPaint
CustomPaint也可以做到类似效果,这种方式也是重写了三层结构,不过进行了封装:
Widget就是CustomPaint自身,它继承了SingleChildRenderObjectWidgetElement是SingleChildRenderObjectElementRenderObject是RenderCustomPaint,它继承了RenderProxyBox extends RenderBox with xx。
CustomPaint的一个很大优势在于它是一个会自动重建的Widget,所以不用像RenderObject一样要考虑维护参数更新、处理繁琐的标记dirty等。一般情况下,使用CustomPaint自定义widget是更好地选择。