likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 64
前言

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

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

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


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


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

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

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


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

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


2.数字管路径的形成

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

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

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

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

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

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

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

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

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] 编号列表,映射为对应的路径列表:

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

 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 可以缩放数字:

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

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

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

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
评论
请登录