@charset "UTF-8";.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1:first-child,.markdown-body h2:first-child,.markdown-body h3:first-child,.markdown-body h4:first-child,.markdown-body h5:first-child,.markdown-body h6:first-child{margin-top:-1.5rem;margin-bottom:1rem}.markdown-body h1:before,.markdown-body h2:before,.markdown-body h3:before,.markdown-body h4:before,.markdown-body h5:before,.markdown-body h6:before{content:"#";display:inline-block;color:#3eaf7c;padding-right:.23em}.markdown-body h1{position:relative;font-size:2.5rem;margin-bottom:5px}.markdown-body h1:before{font-size:2.5rem}.markdown-body h2{padding-bottom:.5rem;font-size:2.2rem;border-bottom:1px solid #ececec}.markdown-body h3{font-size:1.5rem;padding-bottom:0}.markdown-body h4{font-size:1.25rem}.markdown-body h5{font-size:1rem}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body strong{color:#3eaf7c}.markdown-body img{max-width:100%;border-radius:2px;display:block;margin:auto;border:3px solid rgba(62,175,124,.2)}.markdown-body hr{border:none;border-top:1px solid #3eaf7c;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;overflow-x:auto;padding:.2rem .5rem;margin:0;color:#3eaf7c;font-weight:700;font-size:.85em;background-color:rgba(27,31,35,.05);border-radius:3px}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75;border-radius:6px;border:2px solid #3eaf7c}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{font-weight:500;text-decoration:none;color:#3eaf7c}.markdown-body a:active,.markdown-body a:hover{border-bottom:1.5px solid #3eaf7c}.markdown-body a:before{content:"⇲"}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #3eaf7c}.markdown-body thead{background:#3eaf7c;color:#fff;text-align:left}.markdown-body tr:nth-child(2n){background-color:rgba(62,175,124,.2)}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:.5rem solid;border-color:#42b983;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body details{outline:none;border:none;border-left:4px solid #3eaf7c;padding-left:10px;margin-left:4px}.markdown-body details summary{cursor:pointer;border:none;outline:none;background:#fff;margin:0 -17px}.markdown-body details summary::-webkit-details-marker{color:#3eaf7c}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body ol li::marker{color:#3eaf7c}.markdown-body ul li{list-style:none}.markdown-body ul li:before{content:"•";margin-right:4px;color:#3eaf7c}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}
零:前言
1. 系列引言
可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint
组件,自定义 CustomPainter
对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来
的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint
组件来画的,其实 CustomPaint
组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试
、调试
及源码分析
来给出一些在绘制时被忽略
或从未知晓
的东西,而有些要点如果被忽略,就很可能出现问题。
2. 使用 CustomPainter 容易出现的疑问
本文是第一篇,就先从 CustomPaint
开始说起。你在 Flutter 绘制中,还在使用 State#setState
来刷新画板吗?你会不会也有和下面这位哥们相同的疑惑?你是不是只能将绘制抽离一个新组建来局部刷新?通过对源码的分析和研究后,会发现对于 CustomPainter 的重绘,有一个更高效的刷新方式。本文就来分享一下这个非常重要的知识点。
一、Flutter 中自定义绘制的方式
本文的测试案例效果如下,使用 CustomPaint
组件绘制一个圆,让其执行 3 秒红转蓝
的动画。
1.自定义画板 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;
}
}
复制代码
2. 使用画板
自定义的画板想要展示出来,需要使用 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), //<--- 设置画板
),
),
);
}
}
复制代码
3.运行程序
将主程序运行后,就可以看到绘制的效果。
代码语言:javascript复制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());
}
}
复制代码
二、动画中画板的刷新
1. 较高层状态类使用的 setState
(不推荐)
通过 ValueListenableBuilder 篇,我们应该知道在较上级的 State
类中执行 setState
会导致更多的 Build
过程。如下代码中通过监听 AnimationController
,并 setState
对当前 build
方法下的节点进行更新,从而实现颜色的变化。
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)),
),
),
);
}
}
复制代码
2.退而求其次的局部刷新
(不推荐)
那也许你会说,只要降低刷新的节点,将 画板组件
单独抽离
出去,或使用 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 都会构建一次画板,这样的频率,即使是局部刷新,也不是最佳选择。那有没有一种方式,可以悄无声息
的地进行绘制,而不会触发任何组件的重构?答案是 有的!
。
第一次 | 第二次 |
---|---|
3.画板基于监听器的重绘 (推荐)
在刚才 ValueListenableBuilder
版的基础上稍作修改,我们就可以完成这个需求。首先,剔除掉 ValueListenableBuilder
,然后将 Animation
作为 ShapePainter
的成员 factor,在构造函数中传入。并使用 super(repaint: factor)
为成员 repaint
赋值。repaint
是 CustomPainter 的成员,类型为 Listenable
可监听对象,当 repaint
值变化时,会通知画板进行 paint 重绘。
---->[_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
的使用,就表示这些使用的方式相对而言是 最正规
的。
1. _CupertinoActivityIndicatorPainter
第一次的 悟道
,是在 _CupertinoActivityIndicatorPainter
源码中,也就是那个 iOS
的菊花转的绘制画板。 position 是一个 Animation
类型的对象,Animation
也是一个 Listenable
。当时发现 CupertinoActivityIndicator
中没有使用 setState
却可以触发界面的刷新,我是非常惊喜的,经过分析和研究它的实现方式,我终于发现了 CustomPainter
中 repaint
秘密。
2. ScrollbarPainter
上面说的第二种是通过继承自 Listenable
并实现 CustomPainter
的方式,如源码中的 ScrollbarPainter
。它是用来绘制 ScrollBar
组件的,通过这种方式可以让 ScrollbarPainter
即处理绘制,又处理通知。
这样,在 _CupertinoScrollbarState
中就可以将 ScrollbarPainter
作为成员变量,和 State 拥有同样的生命长度。并在某些恰当的时刻,使用该对象触发相应方法进行画布重绘。
3._GlowingOverscrollIndicatorPainter
当时还有一个疑惑是,repaint
中只是传入一个 Listenable
对象,那么多个属性如何去监听呢,比如多个动画同时执行。于是看到 _GlowingOverscrollIndicatorPainter
时便豁然开朗。它画的是滑动到顶底光晕的那个东西。 其中传入的 leadingController
、trailingController
两个可监听对象。除此之外,额外传入 repaint
。
可以通过 Listenable.merge
将多个可监听对象融合。
4. _PlaceholderPainter
但当我觉得 repaint
无敌之时,仍会发现,源码中有很多绘制的类并没使用 repaint
,而是向外界暴露属性进行设置。比如 _PlaceholderPainter
的矩形×,_GridPaperPainter
的网格,于是陷入沉思。
_GridPaperPainter 的源码,只是向外界暴露绘制相关属性。
最终发现了一个共性:当绘制中含有动画和滑动处理时,都会使用 repaint 设置监听对象来触发刷新
,对于仅是静态的绘制,则使用时将绘制属性暴露出去,交由外界处理,需要刷新的话,只能通过重建画板对象。其实这也很容易理解: 动画
和 滑动
的触发频率非常高,所以才会用特殊的方式进行重绘。
那么画板的重绘必须只是通过 可监听对象
吗?并非如此,虽然可以通过可监听对象来触发画布刷新,比如_PlaceholderPainter
中 color 成员变为 ValueNotifier
,但这样就会增加用户使用的复杂性。对于非频繁刷新的场景,局部刷新也就够了,这应该就是源码中,在非 动画和滑动
中不使用 repaint
的原因。但对于频繁触发的绘制,如 动画
和 滑动
一定要用。
最后想说一句:任何东西都不会完美无缺。成年人的世界,没有对错,只有适合与不适合。在一切的困惑、质疑、反驳
之前,你应做的是 多测、多想、多看
。本文就到这里,应该算是说清楚了 CustomPainter
正确的刷新姿势,但这也仅是 绘制探索
的冰山一角,CustomPainter
与 CustomPaint
背后还有很多值得探寻的东西,会随着之后的探索,为你展开一个更加丰满的 Flutter 世界。
@张风捷特烈 2021.01.11 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~