前言
上一篇《【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)
代码语言:javascript复制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
等,另外还有与大写字母相对于的小写字母表示相对路径
,这些都需要对解析逻辑进行拓展。那本篇就到这里,下篇再见,谢谢观看~