SVG开源库解析

SVG开源库解析

背景

原生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常用类型

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补间动画使用问题 2023-03-10
SystemUI插件化流程分析 2023-03-15

评论区