介紹
什麼是數碼管:
數碼管就是我們很多液晶屏或者小家電上顯示數字的小螢幕, 一個數字“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年快樂~~~~