theme: cyanosis
highlight: mono-blue
前言
欢迎来到 Flutter 绘制实践系列,本文有 文章版 和 视频版。视频发布在 bilibli 同名账号下,文章首发于掘金平台。今天来的话题是: 数字显示管 , 如下所示,展示 0~9
十个数字:
结合交互,可以实现豪华版的计数器:
注: Flutter 绘制实践系列视频链接:
- Flutter 绘制实践 | 第一集 · 画板尺寸
- Flutter 绘制实践 | 第二集 · 坐标系
- Flutter 绘制实践 | 第三集 · 画板更新
- Flutter 绘制实践 | 第四集 · 动画数值
- Flutter 绘制实践 | 第五集 · 坐标轴范围
- Flutter 绘制实践 | 第六集 · 函数曲线
- Flutter 绘制实践 | 路径篇 · 雪花1
- Flutter 绘制实践 | 路径篇 · 雪花2
- Flutter 绘制实践 | 路径篇 · 变换中心
- Flutter 绘制实践 | 路径篇 · 阴影模糊
- Flutter 绘制实践 | 路径篇 · 数字显示管
1. 数字显示管的特征分析
通过观察不难发现,这十个数字是由 7 个管
的不同点亮状态决定的,管的编号如下所示。比如对于 数字 8 来说, 七个管全部点亮; 数字 1 点亮 4、7 号管
。这样就将 10 个数字路径的绘制转换成 7 个管路径
的绘制。
再仔细观察可以发现,这 7 个管
之间也有这对应关系。比如 2号和4号
、5号和7号
关于中心 Y 轴对称; 2号和5号
、1号和6号
关于中心 X 轴对称。这样,其实只要完成 1,2,3 号
管路径即可,其他四个可以通过路径变换得到:
2.数字管路径的形成
数字管路径是多边形,所以最重要的是端点坐标数据。可以在 PS 中量取相关的数据,当然这种量取会存在一定的误差,需要最后手动校正一下。如果有条件的可以让设计师制作一个 数字 8 的 svg 文件
,这样其中会包含相关的路径数据,方便取点。
这里 PS 中视口宽高为 104*169
, 量取的尺寸可以直接使用,需要缩放时可以使用路径的变换。如下是 1,2,3
号管路径绘制的效果:
``` double width = 104; double height = 169; Path path1 = Path() ..moveTo(5, 0) ..lineTo(99 , 0) ..lineTo(71, 26) ..lineTo(32.8, 26)..close();
Path path2 = Path() ..moveTo(0, 2) ..lineTo(0 , 74) ..lineTo(13 , 83) ..lineTo(26 , 71) ..lineTo(26 , 27) ..close();
Path path3 = Path() ..moveTo(18, 84) ..lineTo(31 , 97) ..lineTo(73 , 97) ..lineTo(86 , 85) ..lineTo(75 , 74) ..lineTo(31 , 74) ..close(); ```
下面来通过路径变换
生成其他路径,先让 2 号管沿中心 Y 轴对称,生成 4 号管:
Matrix4 mirrorY = Matrix4.identity(); mirrorY.translate(width/2,0.0); mirrorY.scale(-1.0,1.0,0.0); mirrorY.translate(-width/2,0.0); Path path4 = path2.transform(mirrorY.storage);
同样, 5,6,7 号管也可以通过已知路径变换获得:
Matrix4 mirrorX = Matrix4.identity(); mirrorX.translate(0.0,height/2); mirrorX.scale(1.0,-1.0,0.0); mirrorX.translate(0.0,-height/2); Path path5 = path2.transform(mirrorX.storage); Path path7 = path5.transform(mirrorY.storage); Path path6 = path1.transform(mirrorX.storage);
这样数字管最核心的内容就完成了,下面来处理不同数字的装配。
3. 数字路径的装配
不同的数字对应不同的管路径列表,所以它们直接可以通过一个映射关系 (Map) 来维护。这里考虑到未来可能对 :
、.
等字符进行支持,使用 String
和 List<int>
的映射,如下所示:
Map<String,List<int>> digitalMap = { '0': [1,2,4,5,6,7], '1': [4,7], '2': [1,4,3,5,6], '3': [1,4,3,6,7], '4': [2,3,4,7], '5': [1,2,3,6,7], '6': [1,2,3,5,6,7], '7': [1,4,7], '8': [1,2,3,4,5,6,7], '9': [1,2,3,4,6,7], };
此时,可以封装一个 digitalPath
方法,用于返回 value
数字的路径。核心逻辑是从 digitalMap
中查找数字对应管的编号列表,收集之后合并起来,就相当于点亮的对应的管。比如 数字 5
的路径,就是 [1,2,3,6,7]
编号列表,映射为对应的路径列表:
``` Path digitalPath(double rate, int value){ Map map = {}; // 七个 path 形成过程同上,略... map[1] = path1; map[2] = path2; map[3] = path3; map[4] = path4; map[5] = path5; map[6] = path6; map[7] = path7;
List<Path> paths = digitalMap[value.toString()]!.map((value) => map[value]!).toList();
return combineAll(paths);
}
Path combineAll(List
```
4. 路径处理的优化
如果有大量数字或频繁绘制时,每次绘制时都通过 digitalPath
方法获取路径的话,并不是很友好。因为数字路径是相对固定的,管路径以及装配的流程不需要每次都进行处理。我们可以将数字路径通过 Map
进行存储,在使用时从映射表中直接取出,这是很典型的 用空间换取时间。
如下代码中定义 DigitalPath
类,使用 _digitalPathMap
映射维护数字路径。在构造方法中使用 _initDigitalPathMap
方法生成路径并存入 _digitalPathMap
里;另外通过 buildPath
方法让外界访问路径,其中 width
参数应用控制数字的宽度,据此可以算出缩放值,对路径进行变换。
``` class DigitalPath {
static const double kDigitalRate = 169/104;
final Map _digitalPathMap = {};
DigitalPath(){ _initDigitalPathMap(); }
Path buildPath(int value,double width){ double rate = width/104; Matrix4 matrix4 = Matrix4.identity(); matrix4.scale(rate,rate,0.0); return _digitalPathMap[value.toString()]!.transform(matrix4.storage); }
void _initDigitalPathMap(){ _digitalPathMap.clear(); // 略同... digitalMap.forEach((key, v) { List
```
比如下面通过 digitalPath#buildPath
绘制两个数字,通过 width
可以缩放数字:
@override void paint(Canvas canvas, Size size) { Path path = digitalPath.buildPath(8, size.width); canvas.drawPath(path, _mainPainter); canvas.translate(size.width+20, 0); Path path2 = digitalPath.buildPath(5, size.width/2); canvas.drawPath(path2, _mainPainter..color=Colors.red); }
5. 画板绘制内容的组件化
Flutter 中大家比较习惯使用组件,所以可以封装一下简化使用。如下所示,通过 SingleDigitalWidget
组件来展示 单个
数字显示管,可以设置宽度、颜色、数字值:
``` class SingleDigitalWidget extends StatelessWidget { final double width; final Color color; final int value; final DigitalPath digitalPath;
SingleDigitalWidget({ Key? key, required this.width, required this.value, DigitalPath? digitalPath, this.color = Colors.black, }) : digitalPath = digitalPath ?? DigitalPath(), super(key: key);
@override Widget build(BuildContext context) { return CustomPaint( size: Size(width, width * DigitalPath.kDigitalRate), painter: DigitalPainter( color: color, value: value, digitalPath: digitalPath, ), ); } } ```
不过 SingleDigitalWidget
只能展示单个数字,比如想显示 1994
就不行了。这个问题的解决方案也很简单,Flutter 组件拥有强大的组合性质,多排几个就行了。如下所示,通过 Wrap
组件排列 count 个 SingleDigitalWidget
组件,就可以显示 count 位数字,封装为 MultiDigitalWidget
方便使用,效果如下:
``` // 展示若干位数字 class MultiDigitalWidget extends StatelessWidget { final int count; final int value; final DigitalPath digitalPath; final double spacing; final double runSpacing; final double width; final List colors;
MultiDigitalWidget({ Key? key, required this.count, required this.value, this.spacing = 26, this.runSpacing = 26, required this.width, this.colors = const [], DigitalPath? digitalPath, }) : digitalPath = digitalPath ?? DigitalPath(), super(key: key);
@override Widget build(BuildContext context) { int max = math.pow(10, count).toInt(); String numStr = (value % max).toString().padLeft(count, "0"); Color color = Colors.black;
return Wrap(
spacing: spacing,
runSpacing: runSpacing,
children: List.generate(count, (index) {
if (index < colors.length) {
color = colors[index];
}
return SingleDigitalWidget(
width: width,
color: color,
value: int.parse(numStr[index]),
digitalPath: digitalPath,
);
}),
);
} } ```
从这里可以思考一下,很多大的事物都是通过小事物组合而成的。在数字显示管的绘制过程中,核心的是 1,2,3
号管的路径。根据它们的变换和点亮状态,可以聚集成有意义的单个数字、单个数字的聚集可以形成整数。结合交互,就可以形成一个豪华版的计数器: