0%

App开发装X指南:玩转自定义绘制

先来看看某互联网公司前端开发和产品的日常交流(互掐):

前端开发和产品日常交流

很精彩吧,这种故事经常在互联网公司上演,那你可能会问这和本篇文章有什么关系呢?答案是没有关系。

到这里先别急着骂我哈,小编先来捋捋是咋个回事儿,作为一个从来都是和产品和平相处(苦大仇深)的App前端开发,每次碰到类似这种的需求心里都想对产品问候几遍,但是需要装X的时候,咱们得上啊,比人会的咱也会,别人不会的咱还得会,比如说 Flutter 的自定义绘制。

你可能会问,这玩意儿能干啥? Flutter 的内置组件还不够用吗?是的,Flutter 提供的内置组件的确可以满足大部分UI需求,但有时候需要实现一些特殊的UI效果,比如自定义图形(不规则的图形)、动画、渐变背景等,这时候就需要使用自定义绘制来实现。除了可以高度定制化的 UI 效果,同时可以减少 UI 的层级嵌套,优化 UI 性能,好处是不是很多。

自定义图形

比如上图中显示当前温度的圆形进度条,内置的 Widget 组件就没法儿实现了,这里就需要用到 Flutter 中的 CustomPainter

CustomPainter 是啥?

CustomPainterFlutter 中的一个抽象类,用于绘制自定义的图形和图像。通过实现 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 绘制自定义图形有以下几个步骤:

  1. 创建一个继承自 CustomPainter 的子类 MyCustomPainter,并实现其中的 paint 方法来定义绘图逻辑。在 paint 方法中,可以使用 Canvas API 来执行各种绘制操作,如绘制路径、文本、图像等。
  2. 将自定义的绘制类 MyCustomPainter 的实例传递给 CustomPaintpainter 属性,即可将自定义的绘制逻辑应用到 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:当 childnull 时,代表默认绘制区域大小,如果有 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);

// 点击时是否命中,传过来 position 对于当前绘制图形视为命中的点则为true,否则为false
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 源码注释文档已经告诉我们了,

画板刷新

翻译过来就是,触发重绘的最高效方式是:

  1. 继承 [CustomPainter] 类,并在构造函数提供一个 'repaint' 参数,当需要重新绘制时,该对象会进行通知它的监听者。
  2. 继承 [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) {
// 默认返回true,表示总是需要重绘
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() {
// TODO: implement 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 实现重绘,效果如下:

CustomPainter 重绘

好了,先啰嗦到这里了,下篇来实现一个有难度点儿的自定义图形,敬请期待吧。我的公众号:Flutter技术实践,记得点赞加关注哦。

Flutter技术实践