likes
comments
collection
share

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

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

上一篇《【Flutter 绘制番外】svg 文件与绘制 (上)》中,我们对 H、V、L 三个 svg 指令做了介绍,并通过正则表达式进行解析,生成 Flutter 绘制中的 Path 路径。 本篇中将会介绍两个指令 CQ ,它们分别代表 三次贝塞尔曲线(cubic)二次贝塞尔曲线(quadratic) 。对这两个指令进行解析后,就可以让掘金的 svg 图标完美显示了:

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


一、为何要解析 svg ?

可能有人并不能理解,为什么你要把 svg 解析成 Flutter 中的 Path ? 那只能说,你还不了解在绘制中 Path 对象的地位。比如,有了 Path 就可以对绘制进行精细的控制,比如,绘制线框:

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


其实有了路径之后,就是绘制技能的事了,比如给个渐变色:

外框渐变填充渐变
【Flutter 绘制番外】svg 文件与绘制 (中)【Flutter 绘制番外】svg 文件与绘制 (中)

比如通过 shader 为绘制增加图片进行着色

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

或通过 maskFilter 添加 滤色,其实这些本质上都是属于绘制技能的范畴,和 svg 本身并没有太大关系。是 Path 对象让这并无关联的两者产生了交集。关于绘制的技能,在 《Flutter 绘制指南 - 妙笔生花》 中有详细介绍。

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

MaskFilter.blur(BlurStyle.inner, 10)

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

MaskFilter.blur(BlurStyle.solid, 20)

再比如说,有了路径,就可以通过 computeMetrics 完成如下路径绘制的动画。以前有人问过我这种效果如何实现,其实本质上就是路径的操作而已。但是并不是随便给个字就 Flutter 就能拿到路径的,让设计小姐姐用软件帮你设计对应文字的 svg 路径就行了,就像下面的 稀土掘金 一样:

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

其实 svg 本身是一个 记录信息 的静态文件,如果能够解析为Flutter 中的 Path 类对象,就可以有更大的应用空间。毕竟在一旦可以在代码中进行逻辑处理,就能产生无限的可能性。这就是为何要解析 svg 的必要性之一;另外还有两个好处:加深对 svg 文件的理解练习正则解析的能力


二、对 svg 解析的封装

上一篇中直接在画板类中对 svg 文件进行解析,这样无论是对于复用,还是维护拓展都是很不友好的。我们可以封装成一个类单独处理解析的逻辑。如下,定义 SVGPathResult 是解析每条路径的结果。包括路径字符串 path ,填充色 fillColor ,边线色 strokeColor 和 边线宽度 strokeWidth 。 在 SVGParser 中定义一个 parser 方法,解析 src 字符串,生成 SVGPathResult 列表:

class SVGPathResult {
  final String? path;
  final String? fillColor;
  final String? strokeColor;
  final String? strokeWidth;

  SVGPathResult({
    required this.path,
    this.fillColor,
    this.strokeColor,
    this.strokeWidth,
  });
}

class SVGParser {
  
  List<SVGPathResult?> parser(String src) {
    List<SVGPathResult?> result = [];
    // TODO 解析 svg 文件
    return result;
  }
}

1. svg 文件的解析

其实 svg 文件本身就是 xml 的一个子集,所以整体的结构可以通过 xml 解析器去解析,这里引入了 xml 包:

---->[pubspec.yaml]----
xml: ^5.3.1

对节点的解析也非常简单,XmlDocument 对象就是真个 xml 的文档树;findAllElements 方法可以查询子集某类标签。用该方法可以获取到所有的 path 节点,然后遍历节点,通过 getAttribute 方法获取需要的属性信息。这样就可以从 svg 文件中提取期望的数据。

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

List<SVGPathResult?> parser(String src) {
  List<SVGPathResult?> result = [];
  final XmlDocument document = XmlDocument.parse(src);
  XmlElement? root = document.getElement('svg');
  if (root == null) return result;
  List<XmlElement> pathNodes = root.findAllElements('path').toList();
  pathNodes.forEach((pathNode) {
    String? pathStr = pathNode.getAttribute('d');
    String? fillColor = pathNode.getAttribute('fill');
    String? strokeColor = pathNode.getAttribute('stroke');
    String? strokeWidth = pathNode.getAttribute('stroke-width');
    result.add(SVGPathResult(
      path: pathStr,
      fillColor: fillColor,
      strokeColor: strokeColor,
      strokeWidth: strokeWidth,
    ));
  });
  return result;
}

2. svg 路径的解析

可以看出 svg 文件的解析通过 xml 解析,并没有好费我们多大的心力。上面解析出的 path 是字符串,接下来就要面临把字符串解析成 Path 路径的问题了。这里我是希望这段逻辑可以单独抽离出来,所以定义了一个 SvgUtils 的类,通过静态方法 convertFromSvgPath 来完成这项工作。 其中解析逻辑在上一篇中也介绍了一些,本文中会拓展 CQ 两个指令,只需要修改该方法内逻辑即可:

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


要解析 CQ 两个指令,首先要明白它们是干嘛用的。如下所示 C 后面数字个数是 6 的倍数,表示三次贝塞尔曲线,也就是 控制点1控制点2终点 三组坐标。 Q 后面数字个数是 4 的倍数,表示二次贝塞尔曲线,也就是 控制点终点 两组坐标。

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

我们知道 Flutter 中的 cubicTo 方法是形成三次贝塞尔曲线路径的,其中刚好是 6 个入参,实际就是解析出数字,填进去就行了。

if (op.startsWith("C")) {
  List<String> pos = op.substring(1).trim().split(RegExp(r'[, ]'));
  for (int i = 0; i < pos.length; i += 6) {
    double p0x = num.parse(pos[i]).toDouble();
    double p0y = num.parse(pos[i + 1]).toDouble();
    double p1x = num.parse(pos[i + 2]).toDouble();
    double p1y = num.parse(pos[i + 3]).toDouble();
    double p2x = num.parse(pos[i + 4]).toDouble();
    double p2y = num.parse(pos[i + 5]).toDouble();
    path.cubicTo(p0x, p0y, p1x, p1y, p2x, p2y);
    lastX = p2x;
    lastY = p2y;
  }
}

同理, Flutter 中的 quadraticBezierTo 方法是形成二次贝塞尔曲线路径的,其中有是 4 个入参,也是解析出数字作为入参。这样将解析逻辑封装在 PathConvert#convertFromSvgPath 中,当需要拓展其他指令时,只要在这里修改即可。 svg 文件的解析交由 SVGParser 类处理,这样就能各司其职。

if (op.startsWith("Q")) {
  List<String> pos = op.substring(1).trim().split(RegExp(r'[, ]'));
  for (int i = 0; i < pos.length; i += 4) {
    double p0x = num.parse(pos[i]).toDouble();
    double p0y = num.parse(pos[i + 1]).toDouble();
    double p1x = num.parse(pos[i + 2]).toDouble();
    double p1y = num.parse(pos[i + 3]).toDouble();
    path.quadraticBezierTo(p0x, p0y, p1x, p1y);
    lastX = p1x;
    lastY = p1y;
  }
}

3.画笔的设置

svgpath 节点下有 fill 属性表示填充, storke 表示线条。 这些是绘制中画笔Paint 的属性,所有需要根据这些属性来设置画笔:

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

如下,通过 extensionSVGPathResult 类进行拓展,给出 setPaint 方法。根据自身属性为传入的画笔设置属性。

extension SetPaintBySVGPath on SVGPathResult{
  void setPaint(Paint paint){
    if (this.strokeColor != null) {
      paint..style = PaintingStyle.stroke;
      Color resultColor = Color(
          int.parse(this.strokeColor!.substring(1), radix: 16) + 0xFF000000);
      paint..color = resultColor;
    }
    if (this.strokeWidth != null) {
      paint..strokeWidth = num.parse(this.strokeWidth!).toDouble();
    }
    if (this.fillColor != null) {
      paint..style = PaintingStyle.fill;
      Color resultColor = Color(
          int.parse(this.fillColor!.substring(1), radix: 16) + 0xFF000000);
      paint..color = resultColor;
    }
  }
}

可能有人会问,为什么不直接在 SVGPathResult 中写这个方法,而是进行拓展呢?这里是想让 SVGPathResult纯粹 一些,只承担收录解析路径信息的职能,基于其上的功能可以让使用者自己拓展。 另外Paint 本身是 Flutter 中的类,需要运行在设备上起来才能调试,这样并不方便。不引入 Paint ,就可以让 SVGParser 脱离 Flutter 而存在,其中所用的都是 dart 语言本身的类,可以脱离 Flutter 运行。

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


三、解析结果在 Flutter 中的绘制

经过上面的解析和对 Path 以及 Paint 的处理,剩下的绘制工作就非常简单了。如下代码,解析完后,遍历 SVGPathResult 列表,生成路径,绘制即可。代码见【extra_02_svg/02】

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

---->[paint]----
List<SVGPathResult?> parserResults = svgParser.parser(src);
parserResults.forEach((SVGPathResult? result) {
  if (result == null) return;
  if (result.path != null) {
    Path path = SvgUtils.convertFromSvgPath(result.path!);
    result.setPaint(mainPaint);
    canvas.drawPath(path, mainPaint);
  }
});

对显示进行效果处理,本质上是通过读画笔的 maskFiltershader 进行设置。比如下面通过 shader ,使用一张图片进行着色,代码见 【extra_02_svg/03】

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

Matrix4 matrix4 = Matrix4.diagonal3Values(0.1, 0.1, 1)
    .multiplied(Matrix4.translationValues(70, 10, 0));

mainPaint.shader = ImageShader(
  img,
  TileMode.repeated,
  TileMode.repeated,
  matrix4.storage,
);

另外路径动画就是结合动画控制器和 computeMetrics 对路径进行测量,【extra_02_svg/05】

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

parserResults.forEach((SVGPathResult? result) {
  if (result == null) return;
  if (result.path != null) {
    Path path = SvgUtils.convertFromSvgPath(result.path!);
    result.setPaint(mainPaint);
    PathMetrics pms = path.computeMetrics();
    mainPaint.style = PaintingStyle.stroke;
    pms.forEach((pm) {
      canvas.drawPath(pm.extractPath(0, pm.length * progress.value), mainPaint);
    });
  }
});

掘金的 svg 只用到了这几个命令,看似比较完美,但是 svg 的命令可不止于此。还有其他的指令需要解析,比如 A、Q、T 等,另外还有与大写字母相对于的小写字母表示相对路径,这些都需要对解析逻辑进行拓展。那本篇就到这里,下篇再见,谢谢观看~