Flutter 绘制实践 | 路径篇 · 数字显示管


theme: cyanosis

highlight: mono-blue

前言

欢迎来到 Flutter 绘制实践系列,本文有 文章版视频版。视频发布在 bilibli 同名账号下,文章首发于掘金平台。今天来的话题是: 数字显示管 , 如下所示,展示 0~9 十个数字:

e8dd9252b53a58d73ff473a25fa5168.png

结合交互,可以实现豪华版的计数器:

71.gif


注: Flutter 绘制实践系列视频链接:


1. 数字显示管的特征分析

通过观察不难发现,这十个数字是由 7 个管 的不同点亮状态决定的,管的编号如下所示。比如对于 数字 8 来说, 七个管全部点亮; 数字 1 点亮 4、7 号管。这样就将 10 个数字路径的绘制转换成 7 个管路径 的绘制。

image.png


再仔细观察可以发现,这 7 个管 之间也有这对应关系。比如 2号和4号5号和7号 关于中心 Y 轴对称; 2号和5号1号和6号 关于中心 X 轴对称。这样,其实只要完成 1,2,3 号 管路径即可,其他四个可以通过路径变换得到:

image.png


2.数字管路径的形成

数字管路径是多边形,所以最重要的是端点坐标数据。可以在 PS 中量取相关的数据,当然这种量取会存在一定的误差,需要最后手动校正一下。如果有条件的可以让设计师制作一个 数字 8 的 svg 文件,这样其中会包含相关的路径数据,方便取点。

image.png

这里 PS 中视口宽高为 104*169 , 量取的尺寸可以直接使用,需要缩放时可以使用路径的变换。如下是 1,2,3 号管路径绘制的效果:

image.png

``` 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 号管:

image.png

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 号管也可以通过已知路径变换获得:

image.png

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) 来维护。这里考虑到未来可能对 :. 等字符进行支持,使用 StringList<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] 编号列表,映射为对应的路径列表:

image.png

``` 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 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 _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 paths = v.map((value) => map[value]!).toList(); Path path = combineAll(paths); _digitalPathMap[key] = path; }); } }

```

比如下面通过 digitalPath#buildPath 绘制两个数字,通过 width 可以缩放数字:

image.png

@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 组件来展示 单个 数字显示管,可以设置宽度、颜色、数字值:

image.png

``` 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 方便使用,效果如下:

e8dd9252b53a58d73ff473a25fa5168.png

``` // 展示若干位数字 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 号管的路径。根据它们的变换和点亮状态,可以聚集成有意义的单个数字、单个数字的聚集可以形成整数。结合交互,就可以形成一个豪华版的计数器:

71.gif

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值