背景
原生SVG图片仅支持简单线条+填色,网页端的SVG或者说常规的SVG,支持颜色渐变及阴影相关效果,Android不支持。
网上搜索找到androidsvg-aar
支持SVG全标签解析,抱着学习态度分析实现逻辑。
导入
implementation 'com.caverock:androidsvg-aar:1.4'
使用
private void loadSvg() {
InputStream inputStream = null;
try {
inputStream = getAssets().open("loading.svg");
SVG svg = SVG.getFromInputStream(inputStream);
ImageView imageView = findViewById(R.id.svg_test);
imageView.setImageDrawable(new PictureDrawable(svg.renderToPicture()));
} catch (IOException e) {
e.printStackTrace();
} catch (SVGParseException e) {
e.printStackTrace();
}
}
源码分析
1. 从最终结果开始
imageView.setImageDrawable(new PictureDrawable(svg.renderToPicture()))
通过库中的renderToPicture()
方法,传入Picture
对象,最终转换为PictureDrawable
对象。
PictureDrawable
本质上还是Drawable
,正如同常规SVG图片,其类型对应的为VectorDrawable
既然是Drawable
对象,那么其使用方法共通,如绘制canvas,设置背景等。
2. PictureDrawable
https://www.jianshu.com/p/245294f98354
在Picture中进行绘制(类似于canvas画布,后面会说),最终转换为Drawable对象
3. 读取文件
inputStream = getAssets().open("loading.svg");
SVG svg = SVG.getFromInputStream(inputStream);
根据源码,可以看出第一步是打开输出流,读取文件信息。
注:涉及输入输出流,尽量在子线程处理,否则容易出现卡顿。
3.1 读取文件数据
3.1.1 准备输出流
准备构建输出流,解析xml资源
public static SVG getFromInputStream(InputStream is) throws SVGParseException{
SVGParser parser = new SVGParser();
return parser.parse(is, enableInternalEntities);
}
SVG parse(InputStream is, boolean enableInternalEntities) throws SVGParseException{
// 省略构建InputStream相关数据
...
// 解析SVG相关数据
parseUsingXmlPullParser(is, enableInternalEntities);
...
return svgDocument;
}
3.1.2 解析xml
省略case中的代码,按照xml正常解析方式解析各种标签
private void parseUsingXmlPullParser(InputStream is, boolean enableInternalEntities) throws SVGParseException{
try{
XmlPullParser parser = Xml.newPullParser();
XPPAttributesWrapper attributes = new XPPAttributesWrapper(parser);
parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, false);
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
parser.setInput(is, null);
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT){
switch(eventType) {
case XmlPullParser.START_DOCUMENT:
case XmlPullParser.START_TAG:
case XmlPullParser.END_TAG:
case XmlPullParser.TEXT:
case XmlPullParser.CDSECT:
case XmlPullParser.DOCDECL:
case XmlPullParser.PROCESSING_INSTRUCTION:
}
eventType = parser.nextToken();
}
endDocument();
}
3.1.3 xml标签解析
下面省略标签类型解析,针对某一个circle
类型简单分析
这里解析标签数据,基本上按照xml的树结构,照样子构建了java对象中的树结构一样,把xml中的信息解析成对象及属性的模式。
private void startElement(String uri, String localName, String qName, Attributes attributes) throws SVGParseException{
switch (elem){
case circle:
circle(attributes); break;
}
}
private void circle(Attributes attributes) throws SVGParseException{
debug("<circle>");
if (currentElement == null)
throw new SVGParseException("Invalid document. Root element must be <svg>");
SVG.Circle obj = new SVG.Circle();
obj.document = svgDocument;
obj.parent = currentElement;
parseAttributesCore(obj, attributes);
parseAttributesStyle(obj, attributes);
parseAttributesTransform(obj, attributes);
parseAttributesConditional(obj, attributes);
parseAttributesCircle(obj, attributes);
currentElement.addChild(obj);
}
到这里,基本可以看出 SVG svg = SVG.getFromInputStream(inputStream);
这里只做了解析xml成SVG对象的操作,将SVG文件中的标签转换成JAVA对象存储,并没有实际转换成Picture
。
3.2 生成Picture对象
3.2.1 解析宽高和option属性
无参方法最终走到该方法,省略部分判断宽高逻辑
public Picture renderToPicture(RenderOptions renderOptions){
Box viewBox = (renderOptions != null && renderOptions.hasViewBox()) ? renderOptions.viewBox: rootElement.viewBox;
if (renderOptions != null && renderOptions.hasViewPort()){
float w = renderOptions.viewPort.maxX();
float h = renderOptions.viewPort.maxY();
return renderToPicture( (int) Math.ceil(w), (int) Math.ceil(h), renderOptions );
}
}
3.2.2 生成picture对象
可以看到有2个关键代码
-
此处new 了一个
Picture
对象,并在结束时将该对象传回,该对象包含最终要显示的drawable
信息 -
从
Picture
中获取到canvas
对象,最终的绘制都将在canvas
中进行,类似于在onDraw
时进行的绘制public Picture renderToPicture(int widthInPixels, int heightInPixels, RenderOptions renderOptions){ Picture picture = new Picture(); Canvas canvas = picture.beginRecording(widthInPixels, heightInPixels); if (renderOptions == null || renderOptions.viewPort == null) { renderOptions = (renderOptions == null) ? new RenderOptions() : new RenderOptions(renderOptions); renderOptions.viewPort(0f, 0f, (float) widthInPixels, (float) heightInPixels); } SVGAndroidRenderer renderer = new SVGAndroidRenderer(canvas, this.renderDPI); renderer.renderDocument(this, renderOptions); picture.endRecording(); return picture; }
3.2.3 遍历,准备绘制canvas
前后逻辑基本都是在做属性校验,关键位置在
render(rootObj, viewPort, viewBox, preserveAspectRatio)
这一行代码。void renderDocument(SVG document, RenderOptions renderOptions){ ... // Initialise the state resetState(); checkXMLSpaceAttribute(rootObj); // Save state statePush(); Box viewPort = new Box(renderOptions.viewPort); // If root element specifies a width, then we need to adjust our default viewPort that was based on the canvas size if (rootObj.width != null) viewPort.width = rootObj.width.floatValue(this, viewPort.width); if (rootObj.height != null) viewPort.height = rootObj.height.floatValue(this, viewPort.height); // !!重点!! // Render the document render(rootObj, viewPort, viewBox, preserveAspectRatio); // Restore state statePop(); if (renderOptions.hasCss()) document.clearRenderCSSRules(); }
这里开始遍历对象数据,取出xml树状结构中的子元素进行绘制
private void render(SVG.Svg obj, Box viewPort, Box viewBox, PreserveAspectRatio positioning){ // 省略校验,根据属性进行画布平移缩放等逻辑 ... renderChildren(obj, true); ...
}
开始根据类型进行真正的绘制,这里可以看到这里支持很多,例如渐变,文字等svg常用类型
```java
private void renderChildren(SvgContainer obj, boolean isContainer) {
if (isContainer) {
parentPush(obj);
}
for (SVG.SvgObject child: obj.getChildren()) {
render(child);
}
if (isContainer) {
parentPop();
}
}
// 绘制子元素
private void render(SVG.SvgObject obj){
if (obj instanceof SVG.Svg) {
render((SVG.Svg) obj);
} else if (obj instanceof SVG.Use) {
render((SVG.Use) obj);
} else if (obj instanceof SVG.Switch) {
render((SVG.Switch) obj);
} else if (obj instanceof SVG.Group) {
render((SVG.Group) obj);
} else if (obj instanceof SVG.Image) {
render((SVG.Image) obj);
} else if (obj instanceof SVG.Path) {
render((SVG.Path) obj);
} else if (obj instanceof SVG.Rect) {
render((SVG.Rect) obj);
} else if (obj instanceof SVG.Circle) {
render((SVG.Circle) obj);
} else if (obj instanceof SVG.Ellipse) {
render((SVG.Ellipse) obj);
} else if (obj instanceof SVG.Line) {
render((SVG.Line) obj);
} else if (obj instanceof SVG.Polygon) {
render((SVG.Polygon) obj);
} else if (obj instanceof SVG.PolyLine) {
render((SVG.PolyLine) obj);
} else if (obj instanceof SVG.Text) {
render((SVG.Text) obj);
}
}
3.2.4 绘制canvas
其中一个方法进行举例,可以看到最终调用到了canvas.drawPath
方法,底层还是使用了canvas进行绘制
private void render(SVG.Circle obj){
//省略
...
Path path = makePathAndBoundingBox(obj);
updateParentBoundingBox(obj);
checkForGradientsAndPatterns(obj);
checkForClipPath(obj);
boolean compositing = pushLayer();
if (state.hasFill)
doFilledPath(obj, path);
if (state.hasStroke)
doStroke(path);
if (compositing)
popLayer(obj);
}
// 其中一个绘制举例
private void doFilledPath(SvgElement obj, Path path){
// First check for pattern fill. It requires special handling.
if (state.style.fill instanceof SVG.PaintReference){
SVG.SvgObject ref = document.resolveIRI(((SVG.PaintReference) state.style.fill).href);
if (ref instanceof SVG.Pattern) {
SVG.Pattern pattern = (SVG.Pattern)ref;
fillWithPattern(obj, path, pattern);
return;
}
}
// Otherwise do a normal fill
canvas.drawPath(path, state.fillPaint);
}
3.2.5 完成picture对象生成
Picture picture = new Picture();
Canvas canvas = picture.beginRecording(widthInPixels, heightInPixels);
renderer.renderDocument(this, renderOptions);
picture.endRecording();
这是一套完整的picture生成方法,从beginRecording
到 canvas
绘制,到endRecording
完成picture对象的填充。
总结
使用类原生的方式进行svg解析,支持更多标签,并且仍然可以保证不会失真。
优点:
支持完整svg标签,效果和其他端显示效果一致
缺点:
- 不支持res资源引入的方式使用,只能java代码解析。
- 涉及输入输出流,存在性能问题。
本文由 bt 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。