跳到正文
Willh的个人博客 记录 · 思考 · 分享
《Flutter实战·第二版》

10.7 自绘组件: DoneWidget

上一节中我们通过 CustomCheckbox 演示了如何通过自定义 RenderObject 的方式来进行UI绘制、动画调度和事件处理。本节再通过一个实例来巩固换一下。

本节的我们将实现一个 DoneWidget,它可以在创建时执行一个打勾动画,效果如下:

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}

上面代码很简单,但需要注意三点:

  1. 我们对动画应用了easeIn 曲线,可以看到如果在 RenderObject 中对动画应用曲线,另外读者应该也能发现,曲线的本质就是对动画的进度加了一层映射,通过不同的映射规则就可以控制动画在不同阶段的快慢。
  2. 我们重写了 RenderObjectAnimationMixin 中的 duration,该参数用于指定动画时长。
  3. adjustProgress 的作用主要是将“打勾”动画氛围两部分,第一部分是第一个点和第二个点的连线动画,这部分动画站总动画时长的 前 60%; 第二部分是第二点和第三个点的连线动画,该部分动画占总时长的后 40%。