SVG开源库解析

Android · 2023-03-14 · 830 人浏览

背景

原生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个关键代码

  1. 此处new 了一个 Picture对象,并在结束时将该对象传回,该对象包含最终要显示的drawable信息

  2. 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生成方法,从beginRecordingcanvas绘制,到endRecording完成picture对象的填充。

总结

使用类原生的方式进行svg解析,支持更多标签,并且仍然可以保证不会失真。
优点:
支持完整svg标签,效果和其他端显示效果一致
缺点:

  1. 不支持res资源引入的方式使用,只能java代码解析。
  2. 涉及输入输出流,存在性能问题。
android svg 开源库 学习笔记
Theme Jasmine by Kent Liao