Flutter:如何用CustomPainter绘制任务完成环

84 阅读3分钟

当内置的Flutter小部件不够用时。 CustomPainter来拯救我们!

所以,让我们看看如何用它来绘制这个任务完成环的动画的UI。

任务完成环的动画

这个动画序列是由多个阶段组成的。

  1. 显示未完成的任务的用户界面
  2. 当我们点击并按住任务时,逐渐填充环形图
  3. 显示一个用于确认的复选标记图标
  4. 显示已完成任务的用户界面

特别是,这种 "部分完成 "的环形UI并不是我们可以用Flutter SDK中的内置小部件来创建的。

关键点

任务完成环可以表示为一个StatelessWidget 子类,有一个AspectRatio 和一个CustomPaint 子类。

class TaskCompletionRing extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      // Use 1.0 to ensure that the custom painter
      // will draw inside a container with width == height
      aspectRatio: 1.0,
      child: CustomPaint(
        painter: RingPainter(),
      ),
    );
  }
}

然后我们可以添加一个CustomPainter 子类,并重写paint()shouldRepaint() 方法。

class RingPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {}

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

paint() 方法有两个参数。

  • 一个canvas 对象,我们可以用它来画东西
  • 一个size 对象,告诉我们绘图区域有多大。

shouldRepaint() 方法应该在有变化时返回true (我们稍后会回到这个问题)。

实现RingPainter

这就像这样实现了。

class RingPainter extends CustomPainter {
  // 1. add a constructor and properties that can be set from the parent widget
  RingPainter({
    required this.progress,
    required this.taskNotCompletedColor,
    required this.taskCompletedColor,
  });
  // a value between 0 and 1
  final double progress;
  // background color to use when the task is not completed
  final Color taskNotCompletedColor;
  // foreground color to use when the task is completed
  final Color taskCompletedColor;

  @override
  void paint(Canvas canvas, Size size) {
    // 2. configure the paint and drawing properties
    final strokeWidth = size.width / 15.0;
    final center = Offset(size.width / 2, size.height / 2);
    final radius = (size.width - strokeWidth) / 2;

    // 3. create and configure the background paint
    final backgroundPaint = Paint()
      ..isAntiAlias = true
      ..strokeWidth = strokeWidth
      ..color = taskNotCompletedColor
      ..style = PaintingStyle.stroke;
    // 4. draw a circle
    canvas.drawCircle(center, radius, backgroundPaint);

    // 5. create and configure the foreground paint
    final foregroundPaint = Paint()
      ..isAntiAlias = true
      ..strokeWidth = strokeWidth
      ..color = taskCompletedColor
      ..style = PaintingStyle.stroke;
    // 6. draw an arc that starts from the top (-pi / 2)
    // and sweeps and angle of (2 * pi * progress)
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -pi / 2,
      2 * pi * progress,
      false,
      foregroundPaint,
    );
  }

  // 7. only return true if the old progress value
  // is different from the new one
  @override
  bool shouldRepaint(covariant RingPainter oldDelegate) =>
      oldDelegate.progress != progress;
}

注意在重写 时使用的 covariant在覆盖shouldRepaint() 方法时使用了关键字。这是为了让oldDelegate ,即使它在基类中被声明为CustomPainter ,也可以用类型RingPainter

关键点

  • 当任何现有的Flutter小部件不足以满足我们的目的时,我们可以实现一个CustomPainter
  • 在扩展CustomPainter 类时,我们必须实现paint()shouldRepaint() 方法。
  • paint() 方法给了我们一个canvas ,我们可以用它来绘制形状。所有可用的绘制方法都需要一个Paint 对象,我们可以用它来定制形状的外观。
  • 如果我们想绘制相对于父部件大小的形状,我们可以创建取决于size 参数的变量。
  • 我们可以通过传递一些值作为参数来使我们的绘图器变得可定制,就像我们在创建自定义widget时那样。
  • 我们可以以这样的方式实现shouldRepaint() 方法,即它只在有变化时返回true ,这有助于提高性能。

奖励:这里有一个关于如何使用CustomPainter 的完整教程。

在下一课,我们将看到如何使用AnimationController 实现任务动画。

编码愉快!