theme: cyanosis
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 5 天,点击查看活动详情
前言
本文的目的在于绘制练习,将被收录在 FlutterUnit 的绘制集录当中。另外在 《Flutter 语法基础 - 梦始之地》 中有一章需要使用这个表盘,但并不想涉及过多的绘制知识,故而在此进行实现。效果如下,外圈是线条围成的表盘,内部有个小圆指示当前位置,中间显示信息文字。
1. 需求分析
这里绘制的是 秒表表盘
,一圈是 1 分种
,每秒有对应 3
格,也就是说一共有 180
格,每格间的夹角是 2°
。下面来看一下绘制过程中需要的参数,首先需要一个 Duration
对象,表示当前秒表的时间。另外,根据时间可以计算出小圆的角度。
绘制时可以配置的参数,比如半径、刻度颜色、文字颜色、样式等。另外刻度的长短、粗细、小圆半径等更细致的参数可以根据半径进行计算得出。
2. 刻度绘制
如下所示正方形是绘制区域,左侧刻度宽为 scaleLineWidth
,通过矩形区域的宽度和 _kScaleWidthRate
比例来确定。
dart const double _kScaleWidthRate = 0.4/10; final double scaleLineWidth = size.width*_kScaleWidthRate;
绘制刻度时使用直线,只要确定左右两点坐标即可。如下 tag1
处将坐标原点移至区域中心,很容易可以计算出刻度的坐标,如下 tag2
所示。
dart 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
次上面的条刻度,每次绘制完成后画布旋转 2°
,这样绘制 180
次之后,画布会转 360°
回到原本位置。界面上绘制内容显示如下:
dart 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
,表示文字的排布方向。
dart TextPainter textPainter = TextPainter( textAlign: TextAlign.center, textDirection: TextDirection.ltr, );
textPainter
绘制内容通过 text
成员指定,该成员类型是 InlineSpan
。所以可以展示富文本,如下代码展示 commonStyle
和 highlightStyle
两种样式的文字。
dart 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
对象来得到需要展示的文字:
```dart 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.绘制指示器
圆的指示器的半径也是根据大圆半径计算的,然后根据时长计算出偏转角度即可。
dart 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
弧度,不过要注意,为了避免这个旋转变换对其他绘制的影响,需要通过 save
和 restore
方法进行处理。
```dart 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
组件即可。
dart StopWatchWidget( duration: Duration(minutes: 0, seconds: 24, milliseconds: 850), radius: 100, ),
如下在 StopWatchPainter
中封装了4
个可配置的参数,在 shouldRepaint
方法中,当这四者其中之一发生变化时都允许进行重绘。
```dart 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
。
另外注意一些小细节,组件
和 画板
的构造参数比较相似,但组件是和使用者
直接接触的,要考虑到它的易用性,有必要提供一些默认的参数,或根据当前主题来获取某些信息。而画板对象是 创造者
负责创建的,两者面对的角色并不相同,在封装时的考量也有所差异。
```dart 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), ); } } ```
那本文就到这里,下一篇将基于这个绘制组件,实现秒表盘的启动、暂停的功能。谢谢观看 ~