Flutter CustomPaint自定义控件 时钟

时钟效果
时钟效果

我们将用 CustomPaint 来绘制我们的时钟。CustomPaint 的重要参数 paintersizepainter 是一个继承了CustomPainter 的对象,主要实现了绘画控件的功能。size 指定了控件的大小,如果 CustomPaintchild 不为空,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,实现了其中两个重要方法:paintshouldRepaintpaint 当自定义控件需要重画时被调用。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.toDouble() * i); 以当前画布起始点旋转一个角度,这是为了保证每个数字在下面旋转到对应的位置后保持竖直显示。
canvas.restore() 重置画布,即把画布的起始点定位到控件的中心位置。
canvas.rotate(angle.toDouble()) 以控件中心为原点旋转一个角度,即把数字旋转到对应的位置。数字 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 度,同样利用三角函数算出各自对应的两点,再画直线就可以得到分针,秒针。

完整的代码如下:

import 'dart:async';
import 'dart:math';
import 'dart:ui';

import 'package:flutter/material.dart';

class ClockPage extends StatefulWidget {
  final double radius;
  final Color hourHandColor;
  final Color minuteHandColor;
  final Color secondHandColor;
  final Color numberColor;
  final Color borderColor;

  const ClockPage(
      {Key? key,
      required this.hourHandColor,
      required this.minuteHandColor,
      required this.secondHandColor,
      required this.numberColor,
      required this.borderColor,
      this.radius = 150.0})
      : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return ClockPageState();
  }
}

class ClockPageState extends State<ClockPage> {
  late DateTime datetime;
  late Timer timer;

  @override
  void initState() {
    super.initState();
    datetime = DateTime.now();
    timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        datetime = DateTime.now();
      });
    });
  }

  @override
  void dispose() {
    super.dispose();
    timer.cancel();
  }

  @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),
    );
  }
}

class ClockPainter extends CustomPainter {
  final Color handColor;
  final Color numberColor;
  final Color borderColor;
  final double radius;
  List<Offset> secondsOffset = [];
  final DateTime datetime;
  late TextPainter textPainter;
  late num angle;
  late double borderWidth;

  ClockPainter(this.datetime,
      {this.radius = 150.0,
      this.handColor = Colors.black,
      this.numberColor = Colors.black,
      this.borderColor = Colors.black}) {
    borderWidth = radius / 14;
    final secondDistance = radius - borderWidth * 2;
    //init seconds offset
    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);
    }

    textPainter = TextPainter(
      textAlign: TextAlign.center,
      textDirection: TextDirection.rtl,
    );
    angle = degToRad(360 / 60);
  }

  @override
  void paint(Canvas canvas, Size size) {
    final scale = radius / 150;

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

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

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

      List<Offset> bigger = [];
      for (var i = 0; i < secondsOffset.length; i++) {
        if (i % 5 == 0) {
          bigger.add(secondsOffset[i]);

          //draw number
          canvas.save();
          canvas.translate(0.0, -radius + borderWidth * 4);
          textPainter.text = 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.toDouble() * i);

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

      final biggerPaint = Paint()
        ..strokeWidth = 5 * scale
        ..color = numberColor;
      canvas.drawPoints(PointMode.points, bigger, biggerPaint);
    }

    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);
  }

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

num degToRad(num deg) => deg * (pi / 180.0);

num radToDeg(num rad) => rad * (180.0 / pi);

使用的代码如下:

const ClockPage(
  hourHandColor: Colors.black,
  minuteHandColor: Colors.black,
  secondHandColor: Colors.black,
  numberColor: Colors.black,
  borderColor: Colors.black),

参考链接


Flutter CustomPaint自定义控件 时钟

发布者

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注