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

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

其实有了路径之后,就是绘制技能的事了,比如给个渐变色:
| 外框渐变 | 填充渐变 |
|---|---|
![]() |
![]() |
比如通过 shader 为绘制增加图片进行着色:

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

MaskFilter.blur(BlurStyle.inner, 10)
复制代码

MaskFilter.blur(BlurStyle.solid, 20)
复制代码
再比如说,有了路径,就可以通过 computeMetrics 完成如下路径绘制的动画。以前有人问过我这种效果如何实现,其实本质上就是路径的操作而已。但是并不是随便给个字就 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 文件中提取期望的数据。

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 来完成这项工作。
其中解析逻辑在上一篇中也介绍了一些,本文中会拓展 C 、Q 两个指令,只需要修改该方法内逻辑即可:

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

我们知道 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.画笔的设置
svg 的 path 节点下有 fill 属性表示填充, storke 表示线条。 这些是绘制中画笔Paint 的属性,所有需要根据这些属性来设置画笔:

如下,通过 extension 对 SVGPathResult 类进行拓展,给出 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 中的绘制
经过上面的解析和对 Path 以及 Paint 的处理,剩下的绘制工作就非常简单了。如下代码,解析完后,遍历 SVGPathResult 列表,生成路径,绘制即可。代码见【extra_02_svg/02】

---->[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);
}
});
复制代码
对显示进行效果处理,本质上是通过读画笔的 maskFilter 和 shader 进行设置。比如下面通过 shader ,使用一张图片进行着色,代码见 【extra_02_svg/03】

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】

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





















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)


![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)