使用Flutter CustomPainter繪製8段數碼管

Realank Liu發表於2018-12-29

介紹

什麼是數碼管:

數碼管就是我們很多液晶屏或者小家電上顯示數字的小螢幕, 一個數字“8”對應一位數碼管,每個數碼管8個LED:數字“8”的7個筆畫,以及小數點

使用Flutter CustomPainter繪製8段數碼管
一下是網上找到的數碼管尺寸圖,可以看到數碼管是呈10度傾斜的
使用Flutter CustomPainter繪製8段數碼管
因為我在大學,自己搞了些微控制器,所以對這東西非常熟悉

實現分析

因為我自己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段數碼管

相關文章