可能提及 Flutter 绘制,你们第一反应就是用 CustomPaint
组件,自定义 CustomPainter
对象来画。Flutter 中全部能够看获得的组件,好比 Text、Image、Switch、Slider 等等,追其根源都是画出来
的,但经过查看源码能够发现,Flutter 中绝大多数组件并非使用 CustomPaint
组件来画的,其实 CustomPaint
组件是对框架底层绘制的一层封装。这个系列即是对 Flutter 绘制的探索,经过测试
、调试
及源码分析
来给出一些在绘制时被忽略
或从未知晓
的东西,而有些要点若是被忽略,就极可能出现问题。web
本文是第一篇,就先从 CustomPaint
开始提及。你在 Flutter 绘制中,还在使用 State#setState
来刷新画板吗?你会不会也有和下面这位哥们相同的疑惑?你是否是只能将绘制抽离一个新组建来局部刷新?经过对源码的分析和研究后,会发现对于 CustomPainter 的重绘,有一个更高效的刷新方式。本文就来分享一下这个很是重要的知识点。编程
本文的测试案例效果以下,使用 CustomPaint
组件绘制一个圆,让其执行 3 秒红转蓝
的动画。canvas
ShapePainter
以下自定义一个 CustomPainter
,构造函数中传入颜色 color。须要复写两个方法 paint
和 shouldRepaint
。在 paint
方法中会回调 Canvas
和 Size
对象,以供绘制使用。以下代码,绘制一个颜色为 color
的圆。数组
class ShapePainter extends CustomPainter {
final Color color;
ShapePainter({this.color});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = color;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2), size.width / 2, paint);
}
@override
bool shouldRepaint(covariant ShapePainter oldDelegate) {
return oldDelegate.color!=color;
}
}
复制代码
自定义的画板想要展现出来,须要使用 CustomPaint
组件,为其设置 painter
属性。以下代码,在实例化 ShapePainter
时传入红色。微信
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: CustomPaint( //<--- 使用绘制组件
size: Size(100, 100),
painter: ShapePainter(color: Colors.red), //<--- 设置画板
),
),
);
}
}
复制代码
将主程序运行后,就能够看到绘制的效果。markdown
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage());
}
}
复制代码
setState
(不推荐)经过 ValueListenableBuilder 篇,咱们应该知道在较上级的 State
类中执行 setState
会致使更多的 Build
过程。以下代码中经过监听 AnimationController
,并 setState
对当前 build
方法下的节点进行更新,从而实现颜色的变化。app
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController _ctrl;
@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: Duration(seconds: 3))
..addListener(_update);
_ctrl.forward();
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
void _update() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: EdgeInsets.all(20),
child: CustomPaint(
size: Size(100, 100),
painter: ShapePainter(
color: Color.lerp(Colors.red, Colors.blue, _ctrl.value)),
),
),
);
}
}
复制代码
局部刷新
(不推荐)那也许你会说,只要下降刷新的节点,将 画板组件
单独抽离
出去,或使用 ValueListenableBuilder 局部刷新不就行了吗?若是看了 ValueListenableBuilder 的源码就会发现,其实它的本质就是 组件抽离
,只不过对其进行封装,回调出 builder
简化用户使用。以下是使用 ValueListenableBuilder 局部构建的组件,这样能够不使用 setState
实现组件的重建,我仍是想要着重强调一句:并非说 setState
很差,而是看它重建的范围,ValueListenableBuilder 源码中也是基于 State#setState
进行重构的,并非一个东西非好即坏
,还须要看使用的场景和时机。框架
---->[_HomePageState#build]----
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ValueListenableBuilder(
valueListenable: _ctrl,
builder:(ctx,value,child) => CustomPaint(
size: Size(100, 100),
painter: ShapePainter(color: Color.lerp(Colors.red, Colors.blue, value)),
),
),
);
}
复制代码
也许你会以为,如今不是很好吗,如今重建只是对于 CustomPaint
而言了,已经控制了重建的粒度。但重要的一点是 CustomPaint
被重建了,ShapePainter
也会随之重建,以下的调试,是动画过程当中两次 paint 时状况。经过下面的 this
能够看出,当前对象的内存地址是不同,说明每次更新画板都是不一样的。这对于动画来讲是灾难性的,每 16 ms 都会构建一次画板,这样的频率,即便是局部刷新,也不是最佳选择。那有没有一种方式,能够悄无声息
的地进行绘制,而不会触发任何组件的重构?答案是 有的!
。less
第一次 | 第二次 |
---|---|
![]() |
![]() |
在刚才 ValueListenableBuilder
版的基础上稍做修改,咱们就能够完成这个需求。首先,剔除掉 ValueListenableBuilder
,而后将 Animation<double>
做为 ShapePainter
的成员 factor,在构造函数中传入。并使用 super(repaint: factor)
为成员 repaint
赋值。repaint
是 CustomPainter 的成员,类型为 Listenable
可监听对象,当 repaint
值变化时,会通知画板进行 paint 重绘。ide
---->[_HomePageState#build]----
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: CustomPaint(
size: Size(100, 100),
painter: ShapePainter(factor: _ctrl),
),
);
}
class ShapePainter extends CustomPainter {
final Animation<double> factor;
ShapePainter({this.factor}) : super(repaint: factor);
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Color.lerp(Colors.red, Colors.blue, factor.value);
canvas.drawCircle(
Offset(size.width / 2, size.height / 2), size.width / 2, paint);
}
@override
bool shouldRepaint(covariant ShapePainter oldDelegate) {
return oldDelegate.factor != factor;
}
}
复制代码
经过这种方式,点击时在 paint
方法断点调试,结果以下。能够看出,在完成颜色变化的同时,没有任何组件的重建,ShapePainter
对象也没有变化,是否是感受很是神奇。
第一次 | 第二次 |
---|---|
![]() |
![]() |
也许有人会问,这些你是怎么知道的?当一个疑问一直萦绕心头时,我就会想办法去研究它,而研究它最好的途径就是不断测试
和分析源码
。目标能够是 CustomPainter
的源码自己,也能够是源码中使用到CustomPainter
的地方。 其实不少知识,一直都写在源码中,只是不多人看到。经过 CustomPainter
的注释能够发现,触发重绘最高效的方式都是基于可监听对象
实现的。
触发重绘的最高效方式是:
[1]:继承 [CustomPainter] 类,并在构造函数提供一个 'repaint' 参数,
当须要从新绘制时,该对象会进行通知它的监听者。
[2]:继承 [Listenable] (好比经过 [ChangeNotifier])并实现 [CustomPainter],
这样对象自己就能够直接提供通知。
复制代码
CustomPainter
在 Flutter 框架中的应用其实 CustomPainter 在 Flutter 框架源码中的应用并非很是多,一共也就下面的 20 处。这些都是源码中对 CustomPainter
的使用,就表示这些使用的方式相对而言是 最正规
的。
_CupertinoActivityIndicatorPainter
第一次的 悟道
,是在 _CupertinoActivityIndicatorPainter
源码中,也就是那个 iOS
的菊花转的绘制画板。 position 是一个 Animation<double>
类型的对象,Animation
也是一个 Listenable
。当时发现 CupertinoActivityIndicator
中没有使用 setState
却能够触发界面的刷新,我是很是惊喜的,通过分析和研究它的实现方式,我终于发现了 CustomPainter
中 repaint
秘密。
上面说的第二种是经过继承自 Listenable
并实现 CustomPainter
的方式,如源码中的 ScrollbarPainter
。它是用来绘制 ScrollBar
组件的,经过这种方式可让 ScrollbarPainter
即处理绘制,又处理通知。
这样,在 _CupertinoScrollbarState
中就能够将 ScrollbarPainter
做为成员变量,和 State 拥有一样的生命长度。并在某些恰当的时刻,使用该对象触发相应方法进行画布重绘。
_GlowingOverscrollIndicatorPainter
当时还有一个疑惑是,repaint
中只是传入一个 Listenable
对象,那么多个属性如何去监听呢,好比多个动画同时执行。因而看到 _GlowingOverscrollIndicatorPainter
时便豁然开朗。它画的是滑动到顶底光晕的那个东西。 其中传入的 leadingController
、trailingController
两个可监听对象。除此以外,额外传入 repaint
。
能够经过 Listenable.merge
将多个可监听对象融合。
_PlaceholderPainter
但当我以为 repaint
无敌之时,仍会发现,源码中有不少绘制的类并没使用 repaint
,而是向外界暴露属性进行设置。好比 _PlaceholderPainter
的矩形×,_GridPaperPainter
的网格,因而陷入沉思。
_GridPaperPainter 的源码,只是向外界暴露绘制相关属性。
最终发现了一个共性:当绘制中含有动画和滑动处理时,都会使用 repaint 设置监听对象来触发刷新
,对于仅是静态的绘制,则使用时将绘制属性暴露出去,交由外界处理,须要刷新的话,只能经过重建画板对象。其实这也很容易理解: 动画
和 滑动
的触发频率很是高,因此才会用特殊的方式进行重绘。
那么画板的重绘必须只是经过 可监听对象
吗?并不是如此,虽然能够经过可监听对象来触发画布刷新,好比_PlaceholderPainter
中 color 成员变为 ValueNotifier<Color>
,但这样就会增长用户使用的复杂性。对于非频繁刷新的场景,局部刷新也就够了,这应该就是源码中,在非 动画和滑动
中不使用 repaint
的缘由。但对于频繁触发的绘制,如 动画
和 滑动
必定要用。
最后想说一句:任何东西都不会天衣无缝。成年人的世界,没有对错,只有适合与不适合。在一切的困惑、质疑、反驳
以前,你应作的是 多测、多想、多看
。本文就到这里,应该算是说清楚了 CustomPainter
正确的刷新姿式,但这也仅是 绘制探索
的冰山一角,CustomPainter
与 CustomPaint
背后还有不少值得探寻的东西,会随着以后的探索,为你展开一个更加丰满的 Flutter 世界。
@张风捷特烈 2021.01.11 未允禁转
个人公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~