介绍
什么是数码管:
数码管就是我们很多液晶屏或者小家电上显示数字的小显示屏, 一个数字“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:[ 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年快乐~~~~