博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用Flutter CustomPainter绘制8段数码管
阅读量:6848 次
发布时间:2019-06-26

本文共 9332 字,大约阅读时间需要 31 分钟。

介绍

什么是数码管:

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

转载地址:http://bulul.baihongyu.com/

你可能感兴趣的文章
第一次实验报告
查看>>
正则匹配replace替换重复字符串
查看>>
[Linux学习]chattr配置文件隐藏属性及lsattr查看隐藏属性
查看>>
mybatis大数据提交和更新,数据SQL语句批量提交数据库
查看>>
每天一个小算法(Shell Sort2)
查看>>
Java数据结构与算法(7) - ch05双向链表(Double List)
查看>>
fiddler 教程
查看>>
自适应布局
查看>>
ILSpy c#反编译工具,附下载地址
查看>>
课堂练习——查找水王续
查看>>
在后台设置yii的配置文件
查看>>
(4/24) webpack3.x快速搭建本地服务和实现热更新
查看>>
Failure is not fatal, but failure to change might be.
查看>>
L2-015. 互评成绩
查看>>
iOS9新特性
查看>>
poj3186 poj3267
查看>>
烂泥:学习centos之快速搭建LNMP环境
查看>>
Poj2723:Get Luffy Out
查看>>
L365
查看>>
SUST OJ 1642: 绝地求生—死亡顺序
查看>>