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

前言

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


一、为何要解析 svg ?

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


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

| 外框渐变 | 填充渐变 | | ------------------------------------------------------------ | ------------------------------------------------------------ | | image-20220319163545893 | |


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

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

dart MaskFilter.blur(BlurStyle.inner, 10)

dart MaskFilter.blur(BlurStyle.solid, 20)


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

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


二、对 svg 解析的封装

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

```dart 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 parser(String src) { List result = []; // TODO 解析 svg 文件 return result; } } ```


1. svg 文件的解析

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

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


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

dart 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 两个指令,只需要修改该方法内逻辑即可:


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

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

dart 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 类处理,这样就能各司其职。

dart 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 的属性,所有需要根据这些属性来设置画笔:

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

dart 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 中的绘制

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

dart ---->[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 ,使用一张图片进行着色,代码见 【extra02svg/03】

```dart 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 对路径进行测量,【extra02svg/05】

dart 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 等,另外还有与大写字母相对于的小写字母表示相对路径,这些都需要对解析逻辑进行拓展。那本篇就到这里,下篇再见,谢谢观看~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值