先来看看某互联网公司前端开发和产品的日常交流(互掐):
很精彩吧,这种故事经常在互联网公司上演,那你可能会问这和本篇文章有什么关系呢?答案是没有关系。
到这里先别急着骂我哈,小编先来捋捋是咋个回事儿,作为一个从来都是和产品和平相处(苦大仇深)的App前端开发,每次碰到类似这种的需求心里都想对产品问候几遍,但是需要装X的时候,咱们得上啊,比人会的咱也会,别人不会的咱还得会,比如说 Flutter
的自定义绘制。
你可能会问,这玩意儿能干啥? Flutter
的内置组件还不够用吗?是的,Flutter
提供的内置组件的确可以满足大部分UI需求,但有时候需要实现一些特殊的UI效果,比如自定义图形(不规则的图形)、动画、渐变背景等,这时候就需要使用自定义绘制来实现。除了可以高度定制化的 UI
效果,同时可以减少 UI
的层级嵌套,优化 UI
性能,好处是不是很多。
比如上图中显示当前温度的圆形进度条,内置的 Widget
组件就没法儿实现了,这里就需要用到 Flutter
中的 CustomPainter
。
CustomPainter
是啥?
CustomPainter
是 Flutter
中的一个抽象类,用于绘制自定义的图形和图像。通过实现 CustomPainter
类并重写其 paint
方法,开发者可以自由地定义绘制逻辑,从而实现各种复杂的绘图效果。下面使用 CustomPainter
来绘制一个简单的自定义图形.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| class CustomPainterPagePage extends StatefulWidget { const CustomPainterPagePage({Key? key}) : super(key: key);
@override State<CustomPainterPagePage> createState() => _CustomPainterPagePageState(); }
class _CustomPainterPagePageState extends State<CustomPainterPagePage> {
@override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xffF2F4F5), body: CustomPaint( painter: MyCustomPainter(), child: const SizedBox( width: 200.0, height: 200.0, ), ), ); } }
class MyCustomPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue ..strokeWidth = 3.0 ..style = PaintingStyle.fill;
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50.0, paint); }
@override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } }
|
效果:
从上面的例子中可以看到使用 CustomPainter
绘制自定义图形有以下几个步骤:
- 创建一个继承自
CustomPainter
的子类 MyCustomPainter
,并实现其中的 paint
方法来定义绘图逻辑。在 paint
方法中,可以使用 Canvas API
来执行各种绘制操作,如绘制路径、文本、图像等。
- 将自定义的绘制类
MyCustomPainter
的实例传递给 CustomPaint
的 painter
属性,即可将自定义的绘制逻辑应用到 Widget
中。
CustomPaint
介绍
下面是 CustomPaint
的构造函数:
1 2 3 4 5 6 7 8 9
| const CustomPaint({ super.key, this.painter, this.foregroundPainter, this.size = Size.zero, this.isComplex = false, this.willChange = false, super.child, })
|
child
:子节点。
painter
: 背景画笔,会显示在 child
后面;
foregroundPainter
: 前景画笔,会显示在 child
前面
size
:当 child
为 null
时,代表默认绘制区域大小,如果有 child
则忽略此参数,画布尺寸则为 child
尺寸。如果有 child
但是想指定画布为特定大小,可以使用 SizeBox
包裹 CustomPaint
实现。
isComplex
:是否复杂的绘制,如果是,Flutter
会应用一些缓存策略来减少重复渲染的开销。
willChange
:和 isComplex
配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变。
CustomPainter
源码
下面是搂出的 CustomPainter
源码,为了好理解,小编在每个函数上面做了注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| abstract class CustomPainter extends Listenable { const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
final Listenable? _repaint;
@override void addListener(VoidCallback listener) => _repaint?.addListener(listener);
@override void removeListener(VoidCallback listener) => _repaint?.removeListener(listener);
void paint(Canvas canvas, Size size);
SemanticsBuilderCallback? get semanticsBuilder => null;
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => shouldRepaint(oldDelegate);
bool shouldRepaint(covariant CustomPainter oldDelegate);
bool? hitTest(Offset position) => null;
@override String toString() => '${describeIdentity(this)}(${ _repaint?.toString() ?? "" })'; }
|
这里面使用频率最高的就是 void paint(Canvas canvas, Size size);
函数了,Canvas
就是画布,Size
是当前绘制区域大小,下面是 Canvas
内部常用的绘制函数。
drawLine
划线
drawPoint
画点
drawPath
画路径
drawImage
画图像
drawRect
画矩形
drawCircle
画圆
drawOval
画椭圆
drawArc
画圆弧
Paint
是画笔,可以配置画笔的各种属性如粗细、颜色、样式等。
1 2 3 4 5
| final paint = Paint() ..color = Colors.blue ..strokeWidth = 3.0 ..isAntiAlias = true ..style = PaintingStyle.fill;
|
画板刷新
在 CustomPainter
源码中,构造函数中 repaint
是干啥用的?
1 2
| const CustomPainter({ Listenable? repaint }) : _repaint = repaint; final Listenable? _repaint;
|
其实 Fultter
源码注释文档已经告诉我们了,
翻译过来就是,触发重绘的最高效方式是:
- 继承
[CustomPainter]
类,并在构造函数提供一个 'repaint'
参数,当需要重新绘制时,该对象会进行通知它的监听者。
- 继承
[Listenable]
(比如通过 [ChangeNotifier]
)并实现 [CustomPainter]
,这样对象本身就可以直接提供通知。
可能你会问直接 setState
干不就完了吗?还用得着这么麻烦。setState
当然是可以,但咱们是对程序性能有追求的,而且还得根据具体使用的场景。setState
重建的范围太大,如果绘制的是一个大且复杂的自定义图形,加上 CustomPaint
还有一个 child
子节点,亦或者还有一个高频率的动画和滑动,这些情况下用 setState
来销毁再重建 Widget
有可能直接影响页面的流畅度。下面整个例子来实现触发重绘的最高效方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class SizeChangedPainter extends CustomPainter { final Animation<double> animation; SizeChangedPainter({required this.animation});
@override void paint(Canvas canvas, Size size) { double rectWidth = animation.value * size.width; double rectHeight = animation.value * size.height;
Paint paint = Paint()..color = Colors.blue; canvas.drawRect(Rect.fromLTRB(0, 0, rectWidth, rectHeight), paint); }
@override bool shouldRepaint(covariant SizeChangedPainter oldDelegate) { return oldDelegate.animation.value != animation.value; } }
|
页面调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| class CustomPainterPagePage extends StatefulWidget { const CustomPainterPagePage({Key? key}) : super(key: key);
@override State<CustomPainterPagePage> createState() => _CustomPainterPagePageState(); }
class _CustomPainterPagePageState extends State<CustomPainterPagePage> with SingleTickerProviderStateMixin {
late AnimationController _controller; late Animation<double> _animation;
@override void initState() { super.initState();
_controller = AnimationController( vsync: this, duration: const Duration(seconds: 3), ); _animation = Tween<double>(begin: 0.2, end: 3.0).animate(_controller); _controller.forward(); }
@override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xffF2F4F5), body: Column( children: [ CustomPaint( painter: SizeChangedPainter(animation: _animation), child: const SizedBox( width: 200.0, height: 200.0, ), ), ], ), ); } }
|
上面的例子可以看出将 Animation<double>
通过构造函数赋值给成员变量 repaint
。而 repaint
是 Listenable 可监听对象类型,当 repaint
也就是 _animation
值发送变化时,会通知画板调用 paint
实现重绘,效果如下:
好了,先啰嗦到这里了,下篇来实现一个有难度点儿的自定义图形,敬请期待吧。我的公众号:Flutter技术实践,记得点赞加关注哦。