likes
comments
collection
share

【Flutter 绘制番外】svg 文件与绘制 (上)

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

对一些有趣的绘制 技能知识, 我会通过 [番外篇] 的形式加入《Flutter 绘制指南 - 妙笔生花》小册中,一方面保证小册的“与时俱进”“活力”。另一方面,是为了让一些重要的知识有个 好的归宿。本文源码可以看这里。 另外一个好消息: 《Flutter 绘制指南 - 妙笔生花》小册源码 idraw 已经完成了 空安全 的转化。


一、对 svg 的认识

1. 初见

通过 F12 可以看到掘金的 logo 是一个 svg ,可以将它作为文件下载。

【Flutter 绘制番外】svg 文件与绘制 (上)


打开后可以看出其中有很多不明所以的字符,可以确定的是:这些字符决定了 logo 的显示。至于这些字符为什么可以控制显示,又如何控制显示,对于初见者并不能理解。

【Flutter 绘制番外】svg 文件与绘制 (上)


2. 试探

AdroidStudio 中可以实时显示 svg 文件的表现效果,如下将一段 path 注释掉,可以看出 少了一块。

【Flutter 绘制番外】svg 文件与绘制 (上)


再注释掉一个 path ,可以看到又少了一块。所以可以看出,每个 path 块都表示一部分的路径。

【Flutter 绘制番外】svg 文件与绘制 (上)


二、直线路径操作符

1. 水平和竖直 绝对 路径: HV

如下是 M0,0 H50 V50 H0 的效果:

<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M0,0 H50 V50 H0" fill="#FFFFFF"/>
</svg>

M0,0 表示:从起点移至 0,0H50 表示:水平移动 横坐标 50 处。 V50 表示:竖直移动 纵坐标 50 处。 H0 表示:水平移动 横坐标 0 处。 最后路径会连到起点 0,0 ,另外 fill="#FFFFFF" 表示填充白色。

【Flutter 绘制番外】svg 文件与绘制 (上)


2.绝对移动 : M

如下橙色区域是 M20,0 H50 V50 H0 的效果,M20,0 表示路径起点移至 (20,0) 坐标。

【Flutter 绘制番外】svg 文件与绘制 (上)


4.直线绝对 路径:L

如下蓝色区域是 M28,0 L43,0 12,32 4,25 的效果,只要知道点的坐标就可以通过 L 符号,将点依次拼接形成路径。

【Flutter 绘制番外】svg 文件与绘制 (上)


只要知道图形所有的点位坐标,就可以形成路径。将多段路径合在一起,就可以来显示期望的图案,比如下面的 Flutter 图标。

<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M0,0 H50 V50 H0" fill="#FFFFFF" />
    <path d="M28,2 L43,2 12,32 4,25" fill="#3AD0FF" />
    <path d="M16,36 L30,48 44,48 23,29" fill="#00559E" />
    <path d="M16,36 L28,24 42,24 24,43" fill="#3AD0FF" />
</svg>

【Flutter 绘制番外】svg 文件与绘制 (上)


仔细看一下,就可以发现,这么复杂的字符,也只是 M/H/V/L 四个操作符的拼接而已。另外注意一下,逗号 , 和空格 可以互换。一般的 svg 文件都是由 设计软件 生成的,所以都是空格,一般不具有可读性。

【Flutter 绘制番外】svg 文件与绘制 (上)

其实对于 Flutter 绘制而言,最重要的是路径 Path 的形成,那么既然 svg 文件里有路径信息,是不是意味着我们可以提取坐标、生成路径,然后进行绘制呢?废话不多说,一起来试验一下。


三、svg 直线型操作符转化为 Path 对象

1. 如何对 svg 路径进行解析

现在的问题在于如何将 svg 路径解析处我们需要的信息,对一字符串的处理,自然是非 正则 莫属了。只要写出一条何时的正则,进行匹配即可。

<path d="M17.5865 17.3955H17.5902L28.5163 8.77432L25.5528 6.39453L17.5902 12.6808H17.5865L17.5828 12.6845L9.62018 6.40201L6.6604 8.78181L17.5828 17.3992L17.5865 17.3955Z" fill="#1E80FF"/>

在后期会发布一个关于 正则的小册 ,其中会通过实现如下的正则校验的应用,来让大家更有趣地认识正则。目前文件中只有 M,H,V,L 四个指令,通过下面的命令就可以匹配每段指令的信息。

【Flutter 绘制番外】svg 文件与绘制 (上)


通过匹配后,我们就可以获取其中必要的信息,如下所示:

【Flutter 绘制番外】svg 文件与绘制 (上)


2. 与 Flutter 绘制的衔接

如下方法是通过解析一条 svg 路径,形成 FlutterPath 的过程。注意目前只有 M,H,V,L,Z 四个指令,其他 svg 指令在后面会继续完善。通过绘制形成的路径就能显示出来了:

Color color = const Color(0xff1E80FF);
canvas.drawPath(formPathFromSvgOp(src), paint..color=color);

【Flutter 绘制番外】svg 文件与绘制 (上)

Path formPathFromSvgOp(String src) {
  Path path = Path();
  RegExp regExp = RegExp(r'[M,H,V,L](((\d+\.\d+)|\d)([ ,])?)+|Z');
  List<RegExpMatch> results = regExp.allMatches(src).toList();
  double lastX = 0;
  double lastY = 0;
  results.forEach((RegExpMatch element) {
    String? op = element.group(0);
    if (op != null) {
      if (op.startsWith("M")) {
        List<String> pos = op.substring(1).split(RegExp(r'[, ]'));
        double dx = num.parse(pos[0]).toDouble();
        double dy = num.parse(pos[1]).toDouble();
        path.moveTo(dx, dy);
        lastX = dx;
        lastY = dy;
      }
      if (op.startsWith("L")) {
        List<String> pos = op.substring(1).split(RegExp(r'[, ]'));
        for (int i = 0; i < pos.length; i += 2) {
          double dx = num.parse(pos[i]).toDouble();
          double dy = num.parse(pos[i + 1]).toDouble();
          path.lineTo(dx, dy);
          lastX = dx;
          lastY = dy;
        }
      }
      if (op.startsWith("H")) {
        print(op.length);
        List<String> pos = op.substring(1).trim().split(RegExp(r'[, ]'));
        for (int i = 0; i < pos.length; i ++) {
          print('${pos[i]}');
          double dx = num.parse(pos[i]).toDouble();
          double dy = lastY;
          path.lineTo(dx, dy);
          lastX = dx;
        }
      }
      if (op.startsWith("V")) {
        List<String> pos = op.substring(1).trim().split(RegExp(r'[, ]'));
        for (int i = 0; i < pos.length; i ++) {
          double dx = lastX;
          double dy = num.parse(pos[i]).toDouble();
          path.lineTo(dx, dy);
          lastY = dy;
        }
      }
      if (op.startsWith("Z")) {
       path.close();
      }
    }
  });
  return path;
}

这样只要把三个路径画出来,就可以形成掘金的 logo ,如下所示:

【Flutter 绘制番外】svg 文件与绘制 (上)


这样 svg 文件通过 path 进行分割,遍历形成路径即可。注意一下,效果上来看,文字部分似乎不是很好,因为这里 只解析了 M、H、L、V 四个指令。仔细来看,这个文件中还有 C 指令用于形成贝塞尔曲线,这个指令在下一篇进行讲解,使用目前效果不好是正常的。下一章对 C 解析后就可以完美了。

【Flutter 绘制番外】svg 文件与绘制 (上)


3.颜色的解析

同样通过正则表达式匹配每条路径中的颜色信息,如下所示:

【Flutter 绘制番外】svg 文件与绘制 (上)

由于每条 svg 路径都有路径信息和颜色信息,使用可以定义一个 SVGPathResult 对象进行维护。

class SVGPathResult{
  final Color color;
  final Path path;

  SVGPathResult({required this.color, required this.path});
}

对颜色的解析逻辑如下:

  SVGPathResult formPathFromSvgOp(String src) {
    Color resultColor = Colors.black;
    RegExp color = RegExp(r'(?<=#)(.*)(?=")');
    List<RegExpMatch> colorResult = color.allMatches(src).toList();
    if(colorResult.length>0){
      String? colorStr = colorResult[0].group(0);
      if(colorStr!=null){
        print(colorStr);
        resultColor = Color(int.parse(colorStr, radix: 16) + 0xFF000000);
      }
    }

这样,之前写的 Flutter 图标的 svg 就可以解析涂色的,效果如下:

【Flutter 绘制番外】svg 文件与绘制 (上)


本文主要介绍了 H、V、L 三个绝对直线路径的使用以及正则解析,用于 FlutterPath 对象的形成。svg 中还有一些其他比较重要的操作符,我们将在接下来的文章中介绍,并对其进行解析。所以 千万别以为这点解析逻辑能解析任何 svg 文件 ,后续还需很多细节的完善。那本文就到这里,谢谢观看~