
移动小样
V1
2022/08/28阅读:46主题:默认主题
Flutter-自定义之钟表
效果图

绘制控件(四大角色)
-
CustomPaint -
CustomPainter -
Canvas -
Paint
CustomPaint
英文翻译:自定义绘制;CustomPaint可以实现很多酷炫的动画和效果。CustomPaint 提供了让用户自定义widget的能力,它暴露了一个canvas,可以通过这个canvas来绘制widget,CustomPaint会先调用painter绘制背景,然后再绘制child,最后调用foregroundPainter来绘制前景,CustomPaint的定义如下
CustomPaint({Key key,
CustomPainter painter,
CustomPainter foregroundPainter,
Size size: Size.zero,
bool isComplex: false,
bool willChange: false,
Widget child })
-
painter:负责绘制背景的painter -
foregroundPainter : 负责绘制前景的painter -
size : 控件大小 -
isComplex : 是否复杂绘制,需要用cache来提高绘制效率 -
willChange : 和isComplex配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变 -
child : 子widget
CustomPainter
CustomPaint的绘制过程都将会交给CustomPainter来完成,CustomPainter是个抽象接口,在子类继承CustomPainter的时候必须要重写它的paint 跟 shouldRepaint接口。
class MyCustomPainter extends CustomPainter {
//绘制自定义的效果
@override void paint(Canvas canvas, Size size) {
}
//判断是否需要重新绘制ui 通常在当前实例和旧实例属性不一致时返回true。
@override bool shouldRepaint(MyCustomPainter oldDelegate) {
return this != oldDelegate;
}
}
-
paint : 每当CustomPaint需要重绘的时候都会调用此接口 -
shouldRepaint : 当CustomPaint被重新设置了一个新的painter后会回调此方法,CustomPaint会根据shouldRepaint的返回值来判断是否需要重新绘制ui,譬如新的painter跟旧的painter绘制的内容不一样时,此时shouldRepaint需要返回true来通知CustomPaint重新绘制。
Canvas
-
Canvas:画布,真正的绘制是由canvas跟paint来完成的,画布提供了各种绘制的接口来绘制图形。 常用的绘制接口有更多请查看官方文档
小知识点:
-
canvas.save(); 画布将当前的状态保存 -
canvas.restore(); 画布取出原来所保存的状态
Paint
-
Paint:画笔,是用来在画布上面绘制图形。画笔属性:颜色、线宽、绘制模式、抗锯齿等等。
常用属性有:
-
isAntiAlias : 设置画笔是否扛锯齿 -
color : 颜色 -
strokeWidth : 设置画笔画线宽度 -
style :绘制模式,画线或充满
以上简单介绍了Flutter绘制API,下面开始真正来实现钟表的效果。
实现开始
四大步骤
-
1、效果功能分析。 -
2、功能拆解。 -
3、功能参数。 -
4、功能代码实现。
1、效果功能分析
钟表(参考自己家的钟表)
2、功能拆解
-
绘制边框API:
void drawCircle(Offset c, double radius, Paint paint)
-
绘制刻度:
void drawPoints(PointMode pointMode, List<Offset> points, Paint paint)
-
绘制数字:
TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
text: TextSpan(),
) ..layout()
..paint(canvas, offset);
-
绘制时针:
void drawLine(Offset p1, Offset p2, Paint paint)
-
绘制分针:
void drawLine(Offset p1, Offset p2, Paint paint)
-
绘制秒针:
void drawLine(Offset p1, Offset p2, Paint paint)
-
绘制中间圆圈:
void drawCircle(Offset c, double radius, Paint paint)
-
绘制移动小球:
void drawCircle(Offset c, double radius, Paint paint)
-
移动指针:
Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
dateTime = DateTime.now();
});
});
3、功能参数
-
钟表的半径 -
边框的颜色 -
刻度的颜色 -
数字的颜色 -
时针的颜色 -
分针的颜色 -
秒针的颜色 -
走秒小球颜色 -
中间圆颜色
4、功能代码实现
从功能拆解可以看出,所有的功能未知数就是位置的计算,我们可以先计算下点的坐标。
如图:
我们一起来回顾下上学时期的数学几何知识吧!
-
先求A点
-
再求B点
-
再求C点
同上计算出也是:
C点坐标(R + L*sinα,R - L*cosα)
所以说在园中,随便一个点的坐标都是:
(R + Lsinα,R - Lcosα)
上面计算过程中使用了之前学习的诱导公式,这里列举出来:
诱导公式

到这里基本需要计算的知识点已经完毕了,剩下就是对API的使用,因为代码不多,这里直接把完整的代码贴出来,注释很详细。
完整代码
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
///时钟
class ClockView extends StatefulWidget {
const ClockView({
Key key,
this.radius = 150,
this.borderColor = Colors.black,
this.scaleColor = Colors.black,
this.numberColor = Colors.black,
this.moveBallColor = Colors.red,
this.hourHandColor = Colors.black,
this.minuteHandColor = Colors.black,
this.secondHandColor = Colors.red,
this.middleCircleColor = Colors.red,
}) : super(key: key);
//钟表的半径
final double radius;
//边框的颜色
final Color borderColor;
//刻度的颜色
final Color scaleColor;
//数字的颜色
final Color numberColor;
//走秒小球颜色
final Color moveBallColor;
//时针的颜色
final Color hourHandColor;
//分针的颜色
final Color minuteHandColor;
//秒针的颜色
final Color secondHandColor;
//中间圆颜色
final Color middleCircleColor;
@override
State<StatefulWidget> createState() {
return ClockViewState();
}
}
class ClockViewState extends State<ClockView> {
//当前时间
DateTime dateTime;
//定时器
Timer timer;
@override
void initState() {
super.initState();
dateTime = DateTime.now();
timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
dateTime = DateTime.now();
});
});
}
@override
void dispose() {
//取消定时器
if (timer.isActive) {
timer?.cancel();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: ClockPainter(
dateTime,
radius: widget.radius,
borderColor: this.widget.borderColor,
scaleColor: this.widget.scaleColor,
numberColor: this.widget.numberColor,
moveBallColor: this.widget.moveBallColor,
hourHandColor: this.widget.hourHandColor,
minuteHandColor: this.widget.minuteHandColor,
secondHandColor: this.widget.secondHandColor,
middleCircleColor: this.widget.middleCircleColor,
),
size: Size(widget.radius * 2, widget.radius * 2),
);
}
}
class ClockPainter extends CustomPainter {
//边框的颜色
final Color borderColor;
//刻度的颜色
final Color scaleColor;
//数字的颜色
final Color numberColor;
//中间圆颜色
final Color middleCircleColor;
//走秒小球颜色
final Color moveBallColor;
//时针的颜色
final Color hourHandColor;
//分针的颜色
final Color minuteHandColor;
//秒针的颜色
final Color secondHandColor;
//边框画笔的宽度
double borderWidth;
//刻度画笔的宽度
double scaleWidth;
//数字画笔的宽度
double numberWidth;
//时针画笔的宽度
double hourHandWidth;
//分针画笔的宽度
double minuteHandWidth;
//秒针画笔的宽度
double secondHandWidth;
//中间圆的宽度
double middleCircleWidth;
//小刻度的位置集合
List<Offset> scaleOffset = [];
//大刻度的位置集合 每5个小刻度是一个大刻度
List<Offset> bigScaleOffset = [];
//钟表的半径
final double radius;
//当前时间
final DateTime dateTime;
//边框画笔
Paint borderPaint;
//刻度画笔
Paint scalePaint;
//大刻度画笔
Paint biggerScalePaint;
//数字画笔
TextPainter textPainter;
//时针画笔
Paint hourPaint;
//分针画笔
Paint minutePaint;
//秒针画笔
Paint secondPaint;
//中间圆画笔
Paint centerPaint;
//移动小球画笔
Paint moveBallPaint;
ClockPainter(
this.dateTime, {
this.radius,
this.borderColor,
this.scaleColor,
this.numberColor,
this.moveBallColor,
this.hourHandColor,
this.minuteHandColor,
this.secondHandColor,
this.middleCircleColor,
}) {
//根据自己的审美设置这些画笔的宽度
borderWidth = 8 * (radius / 100);
scaleWidth = 2 * (radius / 100);
numberWidth = 20 * (radius / 100);
hourHandWidth = 5 * (radius / 100);
minuteHandWidth = 3 * (radius / 100);
secondHandWidth = 1 * (radius / 100);
middleCircleWidth = 4 * (radius / 100);
//边框画笔
borderPaint =
createPaint(borderColor, borderWidth, style: PaintingStyle.stroke);
//刻度画笔
scalePaint = createPaint(numberColor, scaleWidth);
//大刻度
biggerScalePaint = createPaint(numberColor, scaleWidth * 2);
//时针画笔
hourPaint = createPaint(hourHandColor, hourHandWidth);
//分针画笔
minutePaint = createPaint(minuteHandColor, minuteHandWidth);
//秒针画笔
secondPaint = createPaint(secondHandColor, secondHandWidth);
//中间圆
centerPaint = createPaint(middleCircleColor, middleCircleWidth);
//移动小球画笔
moveBallPaint = createPaint(moveBallColor, scaleWidth * 2);
//数字
textPainter = TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);
//计算出 小刻度和大刻度
final l = radius - borderWidth * 2;
for (var i = 0; i < 60; i++) {
Offset offset = pointOffset(radius, l, 360 / 60 * i);
//小刻度
scaleOffset.add(offset);
//大刻度
if (i % 5 == 0) {
bigScaleOffset.add(offset);
}
}
}
@override
void paint(Canvas canvas, Size size) {
//绘制边框
drawBorder(canvas);
//绘制刻度
drawScale(canvas);
//绘制数字
drawNumber(canvas);
//绘制时针
drawHour(canvas);
//绘制分针
drawMinute(canvas);
//绘制秒针
drawSecond(canvas);
//绘制中间圆圈
drawMiddleCircle(canvas);
//绘制移动小球
drawMoveBall(canvas);
}
//判断是否需要重绘
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
///绘制边框
void drawBorder(Canvas canvas) {
canvas.drawCircle(
Offset(radius, radius), radius - borderWidth / 2, borderPaint);
}
///绘制刻度
void drawScale(Canvas canvas) {
//小刻度
canvas.drawPoints(PointMode.points, scaleOffset, scalePaint);
//大刻度
canvas.drawPoints(PointMode.points, bigScaleOffset, biggerScalePaint);
}
///绘制数字
void drawNumber(Canvas canvas) {
double l = radius - borderWidth * 4;
for (var i = 0; i < bigScaleOffset.length; i++) {
textPainter.text = TextSpan(
text: "${i == 0 ? 12 : i}",
style: TextStyle(color: numberColor, fontSize: numberWidth),
);
Offset offset = pointOffset(radius, l, i * 360 / 12);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
offset.dx - (textPainter.width / 2),
offset.dy - (textPainter.height / 2),
));
}
}
///绘制时针
void drawHour(Canvas canvas) {
final hour = dateTime.hour;
double angle = 360 / 12 * hour + dateTime.minute / 60 * 30;
Offset hourHand1 = pointOffset(radius, radius * 0.1, angle + 180);
Offset hourHand2 = pointOffset(radius, radius * 0.45, angle);
canvas.drawLine(hourHand1, hourHand2, hourPaint);
}
///绘制分针
void drawMinute(Canvas canvas) {
final minute = dateTime.minute;
double angle = 360 / 60 * minute + dateTime.second / 60 * 6;
Offset minuteHand1 = pointOffset(radius, radius * 0.1, angle + 180);
Offset minuteHand2 = pointOffset(radius, radius * 0.7, angle);
canvas.drawLine(minuteHand1, minuteHand2, minutePaint);
}
///绘制秒针
void drawSecond(Canvas canvas) {
final second = dateTime.second;
double angle = 360 / 60 * second;
Offset secondHand1 = pointOffset(radius, radius * 0.1, angle + 180);
Offset secondHand2 = pointOffset(radius, radius * 0.7, angle);
canvas.drawLine(secondHand1, secondHand2, secondPaint);
}
///绘制中间圆圈
void drawMiddleCircle(Canvas canvas) {
canvas.drawCircle(Offset(radius, radius), middleCircleWidth, centerPaint);
}
///绘制移动小球
void drawMoveBall(Canvas canvas) {
final second = dateTime.second;
canvas.drawCircle(scaleOffset[second], middleCircleWidth, moveBallPaint);
}
}
///创建Paint
Paint createPaint(Color color, double strokeWidth,
{PaintingStyle style = PaintingStyle.fill}) {
return Paint()
..color = color
..isAntiAlias = true
..style = style
..strokeCap = StrokeCap.round
..strokeWidth = strokeWidth;
}
///圆中万能求点公式
Offset pointOffset(double radius, double l, double angle) {
return Offset(
radius + l * sin(degToRad(angle)),
radius - l * cos(degToRad(angle)),
);
}
///角度转换为弧度
num degToRad(num deg) => deg * (pi / 180.0);
到这里就结束了,欢迎同学们的关注哦!
公众号:

作者介绍

移动小样
V1