介绍
什么是数码管:
数码管就是我们很多液晶屏或者小家电上显示数字的小显示屏, 一个数字“8”对应一位数码管,每个数码管8个LED:数字“8”的7个笔画,以及小数点
一下是网上找到的数码管尺寸图,可以看到数码管是呈10度倾斜的
因为我在大学,自己搞了些单片机,所以对这东西非常熟悉
实现分析
因为我自己app的实际需要,并不需要小数点,所以只需要显示数字8,如果是显示0~99的数字,那就用Row集合两个“8”就可以了。所以问题的关键就是显示数字“8”
显然用0~9 十张图片是最简单也是最low的,当然不想使用,于是决定试一试Flutter CustomPainter,配合贝塞尔曲线来绘制。
看上面的尺寸图,大家可以看到,“8”的每一个笔画是有编号的,最顶部是a,然后顺时针递增,最中间的笔画是g,后面描述的时候会用到
笔画a最简单,通过6个点,即可画出其轮廓的贝塞尔曲线,然后填充颜色即可,之后的6个笔画,因为位置未知,所以计算三角函数来得出位置很麻烦,所以这里使用了3维变换,(x,y)轴平移,z轴旋转,即可挪到对应位置。比如笔画b,是通过笔画a右移笔画长度(外加两者间隙),然后旋转(90+10)度完成的,笔画C是笔画B移动笔画长度完成的,以此类推。画完了数字“8”,再根据输入的数字是0~9,决定每个笔画的颜色。
所以技术难点解析成了一下几项:
- 如何在flutter中绘图
- 如果绘制贝塞尔曲线
- 如何为贝塞尔曲线填充颜色
- 如何三维变换
- 如何实现数字到数码管笔画的染色
代码实现
1) CustomPainter
Flutter中,CustomPainter是个抽象类,需要我们自己继承子类,然后重写几个方法:
class NumberPart extends CustomPainter {
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
@override
void paint(Canvas canvas, Size size) {
}
}
shouldRepaint告诉flutter是否需要重绘,除非为了性能优化,否则直接返回true就可以了,这里不扩展讲 paint是绘图的核心方法,参数canvas是画布,size是画布大小
canvas可以画圆,画线,画图片和文字等等,定制内容的话,可以画path(贝塞尔曲线)
2) 贝塞尔曲线
贝塞尔曲线在flutter中的实现也很简单,是一个Path类,然后通过在Path上添加点/线/弧等,绘制路径,这里我们使用addPolygon来添加多边形 下面这个方法,就是创建笔画a,一个类似六边形的形状,里面的width是线宽,lerp这个单词其实我也说不清意思,就是六边形左上方的点距离最左边点的x轴位移,length就是笔画长度。
Path genPath(double length, double width) {
final path = Path();
double lerp = width / 1.7;
path.addPolygon([
Offset(0, 0),
Offset(lerp, -width / 2),
Offset(length - lerp, -width / 2) et,
Offset(length, 0) + offset,
Offset(length - lerp, width / 2)+ offset,
Offset(lerp, width / 2)
], true);
return path;
}
3) 笔画染色和绘制
在绘制笔画到画布的时候,需要指定paint,也就是染色方式,比如是否填充啊,各种颜色啊啥的,毕竟贝塞尔曲线只是线的走向,既没宽度也没颜色的
final highlightPaint = Paint()
..style = PaintingStyle.fill
..color = highlightColor;
final delightPaint = Paint()
..style = PaintingStyle.fill
..color = delightColor;
画笔是Paint类,然后通过设置style和color,设置成填充某种颜色,highlightPaint是笔画高亮的时候的颜色,比如亮红色,delightPaint是暗的时候的颜色,比如暗红色,很多数码管,不亮的时候也能看到颜色,为了逼真我们也这么干
canvas.drawPath(pathA, getPaint());
通过canvas.drawPath,然后传入路径和画笔,即可画出笔画a
4) 贝塞尔曲线的移动和旋转
贝塞尔曲线的移动和旋转称为transform,平移叫translate,旋转叫rotate,我们是在水平面旋转,所以是rotateZ,沿Z轴旋转。
Path pathB = genPath(Offset.zero, length, width);
transform.translate(length);
transform.translate(gap, gap);
transform.rotateZ((10 + 90) / 180 * 3.14159);
pathB = pathB.transform(transform.storage);
canvas.drawPath(pathB, getPaint());
第一行创建笔画b的贝塞尔曲线路径,然后平移笔画长度,因为笔画a/b之间有一个间隙,所以x,y轴移动gap,再旋转90+10度,因为rotateZ的参数是弧度制,所以转换一下,最后将transform的数值通过transform.storage变成矩阵,传递给pathB.transform,就旋转完了。 旋转笔画c的时候,还是在画布原点创建,然后在移动b的transform基础上,再向x轴移动length + gap就可以了:
Path pathC = genPath(Offset.zero, length, width);
transform.translate(length + gap);
pathC = pathC.transform(transform.storage);
canvas.drawPath(pathC, getPaint(2));
你可能会奇怪,明明笔画c是笔画b向左下移动,为什么是translate的x轴?因为transform里,本身有个旋转。
5) 数字到绘图的映射
数码管通过7个笔画(a-g)的明暗,来显示0~9,所以我们来通过全局数组来展示编码:
final matrix = [
[
//0
true,
true,
true,
true,
true,
true,
false,
],
[
//1
false,
true,
true,
false,
false,
false,
false,
],
[
//2
true,
true,
false,
true,
true,
false,
true,
],
[
//3
true,
true,
true,
true,
false,
false,
true,
],
[
//4
false,
true,
true,
false,
false,
true,
true,
],
[
//5
true,
false,
true,
true,
false,
true,
true,
],
[
//6
true,
false,
true,
true,
true,
true,
true,
],
[
//7
true,
true,
true,
false,
false,
false,
false,
],
[
//8
true,
true,
true,
true,
true,
true,
true,
],
[
//9
true,
true,
true,
true,
false,
true,
true,
]
];
第一维是哪个数字,第二维是哪个笔画的明暗 所以通过matrix[num][index]即可反应这个笔画的明暗 比如数字0的笔画b的状态,就是matrix[0][1]==true,也就是笔画b在显示数字0时,要亮。
完整代码
import 'package:flutter/material.dart';
final matrix = [
[
//0
true,
true,
true,
true,
true,
true,
false,
],
[
//1
false,
true,
true,
false,
false,
false,
false,
],
[
//2
true,
true,
false,
true,
true,
false,
true,
],
[
//3
true,
true,
true,
true,
false,
false,
true,
],
[
//4
false,
true,
true,
false,
false,
true,
true,
],
[
//5
true,
false,
true,
true,
false,
true,
true,
],
[
//6
true,
false,
true,
true,
true,
true,
true,
],
[
//7
true,
true,
true,
false,
false,
false,
false,
],
[
//8
true,
true,
true,
true,
true,
true,
true,
],
[
//9
true,
true,
true,
true,
false,
true,
true,
]
];
class DigitalNumber extends StatelessWidget {
final double height;
final double width;
final double lineWidth;
final int num;
final bool dotLight;
final Color highlightColor;
final Color delightColor;
DigitalNumber(
{@required this.height,
@required this.width,
this.lineWidth = 8,
num,
this.dotLight = true,
this.highlightColor = Colors.red,
this.delightColor = const Color(0x33FF0000)})
: this.num = num > 0 ? (num > 9 ? 9 : num) : 0;
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: NumberPart(
lineWidth: lineWidth,
num: num,
dotLight: dotLight,
highlightColor: highlightColor,
delightColor: delightColor),
size: Size(width, height),
);
}
}
class NumberPart extends CustomPainter {
final int num;
final bool dotLight;
final Color highlightColor;
final Color delightColor;
final double lineWidth;
NumberPart(
{@required this.lineWidth,
@required this.num,
@required this.dotLight,
@required this.highlightColor,
@required this.delightColor});
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
Paint getPaint(int index) {
final highlightPaint = Paint()
..style = PaintingStyle.fill
..color = highlightColor;
final delightPaint = Paint()
..style = PaintingStyle.fill
..color = delightColor;
return matrix[num][index] ? highlightPaint : delightPaint;
}
Path genPath(Offset offset, double length, double width) {
final path = Path();
double lerp = width / 1.7;
path.addPolygon([ Offset(0, 0) + offset, Offset(lerp, -width / 2) + offset, Offset(length - lerp, -width / 2) + offset, Offset(length, 0) + offset, Offset(length - lerp, width / 2) + offset, Offset(lerp, width / 2) + offset ], true);
return path;
}
@override
void paint(Canvas canvas, Size size) {
double width = lineWidth;
double length = (size.width) / 1.5 - width;
double leftOffset = size.width / 3;
double gap = width / 8;
Path pathA = genPath(Offset.zero, length, width);
Matrix4 transform = Matrix4.identity();
transform.translate(leftOffset, width / 2 + 2);
pathA = pathA.transform(transform.storage);
canvas.drawPath(pathA, getPaint(0));
Path pathB = genPath(Offset.zero, length, width);
transform.translate(length);
transform.translate(gap, gap);
transform.rotateZ((10 + 90) / 180 * 3.14159);
pathB = pathB.transform(transform.storage);
canvas.drawPath(pathB, getPaint(1));
Path pathC = genPath(Offset.zero, length, width);
transform.translate(length + gap);
pathC = pathC.transform(transform.storage);
canvas.drawPath(pathC, getPaint(2));
Path pathD = genPath(Offset.zero, length, width);
transform.translate(length + gap, gap);
transform.rotateZ((90 - 10) / 180 * 3.14159);
pathD = pathD.transform(transform.storage);
canvas.drawPath(pathD, getPaint(3));
Path pathE = genPath(Offset.zero, length, width);
transform.translate(length + gap, gap);
transform.rotateZ((90 + 10) / 180 * 3.14159);
pathE = pathE.transform(transform.storage);
canvas.drawPath(pathE, getPaint(4));
Path pathF = genPath(Offset.zero, length, width);
Matrix4 transformF = transform.clone();
transformF.translate(length + gap);
pathF = pathF.transform(transformF.storage);
canvas.drawPath(pathF, getPaint(5));
Path pathG = genPath(Offset.zero, length, width);
transform.translate(length + gap / 2, gap);
transform.rotateZ((90 - 10) / 180 * 3.14159);
pathG = pathG.transform(transform.storage);
canvas.drawPath(pathG, getPaint(6));
}
}
DigitalNumber类就是单个数码管的widget,需要指定大小(数码管适应指定的大小),可以配置笔画的宽度,指定显示哪个数字,以及明暗两种颜色。dotLight暂时没有实现
使用的代码也很简单:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DigitalNumber(
height: 60,
width: 45,
num: 1
lineWidth: 6,
),
DigitalNumber(
height: 60,
width: 45,
num:3,
lineWidth: 6,
),
],
)
这样就可以显示数字13了。
后记
做这个项目的时候,就想到了当时做单片机 单片机的多位数码管显示,是通过n+8个引脚控制的,n个引脚对应几位数码管,8个引脚对应这一排数码管的笔画,这样组成一个矩阵,然后通过定时器扫描的方式,轮询逐位显示每一位数码管,比如时刻1,使能第0位数码管,然后通过编码控制8个引脚,让数码管显示数字1,然后到时刻2,关闭第0位数码管,使能第1位数码管,通过编码显示数字3,往复扫描,虽然某一时刻只能显示一位数字,但是因为扫描很快,所以肉眼看到的就是完整的数字13了。这个就是最初的屏幕扫描频率
在单片机的显示中,通过某个输入获取到数字,到让数字显示到数码管,是两个逻辑,两个逻辑都有自己的操作周期,所以两个不能耦合,于是获取数字的逻辑,获取到新的数字以后,会将这个数字(或者对应的数码管编码)存放在数组中,然后到了刷新数码管的周期,数码管程序通过读取这个数组的数字,显示在数码管上,那么这个数组,就是显存啦。哈哈
最后贴上我做的完整app,这是一个遥控车控制app,有前进和转向两个摇杆,控制的数据通过udp发送给遥控车,遥控车上有esp8226 wifi芯片,配置成AP模式,也就是wifi基站,app的udp数据发送给esp8226后,下位机转换成PWM数据,控制舵机和L298N电机驱动芯片,后者控制减速电机让小车运动。app、下位机电路、esp8226编程,遥控车整车都是我自己做的,非常有乐趣,下次有机会给大家说说我做的遥控车,大家2019年快乐~~~~
今天的文章使用Flutter CustomPainter绘制8段数码管分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/16362.html