Flutter 绘制集录 | 秒表盘的绘制

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 5 天,点击查看活动详情


前言

本文的目的在于绘制练习,将被收录在 FlutterUnit 的绘制集录当中。另外在 《Flutter 语法基础 - 梦始之地》 中有一章需要使用这个表盘,但并不想涉及过多的绘制知识,故而在此进行实现。效果如下,外圈是线条围成的表盘,内部有个小圆指示当前位置,中间显示信息文字。


1. 需求分析

这里绘制的是 秒表表盘,一圈是 1 分种 ,每秒有对应 3 格,也就是说一共有 180 格,每格间的夹角是 。下面来看一下绘制过程中需要的参数,首先需要一个 Duration 对象,表示当前秒表的时间。另外,根据时间可以计算出小圆的角度。

绘制时可以配置的参数,比如半径、刻度颜色、文字颜色、样式等。另外刻度的长短、粗细、小圆半径等更细致的参数可以根据半径进行计算得出。


2. 刻度绘制

如下所示正方形是绘制区域,左侧刻度宽为 scaleLineWidth,通过矩形区域的宽度和 _kScaleWidthRate 比例来确定。

const double _kScaleWidthRate = 0.4/10;
final double scaleLineWidth = size.width*_kScaleWidthRate;
复制代码

绘制刻度时使用直线,只要确定左右两点坐标即可。如下 tag1 处将坐标原点移至区域中心,很容易可以计算出刻度的坐标,如下 tag2 所示。

final Paint scalePainter = Paint();
@override
void paint(Canvas canvas, Size size) {
  canvas.translate(size.width/2, size.height/2); // tag1
  scalePainter..color = Colors.red..style=PaintingStyle.stroke;
  final double scaleLineWidth = size.width*_kScaleWidthRate;
  canvas.drawLine(
    Offset(size.width/2, 0), // tag2
    Offset(size.width/2 - scaleLineWidth, 0), 
    scalePainter
  );
}
复制代码

下面来绘制刻度盘,之前分析了一共 180 个刻度,可以通过遍历和旋转进行绘制。如下,遍历绘制 180 次上面的条刻度,每次绘制完成后画布旋转 ,这样绘制 180 次之后,画布会转 360° 回到原本位置。界面上绘制内容显示如下:

for(int i = 0; i < 180 ; i++){
  canvas.drawLine(
    Offset(size.width/2, 0), 
    Offset(size.width/2 - scaleLineWidth, 0), 
    scalePainter
  );
  canvas.rotate(pi/180*2);
}
复制代码

3.文字绘制

可以看出这里的文字有两种样式,毫秒数颜色跟随主题色。在 Canvas 文字绘制时可以通过 TextPainter 对象完成。使用该对象必须指定 textDirection ,表示文字的排布方向。

TextPainter textPainter = TextPainter(
  textAlign: TextAlign.center,
  textDirection: TextDirection.ltr,
);
复制代码

textPainter 绘制内容通过 text 成员指定,该成员类型是 InlineSpan 。所以可以展示富文本,如下代码展示 commonStylehighlightStyle 两种样式的文字。

void drawText(Canvas canvas){
  textPainter.text = TextSpan(
      text: '00:04',
      style: commonStyle,
      children: [TextSpan(text: ".65", style: highlightStyle)]);
  textPainter.layout(); // 进行布局
  final double width = textPainter.size.width;
  final double height = textPainter.size.height;
  textPainter.paint(canvas, Offset(-width / 2, -height / 2));
}
复制代码

然后,可以根据一个 duration 对象来得到需要展示的文字:

final Duration duration = Duration(minutes: 0, seconds: 4, milliseconds: 650);

int minus = duration.inMinutes % 60;
int second = duration.inSeconds % 60;
int milliseconds = duration.inMilliseconds % 1000;
String commonStr = '${minus.toString().padLeft(2, "0")}:${second.toString().padLeft(2, "0")}';
String highlightStr = ".${(milliseconds ~/ 10).toString().padLeft(2, "0")}";
复制代码

4.绘制指示器

圆的指示器的半径也是根据大圆半径计算的,然后根据时长计算出偏转角度即可。

final double scaleLineWidth = size.width * _kScaleWidthRate;
final double indicatorRadius = size.width * _kIndicatorRadiusRate;
canvas.drawCircle(
    Offset(0,-size.width/2+scaleLineWidth+indicatorRadius,),
    indicatorRadius/2, 
    indicatorPainter
);
复制代码

下面来算个简单的数学题,已知当前时长,如何求得该时长在表盘的旋转角度?
只要算出当前分钟内毫秒数一分钟毫秒数(60 * 1000) 占比即可。在绘制指示器时,将画布进行旋转 radians 弧度,不过要注意,为了避免这个旋转变换对其他绘制的影响,需要通过 saverestore 方法进行处理。

int second = duration.inSeconds % 60;
int milliseconds = duration.inMilliseconds % 1000;
double radians = (second * 1000 + milliseconds) / (60 * 1000) * 2 * pi;

canvas.save();
canvas.rotate(radians);
// 绘制...
canvas.restore();
复制代码

这样,给出一个 Duration 对象,就能线数处正确的文字及指示器位置:


5. 组件的封装

组件的封装是为了更简洁的使用,如下通过为 StopWatchWidget 组件提供配置即可呈现出对应的绘制效果。就可以将绘制的细节封装起来使用者不需要了解具体是怎么画出来的,只要用 StopWatchWidget 组件即可。

StopWatchWidget(
  duration: Duration(minutes: 0, seconds: 24, milliseconds: 850),
  radius: 100,
),
复制代码

如下在 StopWatchPainter 中封装了4 个可配置的参数,在 shouldRepaint 方法中,当这四者其中之一发生变化时都允许进行重绘。

class StopWatchPainter extends CustomPainter {
  final Duration duration; // 时长
  final Color themeColor; // 主题色
  final Color scaleColor; // 刻度色
  final TextStyle textStyle; // 文本样式
  StopWatchPainter({
    required this.duration,
    required this.themeColor,
    required this.scaleColor,
    required this.textStyle,
  })
    
  // 绘制略...
 
  @override
  bool shouldRepaint(covariant StopWatchPainter oldDelegate) {
    return oldDelegate.duration != duration ||
        oldDelegate.textStyle != textStyle ||
        oldDelegate.themeColor != themeColor||
        oldDelegate.scaleColor != scaleColor;
  }
}
复制代码

StopWatchWidget 继承自 StatelessWidget ,本身并不承担状态变化的能力。也就是说,它的呈现内容只和使用者传入的配置信息有关,并不会主动改变呈现效果。通过 CustomPaint 组件来显示绘制的画板 StopWatchPainter

另外注意一些小细节,组件画板的构造参数比较相似,但组件是和使用者直接接触的,要考虑到它的易用性,有必要提供一些默认的参数,或根据当前主题来获取某些信息。而画板对象是 创造者 负责创建的,两者面对的角色并不相同,在封装时的考量也有所差异。

class StopWatchWidget extends StatelessWidget {
  final double radius;
  final Duration duration;
  final Color? themeColor;
  final TextStyle? textStyle;
  final Color scaleColor;

  const StopWatchWidget({
    Key? key,
    required this.radius,
    required this.duration,
    this.scaleColor = const Color(0xffDADADA),
    this.textStyle,
    this.themeColor
  }) : super(key: key);

  TextStyle get commonStyle => TextStyle(
    fontSize: radius/3,
    fontWeight: FontWeight.w200,
    color: const Color(0xff343434),
  );

  @override
  Widget build(BuildContext context) {
    TextStyle style = textStyle??commonStyle;
    Color themeColor = this.themeColor??Theme.of(context).primaryColor;
    return CustomPaint(
      painter: StopWatchPainter(
          duration: duration,
          themeColor: themeColor,
          scaleColor: scaleColor,
          textStyle: style),
      size: Size(radius * 2, radius * 2),
    );
  }
}
复制代码

那本文就到这里,下一篇将基于这个绘制组件,实现秒表盘的启动、暂停的功能。谢谢观看 ~

猜你喜欢

转载自juejin.im/post/7137081179315896350
今日推荐