前言
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
自身,它继承了SingleChildRenderObjectWidget
Element
是SingleChildRenderObjectElement
RenderObject
是RenderCustomPaint
,它继承了RenderProxyBox extends RenderBox with xx
。
CustomPaint
的一个很大优势在于它是一个会自动重建的Widget,所以不用像RenderObject一样要考虑维护参数更新、处理繁琐的标记dirty等。一般情况下,使用CustomPaint
自定义widget是更好地选择。