矢量图SVG安卓进阶

发布于:2025-08-03 ⋅ 阅读:(15) ⋅ 点赞:(0)

在文章《矢量图Vector安卓详解》中,我们了解到Android只支持Vector矢量图,且必须在项目中预置Vector文件,不支持直接从外部加载Vector文件,也不支持直接加载SVG文件。

但在实际项目中客户端可能需要使用服务端下发的矢量图,如换肤icon,节日主题icon,活动icon等。服务端下发矢量图需要支持多端展示,不会选择下发Vector文件,毕竟只有Android支持Vector,且Vector不支持文本,文本路径,样式和遮罩等,只能绘制path图形。

由于H5和iOS对SVG文件的原生支持较好,服务端选择统一下发SVG文件会更合适。

为了支持服务端下发SVG矢量图和较为复杂的矢量图,Android项目就不能只支持系统的Vector矢量图,还得支持SVG矢量图。下面文章会分别介绍Android直接显示SVG文件的常见方案。

01

AndroidSVG库

AndroidSVG 是一个 Android 的 SVG 解析器和渲染器,它是一个开源库,几乎完全支持 SVG 1.1 和 SVG 1.2 Tiny 规范中的静态视觉标签(滤镜除外),但不支持SVG中的动画标签。可以从其开源代码(https://github.com/BigBadaboom/androidsvg/)中,查看支持的所有SVG标签,如下代码所示:

private enum  SVGElem
{
   svg,//顶部标签
   a,
   circle,//圆
   clipPath,
   defs,
   desc,
   ellipse,//椭圆
   g,//group标签
   image,
   line,
   linearGradient,
   marker,
   mask,
   path,//path标签
   pattern,
   polygon,//多边形
   polyline,
   radialGradient,
   rect,//矩形
   solidColor,
   stop,
   style,
   SWITCH,
   symbol,
   text,
   textPath,
   title,
   tref,
   tspan,
   use,
   view,
   UNSUPPORTED;
}

可以看出AndroidSVG支持了大部分的SVG标签,没有支持任何动画标签。AndroidSVG库加载SVG静态图的使用方式比较简单,下面举例说明。

首先在 build.gradle 文件中添加 AndroidSVG 依赖,代码如下:

dependencies {
    implementation 'com.caverock:androidsvg:1.4'
}

其次将 SVG 文件test2.svg,放入 res/raw/ 目录中。

最后在 ImageView 中显示SVG 文件,其代码如下:

public class MainActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        final ImageView imageView4Svg =  findViewById(R.id.imageView4svg);
        try {
          SVG testSvg = SVG.getFromResource(this.getResources(), R.raw.test2);
          imageView4Svg.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
          imageView4Svg.setImageDrawable(new PictureDrawable(testSvg.renderToPicture()));
        } catch (SVGParseException e) {
          Log.d(TAG, e.getMessage());
        } 
    }
}

我们也可以直接使用封装好的控件SVGImageView加载SVG图片,代码如下:

final SVGImageView svgImageView = findViewById(R.id.svgImageView);
//方式一:直接设置资源id
svgImageView.setImageResource(R.raw.test2); 


//方式二:先获取svg对象,再设置给控件。适合需要多处使用svg对象的场景
SVG testSvg = SVG.getFromResource(this.getResources(), R.raw.test2);
svgImageView.setSVG(testSvg);

我们还可以在layout xml中给SVGImageView控件直接配置svg图片。但这需要我们对AndroidSVG进行源码引用,因为通过maven下载的库,只有androidsvg-1.4.jar,无法使用其R.styleable.SVGImageView_svg属性。

先将最新版Release 1.4源码(https://github.com/BigBadaboom/androidsvg/releases)下载到本地,然后我们通过import module把源码中androidsvg目录加入到sample项目,并在app模块中引用它,代码如下:

//settings.gradle
include ':androidsvg'


//app.gradle
dependencies {
    implementation(project(':androidsvg'))
}


//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="top|center_horizontal"
              android:paddingTop="10dp"
              android:background="#00FFFF">


    <ImageView
        android:id="@+id/imageView4svg"
        android:layout_width="200dp"
        android:layout_height="200dp" />


    <com.caverock.androidsvg.SVGImageView
        android:id="@+id/svgImageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:svg="@raw/test2"/>


</LinearLayout>

如果导入的androidsvg后出现编译问题,可将build.gradle中对publish.gradle的引用移除,相关编译参数根据本地编译环境进行相应适配即可。

通过上面几个步骤,SVGImageView就能成功显示SVG图片了,运行sample,效果显示如下:

下面从源码分析进一步说明AndroidSVG库对SVG矢量图的绘制原理。

首先,SVG支持从Asset,Resource,String,InputStream中,通过SVGParser.parse方法把SVG文件转换成SVG对象,其代码如下所示:

// ~/androidsvg/src/main/java/com/caverock/androidsvg/SVG.java
public static SVG  getFromAsset(AssetManager assetManager, String filename) throws SVGParseException, IOException
{
   SVGParser    parser = new SVGParser();
   InputStream  is = assetManager.open(filename);
   try {
      return parser.parse(is, enableInternalEntities);
   } finally {
      try {
        is.close();
      } catch (IOException e) {
        // Do nothing
      }
   }
}


public static SVG  getFromResource(Resources resources, int resourceId) throws SVGParseException{
   SVGParser    parser = new SVGParser();
   InputStream  is = resources.openRawResource(resourceId);
   try {
      return parser.parse(is, enableInternalEntities);
   } finally {
      try {
        is.close();
      } catch (IOException e) {
        // Do nothing
      }
   }
}


//~/androidsvg/src/main/java/com/caverock/androidsvg/SVGParser.java
SVG  parse(InputStream is, boolean enableInternalEntities) throws SVGParseException{
   //......
      parseUsingXmlPullParser(is, enableInternalEntities);
    //......
}


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:
              //创建SVG对象
              startDocument();
              break;
           case XmlPullParser.START_TAG:
              String qName = parser.getName();
              if (parser.getPrefix() != null)
                 qName = parser.getPrefix() + ':' + qName;
              //解释xml中的每个标签,存储在SVG对象中,
              startElement(parser.getNamespace(), parser.getName(), qName, attributes);
              break;
           case XmlPullParser.END_TAG:
              qName = parser.getName();
              if (parser.getPrefix() != null)
                 qName = parser.getPrefix() + ':' + qName;
              endElement(parser.getNamespace(), parser.getName(), qName);
              break;
           case XmlPullParser.TEXT:
              int[] startAndLength = newint[2];
              char[] text = parser.getTextCharacters(startAndLength);
              text(text, startAndLength[0], startAndLength[1]);
              break;
           case XmlPullParser.CDSECT:
              text(parser.getText());
              break;
        // ......
        }
     }
     //....
   }
}


private void startDocument(){ //解释文档标签时,创建SVG对象
   SVGParser.this.svgDocument = new SVG();
}


private void startElement(String uri, String localName, String qName, Attributes attributes) throws SVGParseException{
   //......
   String tag = (localName.length() > 0) ? localName : qName;
   SVGElem  elem = SVGElem.fromString(tag);
   switch (elem){
      case svg:
         svg(attributes); break;//解释<svg>标签
      case g:
      case a: 
         g(attributes); break;//解释<g>分组标签
      case defs:
         defs(attributes); break;
      case use:
         use(attributes); break;
      case path:
         path(attributes); break;//解释<path>路径标签
      case rect:
         rect(attributes); break;解释<rect>矩形标签
      case circle:
         circle(attributes); break;
      case ellipse:
         ellipse(attributes); break;
      //......
   }
}

上面代码通过SVGParser.parse将SVG文件转换成一个SVG对象。SVGParser中在startDocument是创建SVG对象,在startElement中分发处理SVG的所有标签。不同标签的处理,见下面源码所示:

//~/androidsvg/src/main/java/com/caverock/androidsvg/SVGParser.java
private void  svg(Attributes attributes) throws SVGParseException{
   SVG.Svg  obj = new SVG.Svg();//创建SVG标签对应对象
   obj.document = svgDocument;
   obj.parent = currentElement;//<svg>作为最外层标签,此时currentElement为空。
   parseAttributesCore(obj, attributes);
   parseAttributesStyle(obj, attributes);
   parseAttributesConditional(obj, attributes);
   parseAttributesViewBox(obj, attributes);
   parseAttributesSVG(obj, attributes);
   if (currentElement == null) {
      svgDocument.setRootElement(obj);
   } else {
      currentElement.addChild(obj);
   }
   currentElement = obj;//将SVG.Svg 做为当前currentElement,即成为根节点。
}


private void  path(Attributes attributes) throws SVGParseException{
   //......
   SVG.Path  obj = new SVG.Path();//创建path对象
   obj.document = svgDocument;
   obj.parent = currentElement;//将当前currentElement作为parent
   parseAttributesCore(obj, attributes);
   parseAttributesStyle(obj, attributes);
   parseAttributesTransform(obj, attributes);
   parseAttributesConditional(obj, attributes);
   parseAttributesPath(obj, attributes);
   currentElement.addChild(obj); //将自己作为currentElement的child 
}


//SVG的所有标签,对应的类都继承自SvgObject,都有一个parent
staticclass SvgObject{
   SVG           document;
   SvgContainer  parent;
   //......
}


//SVG的所有容器标签,对应的类都继承自SvgContainer,有一个List<SvgObject>的chilren列表
interface SvgContainer{
   List<SvgObject>  getChildren();
   void             addChild(SvgObject elem) throws SVGParseException;
}


staticabstractclass SvgConditionalContainer extends SvgElement implements SvgContainer, SvgConditional{
   List<SvgObject>  children = new ArrayList<>();
    //......
   @Override
   public List<SvgObject>  getChildren() { return children; }
   @Override
   public void addChild(SvgObject elem) throws SVGParseException  { children.add(elem); }
    //......
}

SVGParser处理<svg>标签时,会创建SVG.Svg对象,作为根节点。然后将解释出的下一层标签加入到根节点的子节点。SVG类中所有标签对应的类都继承SvgObject类,根节点和所有中间节点都继承自SvgContainer类。最后SVG对象会形成一棵SvgObject树,根节点是SVG.Svg类,中间节点是SvgContainer的子类,叶节点是SvgObject的子类。

把SVG对象设置给SVGImageView控件时,会通过SVG.renderToPicture方法把SVG渲染成Android系统的Picture对象,并使用Android系统的PictureDrawable对象把Picture转成了Drawable的子类对象,其代码如下所示:

//~/androidsvg/src/main/java/com/caverock/androidsvg/SVGImageView.java
public void  setSVG(SVG svg, String css)
{
   if (svg == null)
      thrownew IllegalArgumentException("Null value passed to setSVG()");


   this.svg = svg;
   this.renderOptions.css(css);


   doRender();
}


private void  doRender(){
   if (svg == null)
      return;
   Picture  picture = this.svg.renderToPicture(renderOptions);
   setSoftwareLayerType();//svg包含复杂的矢量图形,如果用默认的硬解可能有兼容问题
   setImageDrawable(new PictureDrawable(picture));//调用android/widget/ImageView.java的setImageDrawable方法
}

SVG对象渲染成Android系统的Picture对象,则是由SVGAndroidRenderer负责,它通过循环遍历SVG树的所有元素,将元素都绘制到Picture的canvas上,其源码如下:

// ~/androidsvg/src/main/java/com/caverock/androidsvg/SVG.java
//renderToPicture(renderOptions)会调用此方法,通过SVGAndroidRenderer绘制Picture的canvas。
public Picture  renderToPicture(int widthInPixels, int heightInPixels, RenderOptions renderOptions){
   Picture  picture = new Picture();
   Canvas   canvas = picture.beginRecording(widthInPixels, heightInPixels);
    //......
   SVGAndroidRenderer  renderer = new SVGAndroidRenderer(canvas, this.renderDPI);
   renderer.renderDocument(this, renderOptions);
   picture.endRecording();
   return picture;
}


//~/androidsvg/src/main/java/com/caverock/androidsvg/SVGAndroidRenderer.java
void  renderDocument(SVG document, RenderOptions renderOptions){
   SVG.Svg  rootObj = document.getRootElement();
   //......
   render(rootObj, viewPort, viewBox, preserveAspectRatio);
    //......
}
private void render(SVG.Svg obj, Box viewPort, Box viewBox, PreserveAspectRatio positioning){
   //......
   renderChildren(obj, true);
//......
}


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);
   } elseif (obj instanceof SVG.Use) {
      render((SVG.Use) obj);
   } elseif (obj instanceof SVG.Switch) {
      render((SVG.Switch) obj);
   } elseif (obj instanceof SVG.Group) {
      render((SVG.Group) obj);//对于container,还会调用到renderChildren
   } elseif (obj instanceof SVG.Image) {
      render((SVG.Image) obj);
   } elseif (obj instanceof SVG.Path) {
      render((SVG.Path) obj);//绘制path
   } elseif (obj instanceof SVG.Rect) {
      render((SVG.Rect) obj);//绘制矩形
   } elseif (obj instanceof SVG.Circle) {
      render((SVG.Circle) obj);//绘制圆
   } elseif (obj instanceof SVG.Ellipse) {
      render((SVG.Ellipse) obj);
   } elseif (obj instanceof SVG.Line) {
      render((SVG.Line) obj);
   } elseif (obj instanceof SVG.Polygon) {
      render((SVG.Polygon) obj);
   } elseif (obj instanceof SVG.PolyLine) {
      render((SVG.PolyLine) obj);
   } elseif (obj instanceof SVG.Text) {
      render((SVG.Text) obj);//绘制文本
   }
    //......
}


private void render(SVG.Path obj){
   //......
   if (obj.transform != null)
      canvas.concat(obj.transform); //运用变换属性


   Path  path = (new PathConverter(obj.d)).getPath();
   //......


   if (state.hasFill) {
      path.setFillType(getFillTypeFromState());
      doFilledPath(obj, path); //绘制path和填充
   }
   if (state.hasStroke)
      doStroke(path);//绘制边框
//......
}


private void doFilledPath(SvgElement obj, Path path){
   //......
   canvas.drawPath(path, state.fillPaint);//绘制path并填充
}

ImageView以及ImageView的子类SVGImageView,支持显示Drawable的子类对象,会在控件onDraw方法调用PictureDrawable的draw方法。

02

Glide加载SVG

通过AndroidSVG开源库,可以成功加载出静态SVG图片,但是它不支持直接加载服务端下发的SVG文件Url。

AndroidSVG库需要先把SVG文件下载下来,然后将文件读取到InputStream,再通过SVG.getFromInputStream(InputStream is)方法转换出SVG对象,最后再将SVG对象设置给SVGImageView 控件,过程比较繁琐。

而图片加载库Glide解决了这个问题,其最新版本的示例中已经包含了SVG图片部分,详情可参考 Glide-SVG Sample(https://github.com/bumptech/glide/tree/master/samples/svg)。

首先我们需要把sample中的4个文件导入到项目:

这几个类把SVG转换成Drawable,引入到Glide中。SVG文件的加载依然使用上面介绍的AndroidSVG库。这几个Glide适配类,源码如下:

public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {


@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
    returntrue;
  }


public Resource<SVG> decode(@NonNull InputStream source, int width, int height, @NonNull Options options) throws IOException {
    try {
      SVG svg = SVG.getFromInputStream(source);//SVG文件转换为SVG对象
      if (width != SIZE_ORIGINAL) {
        svg.setDocumentWidth(width);
      }
      if (height != SIZE_ORIGINAL) {
        svg.setDocumentHeight(height);
      }
      returnnew SimpleResource<>(svg);//返回SVG的封装对象
    } catch (SVGParseException ex) {
      thrownew IOException("Cannot load SVG from stream", ex);
    }
  }
}


publicclass SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(
      @NonNull Resource<SVG> toTranscode, @NonNull Options options) {
    SVG svg = toTranscode.get();
    Picture picture = svg.renderToPicture();//SVG对象转绘到Picture对象
    PictureDrawable drawable = new PictureDrawable(picture);//返回Drawable子类
    returnnew SimpleResource<>(drawable);
  }
}


publicclass SvgSoftwareLayerSetter implements RequestListener<PictureDrawable> {


@Override
public boolean onLoadFailed(
      GlideException e, Object model, @NonNull Target<PictureDrawable> target, boolean isFirstResource) {
    ImageView view = ((ImageViewTarget<?>) target).getView();
    view.setLayerType(ImageView.LAYER_TYPE_NONE, null);//恢复硬解
    returnfalse;
  }


@Override
public boolean onResourceReady(
      @NonNull PictureDrawable resource,
      @NonNull Object model,
      Target<PictureDrawable> target,
      @NonNull DataSource dataSource,
      boolean isFirstResource) {
    ImageView view = ((ImageViewTarget<?>) target).getView();
    view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);//设置软解
    returnfalse;
  }
}


@GlideModule
publicclass SvgModule extends AppGlideModule {//通过注解把解码器注册到Glide中
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
    registry //注册转码器,解码器
        .register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
        .append(InputStream.class, SVG.class, new SvgDecoder());
  }


@Override
public boolean isManifestParsingEnabled() {
    returnfalse;
  }
}

其次,在 build.gradle 文件中添加新版glide依赖,代码如下:

dependencies {
    implementation("com.github.bumptech.glide:glide:4.16.0")
    annotationProcessor("com.github.bumptech.glide:compiler:4.14.2")
    implementation("com.github.bumptech.glide:annotations:4.16.0")
    implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
}

最后在 ImageView 中显示SVG 文件,其代码如下:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ImageView imageView4glide = findViewById(R.id.imageView4glide);
        ImageView imageView4glide2 = findViewById(R.id.imageView4glide2);


        //初始化Glide的RequestBuilder,需要设置软解SvgSoftwareLayerSetter
        RequestBuilder<PictureDrawable> requestBuilder = Glide.with(this)
                        .as(PictureDrawable.class)
                        .placeholder(R.drawable.placeholder)
                        .error(R.drawable.ic_error)
                        .transition(withCrossFade())
                        .listener(new SvgSoftwareLayerSetter());
        
        //加载本地资源
        Uri uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
                                + "://"+ getPackageName()+ "/"+ R.raw.test2);
        requestBuilder.load(uri).into(imageView4glide);
        
        //加载网络资源
        Uri uriNet = Uri.parse("https://www.w3.org/TR/SVG11/images/shapes/polygon01.svg");
        requestBuilder.load(uriNet).into(imageView4glide2);
    }
}

从示例代码可以看到,使用Glide加载SVG,能够非常丝滑地让已有项目支持服务端下发的SVG图Url的加载,示例的显示效果如下:

03

Android-PathView库

Android-Pathview也是一个开源库,它依赖AndroidSVG库对SVG文件进行解码,所以也支持所有SVG的静态视觉标签。除此之外,它还支持path路径动画,但支持的动画非常简单,且与SVG协议定义的动画效果无关。

Android-Pathview最新版本1.0.8(https://github.com/geftimov/android-pathview/releases)集成的是AndroidSVG库的1.2.1版本,不支持通过Glide加载图片。我们可以下载Android-PathView的源码(https://github.com/geftimov/android-pathview)分析其动画原理,下面结合代码示例来进行说明。

首先在布局在加入PathView控件,代码如下:

<com.eftimoff.androipathview.PathView
    android:id="@+id/pathView"
    android:layout_width="150dp"
    android:layout_height="150dp"
    app:pathColor="@android:color/white"
    app:svg="@raw/monitor"
    app:pathWidth="5dp"/>

接着在页面中启动path动画,代码如下:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    final PathView pathView_2 = findViewById(R.id.pathView_2);
    pathView_2.setFillAfter(true);//动画完后进行填充。设置false则只绘制path部分
    pathView_2.useNaturalColors();//绘制path时,是否用SVG中的颜色属性
    pathView_2.getPathAnimator().duration(3000).start();
}

示例可以看到,控件会先将SVG中的path进行增量绘制,path动画播放完毕后,再填充path内部颜色和其它非path元素,其动画效果如下图所示:

在Android-Pathview源码中可以看到它引入AndroidSVG库,并包含另外2个类,如下:

  • PathView类:继承自View,负责动画创建和path动画绘制;

  • SvgUtils类:作为工具类,将AndroidSVG库的功能封装提供给PathView使用,包括SVG对象的加载和用于path路径绘制的相关功能。

PathView创建时,会从属性中获取SVG的资源ID存放到svgResourceId,然后在onSizeChanged中异步加载对应的SVG对象,再将SVG对象中的path转存一份到PahtView的paths变量中,用于后续做path动画,其代码如下:

//~/android-pathview/src/main/java/com/eftimoff/androipathview/PathView.java
public PathView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    paint.setStyle(Paint.Style.STROKE);
    getFromAttributes(context, attrs);
}


private void getFromAttributes(Context context, AttributeSet attrs) {
    final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PathView);
    try {
        if (a != null) {
            paint.setColor(a.getColor(R.styleable.PathView_pathColor, 0xff00ff00));
            paint.setStrokeWidth(a.getDimensionPixelSize(R.styleable.PathView_pathWidth, 8));
            //获取svg对应的资源id
            svgResourceId = a.getResourceId(R.styleable.PathView_svg, 0);
            naturalColors = a.getBoolean(R.styleable.PathView_naturalColors, false);
            fill = a.getBoolean(R.styleable.PathView_fill,false);
            fillColor = a.getColor(R.styleable.PathView_fillColor,Color.argb(0,0,0,0));
        }
    } 
    //......
}


protected void onSizeChanged(final int w, final int h, int oldw, int oldh) {
    //......  
    if (svgResourceId != 0) {
        mLoader = new Thread(new Runnable() {
            @Override
            public void run() {//异步加载svgResourceId对应的SVG对象,保存到svgUtils.mSvg中
                svgUtils.load(getContext(), svgResourceId);
                synchronized (mSvgLock) {
                    width = w - getPaddingLeft() - getPaddingRight();
                    height = h - getPaddingTop() - getPaddingBottom();
                    //将svgUtils.mSvg对象中的所有path,转存一份到控件的paths变量中
                    paths = svgUtils.getPathsForViewport(width, height);
                    updatePathsPhaseLocked();//根据progress绘制paths的长度
                }
            }
        }, "SVG Loader");
        mLoader.start();
    }
}

当PathView播放动画时,会先创建一个PathView的percentage属性动画,然后不断调用setPercentage方法更新PathView的progress属性,其代码如下:

//~/android-pathview/src/main/java/com/eftimoff/androipathview/PathView.java
public AnimatorBuilder getPathAnimator() {
    if (animatorBuilder == null) {
        animatorBuilder = new AnimatorBuilder(this);
    }
    return animatorBuilder;
}


public AnimatorBuilder(final PathView pathView) {
    anim = ObjectAnimator.ofFloat(pathView, "percentage", 0.0f, 1.0f);
}


public void start() {
    anim.setDuration(duration);
    anim.setInterpolator(interpolator);
    anim.setStartDelay(delay);
    anim.start();
}


//ObjectAnimator会根据插值器计算值,回调PathView的setPercentage方法
public void setPercentage(float percentage) {
    if (percentage < 0.0f || percentage > 1.0f) {
        thrownew IllegalArgumentException("setPercentage not between 0.0f and 1.0f");
    }
    progress = percentage; //更新path动画进度
    synchronized (mSvgLock) {
        updatePathsPhaseLocked();//根据progress绘制paths的长度
    }
    invalidate();
}

当PathView的progress改变时,会调用updatePathsPhaseLocked方法,根据progress遍历测量paths中的每一个path长度,其代码如下:

//~/android-pathview/src/main/java/com/eftimoff/androipathview/PathView.java
private void updatePathsPhaseLocked() {
    finalint count = paths.size();
    for (int i = 0; i < count; i++) {//遍历所有path
        SvgUtils.SvgPath svgPath = paths.get(i);
        svgPath.path.reset();
        //将path长度,根据progress计算需要绘制的path长度。
        svgPath.measure.getSegment(0.0f, svgPath.length * progress, svgPath.path, true);
        svgPath.path.rLineTo(0.0f, 0.0f);
    }
}


public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
    //......
    //通过native方法设置绘制长度startD, stopD
    return native_getSegment(native_instance, startD, stopD, dst.mutateNI(), startWithMoveTo);
}

当下一帧绘制时,path就会按照上面设置好的长度进行绘制。

动画结束后,如果fillAfter设置为true,会通过AndroidSVG库的renderToCanvas方法绘制出整个SVG图片。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);


    //......
        final int count = paths.size();
        for (int i = 0; i < count; i++) {
            final SvgUtils.SvgPath svgPath = paths.get(i);
            final Path path = svgPath.path;
            final Paint paint1 = naturalColors ? svgPath.paint : paint;
            mTempCanvas.drawPath(path, paint1);//遍历绘制裁剪后的path
        }


        fillAfter(mTempCanvas);//绘制SVG静态图
    //......
}


private void fillAfter(final Canvas canvas) {
    //progress=1时动画结束,如果支持填充,才会绘制完整的SVG图
    if (svgResourceId != 0 && fillAfter && Math.abs(progress - 1f) < 0.00000001) {
        svgUtils.drawSvgAfter(canvas, width, height);
    }
}


public void drawSvgAfter(final Canvas canvas, final int width, final int height) {
    final float strokeWidth = mSourcePaint.getStrokeWidth();
    rescaleCanvas(width, height, strokeWidth, canvas);
}


private void rescaleCanvas(int width, int height, float strokeWidth, Canvas canvas) {
    //......
    mSvg.renderToCanvas(canvas);//最终还是使用AndroidSVG库的方法进行绘制
}

上面示例的效果如下图1,如果不设置fillAfter则效果如下图2,如果也不设置NaturalColors则效果如下图3,我们可以对比它们的区别:

从源码和示例效果,可以发现Android-PathView库只是结合ObjectAnimator,对SVG中的path进行了一个裁剪绘制,从而产生出动画效果,并非支持SVG的动画标签,所以其动画效果比较简单。

04

自定义SvgDrawable

上面三个方案,都不能像Android的AnimatedVectorDrawable一样,支持每个path和group的属性动画,如果能够复用VectorDrawable,AnimatedVectorDrawable的设计来支持SVG中的矢量图和动画,是不是就一举两得了呢?

如果根据项目需求,约定好SVG的只支持 <path>,<group> 等少量静态标签, <animate> 属性动画标签和 <animateTransform> 补间动画标签,然后将SVG静态标签转换成系统支持的VectorDrawable对象,将SVG动画标签转换成系统支持的AnimatedVectorDrawable对象,就可以支持显示服务端SVG矢量图了。下面对两种设计思路进行尝试。

方案一:将SVG文件转换成Android的vector文件和animated-vector文件,再通过android系统的VectorDrawable,AnimatedVectorDrawable类进行加载,实现SVG图片和动画。示例代码如下:

public class SvgDrawable extends VectorDrawable {
    //以res/raw/目录中的svg为例
    public static SvgDrawable fromRawId(Context context,Resources resources, int rawResId) {
         try { 
                // 1. 首先读取SVG文件并解析为DOM树
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = factory.newDocumentBuilder();
                InputStream svgStream = resources.openRawResource(rawResId); // 假设SVG存储在raw目录
                Document doc = builder.parse(svgStream);
    
                // 2. 获取SVG根元素
                Element svgElement = doc.getDocumentElement();
    
                // 3. 创建对应的vector XML
                StringBuilder vectorXml = new StringBuilder();
                vectorXml.append("<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n");
    
                // 4. 转换SVG属性到vector属性
                String width = svgElement.getAttribute("width");
                String height = svgElement.getAttribute("height");
                String viewBox = svgElement.getAttribute("viewBox");
                String[] viewBoxValues = viewBox.split(" ");
    
                vectorXml.append("    android:width=\"").append(width).append("dp\"\n");
                vectorXml.append("    android:height=\"").append(height).append("dp\"\n");
                vectorXml.append("    android:viewportWidth=\"").append(viewBoxValues[2]).append("\"\n");
                vectorXml.append("    android:viewportHeight=\"").append(viewBoxValues[3]).append("\" >\n");
    
                // 5. 处理SVG子元素, 使用SvgDrawable的静态方法
                SvgDrawable.processChildren(svgElement, vectorXml, "    ");
    
                // 6. 关闭对应的vector tag
                vectorXml.append("</vector>");
    
                return SvgDrawable.create(getApplicationContext(), vectorXml.toString())
            }catch (Exception e) {
                Log.w(TAG, e);
            }
    
            returnnull;
        }
    }


    public static SvgDrawable create(Context context,String vectorXml) {
        XmlPullParser vectorParser = null;
        SvgDrawable drawable = new SvgDrawable();


        // 1. 将生成的vector XML解析并传递给父类的inflate方法
        XmlPullParser vectorParser = Xml.newPullParser();
        vectorParser.setInput(new java.io.StringReader(vectorXml.toString()));


        // 2. 继续调用父类(即VectorDrawable类)的inflate方法解析SVG
        drawable.inflate(resources, vectorParser, Xml.asAttributeSet(vectorParser), null);
        return drawable;
    }
    //......
}




//获取res/raw/test.svg文件对应的Drawable对象
SvgDrawable svg1 = SvgDrawable.fromRawId(getApplicationContext(), getResources(), R.raw.test)

实现流程不复杂,编译通过,但是运行时报错,错误信息如下:

FATAL EXCEPTION: main
  //......
  Caused by: java.lang.ClassCastException: android.util.XmlPullAttributes cannot be cast to android.content.res.XmlBlock$Parser
    at android.content.res.Resources.obtainAttributes(Resources.java:1927)
    at android.graphics.drawable.Drawable.obtainAttributes(Drawable.java:1624)
    at android.graphics.drawable.VectorDrawable.inflate(VectorDrawable.java:735)
    at com.eftimoff.sample.SvgDrawable.create(SvgDrawable.java:120)
    at com.eftimoff.sample.SvgDrawable.fromRawId(SvgDrawable.java:92)
    at com.eftimoff.sample.SecondActivity.onCreate(SecondActivity.java:177)

这个错误原因是,VectorDrawable中只支持解释res目录中的非raw资源,这些资源在编译过程中会被编译处理,而res/raw中的资源只是生成资源id,并不会被编译处理。俩者获取元素的方式不同,无法复用。VectorDrawable获取XmlPullParser的源码如下:

public class VectorDrawable extends Drawable {
    public static VectorDrawable create(Resources resources, int rid) {
        //......
        final XmlPullParser parser = resources.getXml(rid);
        final AttributeSet attrs = Xml.asAttributeSet(parser);
       
        final VectorDrawable drawable = new VectorDrawable();
        drawable.inflate(resources, parser, attrs);
        
        return null;
    }
    //res
}

VectorDrawable的XmlPullParser通过Resource.getXml(rid)获取,返回的是XmlBlock.Parser对象,它继承自XmlResourceParser,专门用于解释经过编译优化处理过的资源。

而自定义SvgDrawable的XmlPullParser通过Xml.newPullParser()获取,返回的是KXmlParser对象,只能处理原xml文件。

由于VectorDrawable没有对文件类型的vector xml的支持,所以无法复用其去加载SVG文件。

既然VectorDrawable不支持未编译的vector xml,那么是否可以直接把SVG文件解释到VectorDrawable,AnimatedVectorDrawable对象呢?下面方案二继续这个尝试。

方案二:仿照VectorDrawable,AnimatedVectorDrawable实现SVGDrawable,AnimatedSVGDrawable,重写vector res的解释方法,直接将SVG文件中的属性映射到VectorDrawable的成员变量中,示例代码如下:

public class SvgDrawable2 {
    public static SvgDrawable2 create(Context context,String svgXml) {
        try {
            // 1. 解析SVG文件字符串为DOM树
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            InputStream svgStream = new ByteArrayInputStream(svgXml.getBytes());
            Document doc = builder.parse(svgStream);
            svgElementRoot = doc.getDocumentElement();


            VectorDrawable vectorDrawable = new VectorDrawable();


            // 2. 获取SVG的基本属性
            float width = parseFloatAttribute(svgElementRoot, "width", 24);
            float height = parseFloatAttribute(svgElementRoot, "height", 24);
            String viewBox = svgElementRoot.getAttribute("viewBox");
            float[] viewBoxValues = parseViewBox(viewBox);


            // 通过反射访问mVectorState
            Field vectorStateField = VectorDrawable.class.getDeclaredField("mVectorState");
            vectorStateField.setAccessible(true);
            // 获取vectorStateField字段的值
            Object vectorStateValue = vectorStateField.get(vectorDrawable);


            // 3. 处理SVG的根组
            // 通过setViewportSize方法并调用
            Method setViewportSizeMethod = vectorStateValue.getClass().getDeclaredMethod("setViewportSize", float.class, float.class);
            setViewportSizeMethod.setAccessible(true);
            setViewportSizeMethod.invoke(vectorStateValue, viewBoxValues[2], viewBoxValues[3]);
            
            //......
            return vectorDrawable;
        } catch (Exception e) {
            Log.e(TAG, "Error parsing SVG", e);
        }
        
        returnnull;
    }
    //......
}


//获取res/raw/test.svg文件对应的Drawable对象
String svgXml = "<?xml version=\"1.0\" standalone=\"no\"?>\n" +
        "<svg width=\"100\" height=\"100\" viewBox=\"0 0 24 24\" version=\"1.1\"  xmlns=\"http://www.w3.org/2000/svg\">\n" +
        "  <path d=\"M20,20 l 0,4 4,0 0,-4 -4,0z\" fill=\"#0000FF\" />\n" +
        "</svg>";


SvgDrawable2 svg2 = SvgDrawable2.create(getApplicationContext(), svgXml)

编译通过后,运行时报错,错误信息如下:

Error parsing SVG
  java.lang.NoSuchFieldException: No field mVectorState in class Landroid/graphics/drawable/VectorDrawable; (declaration of 'android.graphics.drawable.VectorDrawable' appears in /system/framework/framework.jar)
 at java.lang.Class.getDeclaredField(Native Method)
 at com.svg.sample.SvgDrawable2.inflate(SvgDrawable2.java:102)
 at com.svg.sample.SvgDrawable2.create(SvgDrawable2.java:49)
 at com.eftimoff.sample.SecondActivity.onCreate(SecondActivity.java:178)

提示在VectorDrawable中找不到mVectorState字段。但是单步时我们能从调试面板看到mVectorState变量,如下图所示:

为了再次确认,我们将VectorDrawable的所有字段和方法都打印出来,代码如下:

//返回类声明的所有字段(包括公共、保护、默认、私有字段,但不包括父类字段)
Field[] fields = VectorDrawable.class.getDeclaredFields();
for (Field field : fields) {
    System.out.println(field.getName());
}


//返回类声明的所有公共字段,(包括父类字段)
Field[] fields2 = VectorDrawable.class.getFields();
for (Field field : fields2) {
    System.out.println(field.getName());
}


//返回类声明的所有方法(包括公共、保护、默认、私有方法,但不包括从父类或父接口继承的方法)
Method[] mothods = VectorDrawable.class.getDeclaredMethods();
for (Method mothod : mothods) {
    System.out.println(mothod.getName());
}


//返回类的所有公共方法(包括从父类或父接口继承的公共方法)
Method[] mothods2 = VectorDrawable.class.getMethods();
for (Method mothod : mothods2) {
    System.out.println(mothod.getName());
}

从调试面板看这些字段,发现确实没有mVectorState字段,只有一个mTintFilter字段,如下图所示:

最后这个问题没用解决,所以方案二也没有实现VectorDrawable的复用。另外方案二需要大量使用反射操作,实现本身比较复杂,也没有达到高效复用的目标。对于自定义SvgDrawable复用VectorDrawable方案遇到的问题,也欢迎知道原因或解法的同学能给我留言反馈。

当然,我们也可以像PathView一样,尝试扩展AndroidSVG库,把SVG动画标签映射到Android的ObjectAnimation对象,从而实现对SVG动画的支持,本文就不做展开说明。

05

矢量字体库:iconfont

矢量字体也不是基于像素绘制,而是基于数学公式或向量数据进行绘制,所有也支持无损缩放,无论放大还是缩小,字体都能保持清晰和光滑。

iconfont 运用矢量字体和SVG矢量图原理相同的特点,将SVG矢量图转换成Android支持的ttf格式矢量字体库来使用,具体说明可参考其官网:iconfont官网(https://www.iconfont.cn/)。下面我们通过示例来说明其使用方式。

使用iconfont需要先在网站上制作SVG矢量图字体库。首先登入iconfont网站,从资源管理菜单中创建自己的项目“test”。然后将自己的svg图片上传到项目中,或从网站的svg图片库中挑选图片添加到项目中。

完成图片添加后,将项目下载到本地,解压项目zip文件后可以看我们需要的文件,如下图所示:

先将图中红框标出的矢量字库ttf拷贝到Android项目的assert目录下,然后打开红框标出的html网页,查看各矢量图对应的字符编码,如下图所示:

有了字符编码,我们就可以像使用文字一样加载SVG矢量图了,示例代码如下:

//布局文件中,将svg编码赋值到text属性
<TextView
    android:id="@+id/cjf_ttf"
    android:layout_width="300dp"
    android:layout_height="200dp"
    android:gravity="top|center_horizontal"
    android:textSize="30sp"
    android:text="眼镜:&#xe8ab;\n红包:&#xe8b0;" />


//Activity的onCreate中,将svg字体库设置给对应控件中
TextView textViewTtf = findViewById(R.id.cjf_ttf);
Typeface iconfont = Typeface.createFromAsset(getAssets(), "iconfont.ttf");
textViewTtf.setTypeface(iconfont);

转成字体库后,就可以像使用文字一样加载SVG矢量图了,也可以像文字一样去动态调整大小和颜色,其示例显示效果如下:

06

总结

SVG矢量图功能非常强大,现在只有浏览器支持全功能的 SVG 渲染器,其它平台要么像Android一样,支持一个比SVG更简单的矢量图格式Vector,要么像AndroidSVG库一样只支持部分SVG标签功能。

常见方案大都对SVG的动画支持较少,比如,Android Vector仅支持属性动画,PathView仅支持path路径动画。如果需要支持复杂的动画特效,更推荐使用Lottie动画。

最后我们对Android矢量图和矢量动画的各种方案进行总结和梳理,如下:

  • VectorDrawable:直接支持Android平台,标签简单,入门快,适合简单图形;

  • AndroidSVG: 支持完整的SVG标准,能够处理复杂的SVG图形,提供更丰富的样式和效果,所以性能上比VectorDrawable略逊一筹,但可以通过glide便捷地加载服务端SVG文件;

  • IconFont: 可以通过字体文件实现SVG矢量图标,能够在不同分辨率下保持清晰度,还可以和文字混排。它适合图标类的简单图形,无法处理复杂的图形和颜色渐变。需要依赖官网制作字体库;

  • AnimatedVectorDrawable:直接集成在Android中,支持简单的矢量动画,易于使用,动画效果相对简单;

  • PathView: 复用AndroidSVG库,增加了SVG路径动画功能,但动画效果最为简单;

  • Lottie: 支持复杂的动画效果,能够导入Adobe After Effects制作的动画,效果丰富且灵活。文件体积相对较大,性能会差一些。

在选择矢量图和矢量动画方案时,需要根据项目的需求,复杂性和性能要求进行权衡。对于简单的本地图形和动画可以选择VectorDrawable和Animation Vector,对于复杂的图形和动画可以选择AndroidSVG和Lottie,而IconFont适合用于简单的应用图标及图文混排图标。


网站公告

今日签到

点亮在社区的每一天
去签到