可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint
组件,自定义 CustomPainter
对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来
的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint
组件来画的,其实 CustomPaint
组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试
、调试
及源码分析
来给出一些在绘制时被忽略
或从未知晓
的东西,而有些要点如果被忽略,就很可能出现问题。
前面六篇应该对 CustomPaint
组件相关的知识说得淋漓尽致了。但 CustomPaint
在源码中的应用只有大约 20 个组件,绝大多数可视的组件都是其他方式绘制的。当你深入了解这些组件的绘制时,就会发现,无论是 CustomPaint
还是其他组件,它们最终都是基于 RenderObject#paint
进行的绘制操作。那 CustomPaint
相比于 RenderObject#paint
有着什么优劣区别呢?
优势在于:CustomPaint
作为封装,内部维护着 RenderCustomPaint
渲染对象处理通用的逻辑。这样最大的好处是将 RenderObject
相关的操作封装框架中,抽象出 CustomPainter
暴露给用户,提供绘制的接口。这样框架的使用者就无须和 RenderObject
打交道,同时也能通过回调的 Canvas
操作绘制相关方法,总之就是方便了用户使用。
劣势在于:越是底层的东西,可操作性越大,就越灵活,用起来或理解起来就越复杂。反之,越是上层封装的东西,可操作性越小,就越死板,用起来或理解起来就越简单。为了使用方便,必然要有所牺牲,CustomPainter
暴露出的操作十分有限,刷新和尺寸控制没有直接使用 RenderObject
灵活 。
仅停在上层的使用,虽然也不会影响你做出东西。但能从下往上看,便可以知其然,知其所以然
。困惑和疑虑能从原理上给出有力的分析,用起来就不会畏首畏尾,出错时也会有一定的应变能力。本文会介绍几个非 CustomPainter
绘制的组件,看看源码中是如何使用 RenderObject
的。
ColoredBox
的作用就是为自己所在的 size
区域填充颜色。Container
组件源码的 color
属性就是集成该组件实现的。
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.green,
child: SizedBox(
height: 200,
width: 200,
),
);
}
}
复制代码
ColoredBox
继承自 SingleChildRenderObjectWidget
,说明它可以有一个 child
,并且是属于RenderObjectWidget
一族。使用它完成需要创建和更新 RenderObject
的任务,可以看到这里相关的 RenderObject
是 _RenderColoredBox
。也就是说绘制的任务是在 _RenderColoredBox
中完成的。
class ColoredBox extends SingleChildRenderObjectWidget {
const ColoredBox({@required this.color, Widget child, Key key})
: assert(color != null),
super(key: key, child: child);
final Color color;
@override
_RenderColoredBox createRenderObject(BuildContext context) {
return _RenderColoredBox(color: color);
}
@override
void updateRenderObject(BuildContext context, _RenderColoredBox renderObject) {
renderObject.color = color;
}
//略...
}
复制代码
_RenderColoredBox
渲染对象源码下面就是 _RenderColoredBox
的全部源码。在 paint
方法中,当尺寸大于 Size.zero
时,会使用传入的颜色绘制矩形。渲染对象会形成树状结构,成为 渲染树
。 如果_RenderColoredBox
的 child
非空,会再绘制子渲染对象。
class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
_RenderColoredBox({@required Color color})
: _color = color,
super(behavior: HitTestBehavior.opaque);
Color get color => _color;
Color _color;
set color(Color value) {
assert(value != null);
if (value == _color) {
return;
}
_color = value;
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
if (size > Size.zero) {
// tag 1: 绘制矩形
context.canvas.drawRect(offset & size, Paint()..color = color);
}
if (child != null) {
// tag 2 : 孩子非空则绘制孩子
context.paintChild(child, offset);
}
}
}
复制代码
结合之前 CustomPaint
组件的绘制原理,整体来看是一致的,只不过这里将绘制方法实现了,而 RenderCustomPaint
将绘制逻辑抽象出去,交由用户处理。
Image 组件是一个 StatefulWidget
,该类组件,其 State 依赖其他的 Widget 完成构建任务,自身不承担创建渲染对象的任务。如下,_ImageState
主要依赖于 RawImage
进行构建。
RawImage
继承自 LeafRenderObjectWidget
,它是一个 RenderObjectWidget
,所以需要完成 创建和维护 RenderObject
的任务。
其中 RawImage#createRenderObject
返回的是 RenderImage
,它是一个 RenderObject
对象,也就是说图片的绘制工作将由该对象完成。
RenderImage
是一个 RenderObject
对象,这里只看它的 paint
方法。如下,当 _image
非 null
时,会执行 paintImage
方法,将 canvas 及需要的绘制参数传入。
在 paintImage
方法中,最终还是通过 canvas
绘制图片的相关 API
进行操作的。所以我们传入 Image 组件中的参数都可以在 RenderImage
中找到其使用的场景和作用。
Image 组件是一个 StatelessWidget
,该类组件,依赖其他的 Widget 完成构建任务。自身不承担创建渲染对象的任务。如下,Text
主要依赖于 RichText
进行构建。
RichText
继承自 MultiChildRenderObjectWidget
,是一个 RenderObject
对象。所以需要完成 创建和维护 RenderObject
的任务。
其中 RichText#createRenderObject
返回的是 RenderParagraph
,它是一个 RenderObject
对象,也就是说文字的绘制工作将由该对象完成。
RenderParagraph
渲染对象RenderParagraph
是一个 RenderObject
对象,这里只看它的 paint
方法。如下,文字的绘制通过 —textPainter
成员完成。
TextPaint#paint
通过 canvas.drawParagraph
完成文字的绘制。
可以看出,组件并不是自身来完成绘制工作,而是通过对应的 RenderObject
进行绘制。要知道 RenderObject
除了绘制之外,还有一个重要的任务,就是 布局
。 所以 Flutter 框架内 RenderObject
是一个非常重要的类。和 Widget
不同,一个 RenderObject
的生命较长,在重新构建时,只是更新了 Widget
对象,并用新的 Widget
提供的信息对 RenderObject
进行 更新
。这也是 Flutter
框架一个非常好的处理。关于这点在 Flutter 绘制探索 4 | 深入分析 setState 重建和更新 里有详细论述。
Widget
是我们最先接触的对象,它最大的特点是 所以的成员属性需要是 final
。这表示:该族对象,一旦实例化就无法修改自身配置属性。需要新的值时,会被重新创建
含有新新配置信息的对象。是炮灰般的存在。按照继承关系,大致可以分为如下五种:
[1]. StatelessWidget 不可变状态组件
[2]. StatefulWidget 可变状态组件
[3]. ProxyWidget 代理组件
[4]. PreferredSizeWidget 固定尺寸组件
[5]. RenderObjectWidget 渲染对象组件
其实我更想把 Widget
分为两种 组合型 Widget
和 渲染型 Widget
。上面的前四种 Widget
都是使用已有的 Widget
进行组合,并不承担维护 RenderObject
的任务。
Flutter 框架中,Widget 主要用途就是创建 Element
。元素分为两大类 ComponentElement
组合型元素 和 RenderObjectElement
渲染型元素。在根 Element#mount
会通过依次触发子元素的 mount
,从而形成一个元素树结构。
RenderObjectElement
会持有 RenderObject
成员,该成员的创建是由 RenderObjectWidget
完成的。这句话,就是 Element
、RenderObject
、Widget
三者最重要的关系。而 ComponentElement
不持有 RenderObject
,所以和渲染对象是无关
的,只是像它的名字那样进行 Component
(组合)。
RenderObject
被 RenderObjectElement
持有,被 RenderObjectWidget
创建。它的实例化时机在 RenderObjectElement#mount
时,创建后会进行 attach
关联到 渲染树
中,从跟依次形成渲染树。
RenderObject
负责 绘制 paint
和 布局 layout
,是最核心的对象。无论是可视的组件,还是用于布局的组件,它们功能实现都依赖于 RenderObject
。我们如果能力足够,也可以效仿源码中的处理,自己实现 RenderObject
进行绘制、布局。
到这里,本系列就结束了。七天日更,七日打卡。两年前,第一次接触 Flutter ,也是 七天的日更 ,不过当初Flutter 的知识相关的非常少,层次较低,不规范的使用也在所难。随着对 Flutter 认知的加深,也希望在此通过自己的分享,来展现一个更真实的 Flutter 世界。通过这七篇文章,希望能让你对 Flutter 有一个更全面的认识,特别是在绘制方面,通过了解内在实现,从而做到游刃有余。下面对七篇做一个特写:
@张风捷特烈 2021.01.17 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com --
~ END ~