Flutter 绘制集录 | 画个表 (上)
一、本作介绍和准备
1. 效果图
如下图,通过 Flutter 的 Canvas 绘制如下的 静态
表面。

本文知识点
【1】. Flutter 中绘制旋转型的元素
【2】. Flutter 中弧线的方式
【3】. Flutter 中绘制文字的方式
【4】. Canvas.save 和 Canvas.restore 的使同。
2.资源准备
绘制文字时,可以指定文字的字体,如下在 assets/fonts
中放入对应字体。

在 pubspec.yaml
中配置后即可使用。

3.程序入口
程序入口如下,通过 ClockWidget
组件来绘制表盘。本篇 ClockWidget
只是进行静态效果展现,只需继承 StatelessWidget
即可。在 300*300
的区域内通过 ClockPainter
进行绘制。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: Center(child: ClockWidget()),
));
}
}
class ClockWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(300, 300),
painter: ClockPainter(),
);
}
}
一、表盘外围绘制
1.画板的定义
这个绘制中的旋转操作很多,为了方便处理,可以将画布的中心移到画板区域的中心。这样绘制时原点就会在中心。

class ClockPainter extends CustomPainter {
Paint _paint = Paint();
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
drawHelp(canvas,size);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
void drawHelp(Canvas canvas,Size size){
canvas.drawPoints(PointMode.lines, [
Offset( -size.width/2,0),Offset( size.width/2,0),
Offset( 0,-size.height/2),Offset(0,size.height/2),
], Paint());
}
}
2.绘制外圈
首先分析一下外圈的绘制,这里只要运用 绘制圆弧
和 画布旋转
。如下图,左侧是一个弧,然后通过三次旋转画布,再绘制即可的得到右图。代码如下,在每次绘制完后,将画布旋转 90°
,由于涉及局部的旋转操作,需要保存之前的画布状态,局部变换完后,恢复到之前的状态。(其实这里恰好转了360°,已复原,保不保存无所谓,但如果非 360°,如果不保存和恢复,变换将会影响之后的绘制)
四分之一 | 旋转绘制 |
---|---|
![]() |
![]() |
void drawOuterCircle(Canvas canvas, Size size) {
final offsetAngle = 5; // 圆弧顶点和轴的夹角
_paint ..strokeWidth = 4
..color = Color(0xffD5D5D5)
..style = PaintingStyle.stroke;
canvas.save();
for (int i = 0; i < 4; i++) {
canvas.drawArc(
Rect.fromPoints(Offset(-size.width / 2, -size.height / 2),
Offset(size.width / 2, size.height / 2)),
offsetAngle * pi / 180,
pi / 2 - 2 * offsetAngle * pi / 180,
false, _paint);
canvas.rotate(pi / 2);
}
canvas.restore();
}
3.外圈格点的绘制
如下 drawDot
方法,count 变量表示格点的个数,每次遍历之后,通过 canvas.rotate(perAngle)
进行画布旋转。判断 i % 5 == 0
时绘制较粗的格线,其他的是普通格线。这就是在遍历时根据情况进行绘制的方式,在一些刻度类型的绘制中很常用。

void drawDot(Canvas canvas) {
canvas.save();
_paint
..strokeCap = StrokeCap.round
..style = PaintingStyle.fill;
double count = 60;
double perAngle = 2 * pi / count;
for (int i = 0; i < count; i++) {
if (i % 5 == 0) {
_paint
..strokeWidth = 3
..color = Colors.blue;
canvas.drawLine(Offset(120, 0), Offset(135, 0), _paint);
canvas.drawCircle(Offset(115, 0), 2, _paint..color = Colors.orange);
} else {
_paint
..strokeWidth = 1.5
..color = Colors.black;
canvas.drawLine(Offset(125, 0), Offset(135, 0), _paint);
}
canvas.rotate(perAngle);
}
canvas.restore();
}
4.绘制文字
文字通过 TextPainter
对象进行绘制,如下 _drawCircleText
方法绘制圆框中的四个文字。 _drawLogoText
绘制 CHOPS
字体的文字。这样,主要的外框就绘制完毕。

final TextPainter _textPainter = TextPainter(
textAlign: TextAlign.center, textDirection: TextDirection.ltr);
void drawText(Canvas canvas) {
_drawCircleText(canvas, 'Ⅸ', offsetX: -150);
_drawCircleText(canvas, 'Ⅲ', offsetX: 150);
_drawCircleText(canvas, 'Ⅵ', offsetY: 150);
_drawCircleText(canvas, 'Ⅻ', offsetY: -150);
_drawLogoText(canvas, offsetY: -80);
}
_drawCircleText(Canvas canvas, String text,
{double offsetX = 0, double offsetY = 0}) {
_textPainter.text = TextSpan(
text: text, style: TextStyle(fontSize: 20, color: Colors.blue));
_textPainter.layout();
_textPainter.paint(
canvas,
Offset.zero.translate(-_textPainter.size.width / 2 + offsetX,
-_textPainter.height / 2 + offsetY));
}
_drawLogoText(Canvas canvas, {double offsetX = 0, double offsetY = 0}) {
_textPainter.text = TextSpan(
text: 'Toly',
style:
TextStyle(fontSize: 30, color: Colors.blue, fontFamily: 'CHOPS'));
_textPainter.layout();
_textPainter.paint(
canvas,
Offset.zero.translate(-_textPainter.size.width / 2 + offsetX,
-_textPainter.height / 2 + offsetY));
}
三、表盘指针绘制
1.时针绘制
如下,通过 drawH(canvas, 120);
绘制120° 偏转的时针。注意,此角度是与横轴正向的夹角。

void drawH(Canvas canvas, double deg) {
canvas.save();
canvas.rotate(deg / 180 * pi);
_paint
..strokeWidth = 3
..color = Color(0xff8FC552)
..strokeCap = StrokeCap.round;
canvas.drawLine(Offset.zero, Offset(60, 0), _paint);
canvas.restore();
}
2.分针绘制
同理,如下,通过 drawM(canvas, 0);
绘制 0° 偏转的分针。

void drawM(Canvas canvas, double deg) {
canvas.save();
canvas.rotate(deg / 180 * pi);
_paint
..strokeWidth = 2
..color = Color(0xff87B953)
..strokeCap = StrokeCap.round;
canvas.drawLine(Offset.zero, Offset(80, 0, ), _paint);
canvas.restore();
}
3.秒针绘制
如下,通过 drawS(canvas, 90);
绘制 90° 偏转的秒针。

void drawS(Canvas canvas, double deg) {
_paint..strokeWidth = 2.5
..color = Color(0xff6B6B6B)
..strokeCap = StrokeCap.square
..style = PaintingStyle.stroke;
Path path = Path();
canvas.save();
canvas.rotate(deg / 180 * pi);
canvas.save();
canvas.rotate((360 - 270) / 2 / 180 * pi);
path.addArc(Rect.fromPoints(Offset(-9, -9), Offset(9, 9)), 0, 270 / 180 * pi);
canvas.drawPath(path, _paint);
canvas.restore();
_paint..strokeCap = StrokeCap.round;
canvas.drawLine(Offset(-9, 0), Offset(-20, 0), _paint);
_paint..strokeWidth = 1..color = Colors.black;
canvas.drawLine(Offset(0, 0), Offset(100, 0), _paint);
_paint..strokeWidth = 3..color = Color(0xff6B6B6B);
canvas.drawCircle(Offset.zero, 5, _paint);
_paint..color = Color(0xff8FC552)..style = PaintingStyle.fill;
canvas.drawCircle(Offset.zero, 4, _paint);
canvas.restore();
}
这里指针指示进行了简单的绘制,你可以在对应的方法中进行自己的处理。这样通过角度的设置就可以将指针进行变动,加上定时器,或者动画就可以形成动态的表面。这些处理我们在下篇进行讲述,谢谢观看 ~