Flutter CustomPaint自定义控件 时钟

时钟效果

我们将用CustomPaint来绘制我们的时钟。CustomPaint的重要参数painter和size。painter是一个继承了CustomPainter的对象,主要实现了绘画控件的功能。size指定了控件的大小,如果CustomPaint的child不为空,size的值就是child控件的大小,指定size的值则无效;如果child为空,所指定的size的值,就是画布的大小。

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: ClockPainter(datetime,
          numberColor: Colors.black,
          handColor: Colors.black,
          borderColor: Colors.black,
          radius: widget.radius),
      size: Size(widget.radius * 2, widget.radius * 2),
    );
  }

ClockPainter继承了CustomPainter,实现了其中两个重要方法:paint和shouldRepaint。paint当自定义控件需要重画时被调用。shouldRepaint则决定当条件变化时是否需要重画。

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

  @override
  bool shouldRepaint(ClockPainter oldDelegate) {
    return true;
  }
}

首先先时钟的边框,代码如下,只需drawCircle就可以实现边框的绘制。

    //draw border
    final borderPaint = Paint()
      ..color = borderColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = borderWidth;
    canvas.drawCircle(
        Offset(radius, radius), radius - borderWidth / 2, borderPaint);
时钟边框

canvas的起始点是画布的左上角坐标点为起点,即x,y都为零。drawCircle画一个指定了半径的圆。圆的形状和样式由Paint对象指定。style决定了是画圆盘(PaintingStyle.fill)还是圆环(PaintingStyle.stroke)。当style设定为PaintingStyle.stroke时,strokeWidth就是指定了圆环的宽度。

其次,可以画时钟刻度和数字了。

刻度
 List<Offset> secondsOffset = [];
   
    for (var i = 0; i < 60; i++) {
      Offset offset = Offset(
          cos(degToRad(6 * i - 90)) * secondDistance + radius,
          sin(degToRad(6 * i - 90)) * secondDistance + radius);
      secondsOffset.add(offset);
    }

    //draw second point
    final secondPPaint = Paint()
      ..strokeWidth = 2 * scale
      ..color = numberColor;
    if (secondsOffset.length > 0) {
      canvas.drawPoints(PointMode.points, secondsOffset, secondPPaint);
    }

时钟的刻度占整个圆弧的360/60=6度,运用数学公式每个刻度点的坐标,然后用drawPoints画出每一个刻度点。


数字
      canvas.save();
      canvas.translate(radius, radius);

      for (var i = 0; i < secondsOffset.length; i++) {
        if (i % 5 == 0) {
          //draw number
          canvas.save();
          canvas.translate(0.0, -radius + borderWidth * 4);
          textPainter.text = new TextSpan(
            text: "${(i ~/ 5) == 0 ? "12" : (i ~/ 5)}",
            style: TextStyle(
              color: numberColor,
              fontFamily: 'Times New Roman',
              fontSize: 28.0 * scale,
            ),
          );

          //helps make the text painted vertically
          canvas.rotate(-angle * i);

          textPainter.layout();
          textPainter.paint(canvas,
              new Offset(-(textPainter.width / 2), -(textPainter.height / 2)));
          canvas.restore();
        }
        canvas.rotate(angle);
      }
      canvas.restore();

canvas.save()保存当前画布,以便画完数字恢复。
canvas.translate(radius, radius)把画布的起始点移到画布的中心。
再次保存画布后,再把起始点移到正上方位置,这里是把起始点数字12的位置。
TextPainter用来画文字。
canvas.rotate(-angle * i);以当前画布起始点旋转一个角度,这是为了保证每个数字在下面旋转到对应的位置后保持竖直显示。
canvas.restore()重置画布,即把画布的起始点定位到控件的中心位置。
canvas.rotate(angle)以控件中心为原点旋转一个角度,即把数字旋转到对应的位置。数字12旋转角度为零,数字1旋转角度为30度。
所有的数字都画完并旋转到对应的位置后即可恢复起始点到控件的左上角。

最后,我们接着画时针,分针,秒针。


完整时钟
    final hour = datetime.hour;
    final minute = datetime.minute;
    final second = datetime.second;

    // draw hour hand
    Offset hourHand1 = Offset(
        radius - cos(degToRad(360 / 12 * hour - 90)) * (radius * 0.2),
        radius - sin(degToRad(360 / 12 * hour - 90)) * (radius * 0.2));
    Offset hourHand2 = Offset(
        radius + cos(degToRad(360 / 12 * hour - 90)) * (radius * 0.5),
        radius + sin(degToRad(360 / 12 * hour - 90)) * (radius * 0.5));
    final hourPaint = Paint()
      ..color = handColor
      ..strokeWidth = 8 * scale;
    canvas.drawLine(hourHand1, hourHand2, hourPaint);

    // draw minute hand
    Offset minuteHand1 = Offset(
        radius - cos(degToRad(360 / 60 * minute - 90)) * (radius * 0.3),
        radius - sin(degToRad(360 / 60 * minute - 90)) * (radius * 0.3));
    Offset minuteHand2 = Offset(
        radius +
            cos(degToRad(360 / 60 * minute - 90)) * (radius - borderWidth * 3),
        radius +
            sin(degToRad(360 / 60 * minute - 90)) * (radius - borderWidth * 3));
    final minutePaint = Paint()
      ..color = handColor
      ..strokeWidth = 3 * scale;
    canvas.drawLine(minuteHand1, minuteHand2, minutePaint);

    // draw second hand
    Offset secondHand1 = Offset(
        radius - cos(degToRad(360 / 60 * second - 90)) * (radius * 0.3),
        radius - sin(degToRad(360 / 60 * second - 90)) * (radius * 0.3));
    Offset secondHand2 = Offset(
        radius +
            cos(degToRad(360 / 60 * second - 90)) * (radius - borderWidth * 3),
        radius +
            sin(degToRad(360 / 60 * second - 90)) * (radius - borderWidth * 3));
    final secondPaint = Paint()
      ..color = handColor
      ..strokeWidth = 1 * scale;
    canvas.drawLine(secondHand1, secondHand2, secondPaint);

    final centerPaint = Paint()
      ..strokeWidth = 2 * scale
      ..style = PaintingStyle.stroke
      ..color = Colors.yellow;
    canvas.drawCircle(Offset(radius, radius), 4 * scale, centerPaint);

一个小时占时钟角度为30度,利用三角函数算出时针的两个点,用drawLine画出带宽度的时针。
每一分钟每一秒中所占的角度为6度,同样利用三角函数算出各自对应的两点,再画直线就可以得到分针,秒针。

这样一个简单的时钟控件就完成了。github地址

推荐阅读更多精彩内容