使用Flutter CustomPainter绘制8段数码管

介绍

什么是数码管:

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

猜你喜欢

转载自juejin.im/post/5c27366af265da615a41e681
今日推荐