Flutter 绘制实践 | 路径篇 · 数字显示管
前言
结合交互,可以实现豪华版的计数器:
注: 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<int,Path> 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<Path> paths,
{PathOperation operation = PathOperation.union}) {
if (paths.isEmpty) return Path();
if (paths.length <= 1) return paths.first;
Path result = paths.first;
for (int i = 1; i < paths.length; i++) {
result = Path.combine(operation, paths[i], result);
}
return result;
}
4. 路径处理的优化
如果有大量数字或频繁绘制时,每次绘制时都通过 digitalPath
方法获取路径的话,并不是很友好。因为数字路径是相对固定的,管路径以及装配的流程不需要每次都进行处理。我们可以将数字路径通过 Map
进行存储,在使用时从映射表中直接取出,这是很典型的 用空间换取时间。
如下代码中定义 DigitalPath
类,使用 _digitalPathMap
映射维护数字路径。在构造方法中使用 _initDigitalPathMap
方法生成路径并存入 _digitalPathMap
里;另外通过 buildPath
方法让外界访问路径,其中 width
参数应用控制数字的宽度,据此可以算出缩放值,对路径进行变换。
class DigitalPath {
static const double kDigitalRate = 169/104;
final Map<String,Path> _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<Path> paths = v.map((value) => map[value]!).toList();
Path path = combineAll(paths);
_digitalPathMap[key] = path;
});
}
}
比如下面通过 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<Color> 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
号管的路径。根据它们的变换和点亮状态,可以聚集成有意义的单个数字、单个数字的聚集可以形成整数。结合交互,就可以形成一个豪华版的计数器:
转载自:https://juejin.cn/post/7196698315835129912