10.7 自绘组件: DoneWidget
上一节中我们通过 CustomCheckbox 演示了如何通过自定义 RenderObject 的方式来进行UI绘制、动画调度和事件处理。本节再通过一个实例来巩固换一下。
本节的我们将实现一个 DoneWidget,它可以在创建时执行一个打勾动画,效果如下:

todo补图:
1class DoneWidget extends LeafRenderObjectWidget {
2 DoneWidget({
3 Key? key,
4 this.strokeWidth = 2.0,
5 this.color = Colors.green,
6 this.outline = false,
7 }) : super(key: key);
8
9 //线条宽度
10 final double strokeWidth;
11 //轮廓颜色或填充色
12 final Color color;
13 //如果为true,则没有填充色,color代表轮廓的颜色;如果为false,则color为填充色
14 final bool outline;
15
16
17 RenderObject createRenderObject(BuildContext context) {
18 return RenderDoneObject(
19 strokeWidth,
20 color,
21 outline,
22 )..animationStatus = AnimationStatus.forward; // 创建时执行正向动画
23 }
24
25
26 void updateRenderObject(context, RenderDoneObject renderObject) {
27 renderObject
28 ..strokeWidth = strokeWidth
29 ..outline = outline
30 ..color = color;
31 }
32}DoneWidget 有两种模式,一种是 outline 模式,该模式背景没有填充色,此时 color 表示的是轮廓线条的颜色;如果是非 outline 模式,则 color 表示填充的背景色,此时 “勾” 的颜色简单设置为白色。
接下来需要实现 RenderDoneObject,由于组件不需要响应事件,所以我们可以不用添加事件相关的处理代码;但是组件需要执行动画,因此我们可以直接使用上一节中封装的 RenderObjectAnimationMixin,具体实现代码如下:
1class RenderDoneObject extends RenderBox with RenderObjectAnimationMixin {
2 double strokeWidth;
3 Color color;
4 bool outline;
5
6 ValueChanged<bool>? onChanged;
7
8 RenderDoneObject(
9 this.strokeWidth,
10 this.color,
11 this.outline,
12 );
13
14 // 动画执行时间为 300ms
15
16 Duration get duration => Duration(milliseconds: 300);
17
18
19 void doPaint(PaintingContext context, Offset offset) {
20 // 可以对动画运用曲线
21 Curve curve = Curves.easeIn;
22 final _progress = curve.transform(progress);
23
24 Rect rect = offset & size;
25 final paint = Paint()
26 ..isAntiAlias = true
27 ..style = outline ? PaintingStyle.stroke : PaintingStyle.fill //填充
28 ..color = color;
29
30 if (outline) {
31 paint.strokeWidth = strokeWidth;
32 rect = rect.deflate(strokeWidth / 2);
33 }
34
35 // 画背景圆
36 context.canvas.drawCircle(rect.center, rect.shortestSide / 2, paint);
37
38 paint
39 ..style = PaintingStyle.stroke
40 ..color = outline ? color : Colors.white
41 ..strokeWidth = strokeWidth;
42
43 final path = Path();
44
45 //接下来画 "勾"
46 Offset firstOffset =
47 Offset(rect.left + rect.width / 6, rect.top + rect.height / 2.1);
48
49 final secondOffset = Offset(
50 rect.left + rect.width / 2.5,
51 rect.bottom - rect.height / 3.3,
52 );
53
54 path.moveTo(firstOffset.dx, firstOffset.dy);
55
56 //下面解释
57 final adjustProgress = .6;
58
59 if (_progress < adjustProgress) {
60 //第一个点到第二个点的连线做动画(第二个点不停的变)
61 Offset _secondOffset = Offset.lerp(
62 firstOffset,
63 secondOffset,
64 _progress / adjustProgress,
65 )!;
66 path.lineTo(_secondOffset.dx, _secondOffset.dy);
67 } else {
68 //链接第一个点和第二个点
69 path.lineTo(secondOffset.dx, secondOffset.dy);
70 //第三个点位置随着动画变,做动画
71 final lastOffset = Offset(
72 rect.right - rect.width / 5,
73 rect.top + rect.height / 3.5,
74 );
75 Offset _lastOffset = Offset.lerp(
76 secondOffset,
77 lastOffset,
78 (progress - adjustProgress) / (1 - adjustProgress),
79 )!;
80 path.lineTo(_lastOffset.dx, _lastOffset.dy);
81 }
82 context.canvas.drawPath(path, paint..style = PaintingStyle.stroke);
83 }
84
85
86 void performLayout() {
87 // 如果父组件指定了固定宽高,则使用父组件指定的,否则宽高默认置为 25
88 size = constraints.constrain(
89 constraints.isTight ? Size.infinite : Size(25, 25),
90 );
91 }
92}上面代码很简单,但需要注意三点:
- 我们对动画应用了easeIn 曲线,可以看到如果在 RenderObject 中对动画应用曲线,另外读者应该也能发现,曲线的本质就是对动画的进度加了一层映射,通过不同的映射规则就可以控制动画在不同阶段的快慢。
- 我们重写了 RenderObjectAnimationMixin 中的 duration,该参数用于指定动画时长。
- adjustProgress 的作用主要是将“打勾”动画氛围两部分,第一部分是第一个点和第二个点的连线动画,这部分动画站总动画时长的 前 60%; 第二部分是第二点和第三个点的连线动画,该部分动画占总时长的后 40%。