在文章《矢量图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的只支持
方案一:将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="眼镜:\n红包:" />
//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适合用于简单的应用图标及图文混排图标。