在之前分享的文章《矢量图SVG应用探索》中,我们知道了SVG作为一个矢量图形标准,不仅包括了矢量图形,还包含文字,图片,滤镜,字体,链接,脚本,CSS和动画等功能,这让SVG渲染器的开发变得过于复杂。
现在只有浏览器支持全功能的 SVG 渲染器(不同浏览器可能有兼容问题),其它SVG渲染器要么只支持部分功能,要么支持一个比SVG更简单的矢量图格式,比如Android的Vector。
Android对矢量图的支持做了大量简化,不再支持脚本,互动,复杂样式和动画,只保留了少量的元素和属性支持,并使用了一套新标签来描述矢量图,也支持无限缩放都保持清晰度的特性,其官方说明可参考:Android矢量可绘制对象概览(https://developer.android.google.cn/develop/ui/views/graphics/vector-drawable-resources)。
01 矢量图VectorDrawable
Android中使用VectorDrawable 定义静态矢量图,它与 SVG 格式类似,都是xml格式,但它的文件后缀不是svg,而是xml。
VectorDrawable文件为树状层次结构,其根元素为<vector>标签,支持的标签很少,主要包括:<group>, <path> 和 <clipPath>,其中最常用且最基本的标签是<path>。
1.1 元素和属性
VectorDrawable支持的标签比较少,每个标签支持的属性也比SVG少。通过系统的attrs.xml文件,可以查看到所有VectorDrawable的元素和属性定义,如下代码所示:
<!-- Drawable used to draw vector paths. -->
<declare-styleable name="VectorDrawable">
<!-- If set, specifies the color to apply to the drawable as a tint. By default,
no tint is applied. May be a color state list. -->
<attr name="tint" />
<!-- When a tint color is set, specifies its Porter-Duff blending mode. The
default value is src_in, which treats the drawable as an alpha mask. -->
<attr name="tintMode" />
<!-- Indicates if the drawable needs to be mirrored when its layout direction is
RTL (right-to-left). -->
<attr name="autoMirrored" />
<!-- The intrinsic width of the Vector Drawable. -->
<attr name="width" />
<!-- The intrinsic height of the Vector Drawable. -->
<attr name="height" />
<!-- The width of the canvas the drawing is on. -->
<attr name="viewportWidth" format="float"/>
<!-- The height of the canvas the drawing is on. -->
<attr name="viewportHeight" format="float"/>
<!-- The name of this vector drawable. -->
<attr name="name" />
<!-- The opacity of the whole vector drawable, as a value between 0
(completely transparent) and 1 (completely opaque). -->
<attr name="alpha" />
<!-- Left optical inset. -->
<attr name="opticalInsetLeft" format="dimension" />
<!-- Top optical inset. -->
<attr name="opticalInsetTop" format="dimension" />
<!-- Right optical inset. -->
<attr name="opticalInsetRight" format="dimension" />
<!-- Bottom optical inset. -->
<attr name="opticalInsetBottom" format="dimension" />
</declare-styleable>
<!-- Defines the group used in VectorDrawables. -->
<declare-styleable name="VectorDrawableGroup">
<!-- The name of this group. -->
<attr name="name" />
<!-- The amount to rotate the group. -->
<attr name="rotation" />
<!-- The X coordinate of the center of rotation of a group. -->
<attr name="pivotX" />
<!-- The Y coordinate of the center of rotation of a group. -->
<attr name="pivotY" />
<!-- The amount to translate the group on X coordinate. -->
<attr name="translateX" format="float"/>
<!-- The amount to translate the group on Y coordinate. -->
<attr name="translateY" format="float"/>
<!-- The amount to scale the group on X coordinate. -->
<attr name="scaleX" />
<!-- The amount to scale the group on X coordinate. -->
<attr name="scaleY" />
</declare-styleable>
<!-- Defines the path used in VectorDrawables. -->
<declare-styleable name="VectorDrawablePath">
<!-- The name of this path. -->
<attr name="name" />
<!-- The width a path stroke. -->
<attr name="strokeWidth" format="float" />
<!-- The color to stroke the path if not defined implies no stroke. -->
<attr name="strokeColor" format="color" />
<!-- The opacity of a path stroke, as a value between 0 (completely transparent)
and 1 (completely opaque). -->
<attr name="strokeAlpha" format="float" />
<!-- The color to fill the path if not defined implies no fill. -->
<attr name="fillColor" format="color" />
<!-- The alpha of the path fill, as a value between 0 (completely transparent)
and 1 (completely opaque). -->
<attr name="fillAlpha" format="float" />
<!-- The specification of the operations that define the path. -->
<attr name="pathData" format="string" />
<!-- The fraction of the path to trim from the start from 0 to 1. -->
<attr name="trimPathStart" format="float" />
<!-- The fraction of the path to trim from the end from 0 to 1 . -->
<attr name="trimPathEnd" format="float" />
<!-- Shift trim region (allows visible region to include the start and end) from 0 to 1. -->
<attr name="trimPathOffset" format="float" />
<!-- sets the linecap for a stroked path. -->
<attr name="strokeLineCap" format="enum">
<enum name="butt" value="0"/>
<enum name="round" value="1"/>
<enum name="square" value="2"/>
</attr>
<!-- sets the lineJoin for a stroked path. -->
<attr name="strokeLineJoin" format="enum">
<enum name="miter" value="0"/>
<enum name="round" value="1"/>
<enum name="bevel" value="2"/>
</attr>
<!-- sets the Miter limit for a stroked path. -->
<attr name="strokeMiterLimit" format="float"/>
<!-- sets the fillType for a path. It is the same as SVG's "fill-rule" properties.
For more details, see https://www.w3.org/TR/SVG/painting.html#FillRuleProperty. -->
<attr name="fillType" format="enum">
<enum name="nonZero" value="0"/>
<enum name="evenOdd" value="1"/>
</attr>
</declare-styleable>
1.2 基本图形
Android没有支持SVG中预定义的形状标签,因为所有的形状都可以通过path绘制出来。
简单的形状,我们可以手动设置其path。复杂的形状,我们也可以借助SVG Path编辑器绘制path。本文绘制的path都是使用这款在线,免费,操作简单的SVG Path编辑器(https://yqnn.github.io/svg-path-editor/)绘制的。
通过上面的属性定义可以知道,<path>
下面的test1.xml文件,展示了一些基本图形的绘制方法:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<!-- Group 标签用于将多个路径形状组合在一起,并可以对它们应用变换 -->
<group
android:name="icon_group"
android:translateX="0"
android:translateY="0"
android:rotation="0">
<!-- Path 标签用于描述矢量路径 -->
<!-- 三角形 -->
<path
android:name="triangle"
android:fillColor="#00FF00"
android:pathData="M0,0 L0,10 L10,0z" />
<!-- 正方形 -->
<path
android:name="square"
android:fillColor="#00FF00"
android:pathData="M10,0 L20,0 L20,10 L10,10 Z" />
<!-- 圆形 -->
<path
android:name="circle"
android:fillColor="#00FF00"
android:pathData="M0,18 A5,5 0 1,1 10,18 A5,5 0 1,1 0,18 Z" />
<!-- 五角星 -->
<path
android:name="star"
android:fillColor="#00FF00"
android:pathData="M16,13 L17,17 21,17 18,19 20,23 16, 20 12,23 14,19 11,17 15,17 Z" />
</group>
<!-- 参考图形 -->
<path
android:name="triangle2"
android:fillColor="#FF0000"
android:pathData="M0,30 L0,40 L10,30Z" />
</vector>
元素<path>
将上面的文件拷贝到 projectpath/src/main/res/drawable/test1.xml目录,可以在Android Studio的资源预览窗口中查看,其效果如下:

1.3 图形变换
通过attrs.xml文件中的属性定义可以发现,<group>标签支持旋转,平移和缩放变换,而<path>
下面的test2.xml示例中,修改了test1.xml中<group>的变换属性和<path>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<!-- Group 标签用于将多个路径形状组合在一起,并可以对它们应用变换 -->
<group
android:name="icon_group"
android:translateX="10"
android:translateY="0"
android:rotation="10">
<!-- Path 标签用于描述矢量路径 -->
<!-- 三角形 -->
<path
android:name="triangle"
android:fillColor="#00FF00"
android:strokeColor="#FF0000"
android:strokeWidth="0.5"
android:pathData="M0,0 L0,10 L10,0z" />
<!-- 正方形 -->
<path
android:name="square"
android:fillColor="#00FFFF"
android:strokeColor="#FF0000"
android:strokeWidth="0.5"
android:pathData="M10,0 L20,0 L20,10 L10,10 Z" />
<!-- 圆形 -->
<path
android:name="circle"
android:fillColor="#FFFF00"
android:strokeColor="#FF0000"
android:strokeWidth="0.5"
android:pathData="M0,18 A5,5 0 1,1 10,18 A5,5 0 1,1 0,18 Z" />
<!-- 五角星 -->
<path
android:name="star"
android:fillColor="#0000FF"
android:strokeColor="#000000"
android:strokeWidth="0.5"
android:pathData="M16 13 17 17 21 17 18 19 20 23 16 20 12 23 14 19 11 17 15 17Z" />
</group>
<!-- 参考图形 -->
<path
android:name="triangle2"
android:fillColor="#FF0000"
android:pathData="M0,30 L0,40 L10,30Z" />
</vector>
在Android Studio的资源预览窗口中查看修改后的test2.xml,可以看到<group>中的所有图形都进行了10度的旋转,每个<path>

由此也可以看出<group>标签的作用就是对图形组合进行变换,所以不需要变换的<path>
1.4 渐变色
VectorDrawable中有2个色彩属性:fillColor和strokeColor。上面的例子中,所有图形都设置了单色值,但其实fillColo和strokeColor都支持渐变色。
下面的test3.xml中,展示fillColor的线性渐变和放射状渐变的颜色设置方法:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:name="oval1"
android:strokeColor="#ff0000"
android:strokeWidth="2"
android:pathData="M10,60 a30,40 0 1,0 80,0 a30,40 0 1,0 -80,0Z" >
<aapt:attr name="android:fillColor">
<gradient
android:startX="0"
android:startY="0"
android:endX="0"
android:endY="100"
android:type="linear">
<item android:offset="0" android:color="#FF0000"/>
<item android:offset="1" android:color="#0000FF"/>
</gradient>
</aapt:attr>
</path>
<path
android:name="oval2"
android:strokeColor="#ff0000"
android:strokeWidth="2"
android:pathData="M100,60 a30,40 0 1,0 80,0 a30,40 0 1,0 -80,0Z" >
<aapt:attr name="android:fillColor">
<gradient
android:centerX="100"
android:centerY="60"
android:gradientRadius="80"
android:type="radial">
<item android:offset="0" android:color="#FF0000"/>
<item android:offset="1" android:color="#0000FF"/>
</gradient>
</aapt:attr>
</path>
</vector>
上面例子中通过<aapt:attr>来设置渐变色,<aapt:attr>是 Android 资源编译工具AAPT提供的一个特殊标签,用于在 XML 文件中定义或引用自定义属性,需要我们在头部引入命名空间xmlns:aapt="http://schemas.android.com/aapt"。当然也可以把gradient渐变色定义在单独的xml文件里,然后通过drawable id来引用渐变色,如下:
<!-- res/drawable/gradient_oval1.xml -->
<gradient
xmlns:android="http://schemas.android.com/apk/res/android"
android:startX="0"
android:startY="0"
android:endX="0"
android:endY="100"
android:type="linear">
<item android:offset="0" android:color="#FF0000"/>
<item android:offset="1" android:color="#0000FF"/>
</gradient>
<!-- res/drawable/gradient_oval2.xml -->
<gradient
android:centerX="100"
android:centerY="60"
android:gradientRadius="80"
android:type="radial">
<item android:offset="0" android:color="#FF0000"/>
<item android:offset="1" android:color="#0000FF"/>
</gradient>
<!-- res/drawable/test4.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:name="oval1"
android:fillColor="@drawable/gradient_oval1"
android:strokeColor="#ff0000"
android:strokeWidth="2"
android:pathData="M10,60 a30,40 0 1,0 80,0 a30,40 0 1,0 -80,0Z" >
</path>
<path
android:name="oval2"
android:fillColor="@drawable/gradient_oval2"
android:strokeColor="#ff0000"
android:strokeWidth="2"
android:pathData="M100,60 a30,40 0 1,0 80,0 a30,40 0 1,0 -80,0Z" >
</path>
</vector>
在Android Studio的资源预览窗口中查看test3.xml和修改后的test4.xml,可以看见oval1的填充色从上到下由红色线性变色为蓝色,而oval2的填充色以顶点(100,60)为中心点,开始由红色发射状渐变为蓝色,其效果如下:

如果把fillColor设置为透明色,把渐变色赋值给strokeColor,我们发现轮廓线也可以变成渐变色,效果图如下:

1.5 clipPath
<clipPath>定义当前绘制的剪切路径,对当前的 group 和子 group 有效。通过系统的attrs.xml文件,我们能查到<clipPath>只有两个属性,Name和PathData,其声明如下:
<!-- Defines the clip path used in VectorDrawables. -->
<declare-styleable name="VectorDrawableClipPath">
<!-- The Name of this path. -->
<attr name="name" />
<!-- The specification of the operations that define the path. -->
<attr name="pathData"/>
</declare-styleable>
下面的test5.xml中,展示clip-path的图形裁剪设置方法:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<group>
<!-- clip-path 标签用于描述裁剪的矢量路径 -->
<clip-path
android:name="clip1"
android:pathData="M0,0 h100 v80 h-100 Z" />
<path
android:name="oval1"
android:fillColor="#0000ff"
android:strokeColor="#ff0000"
android:strokeWidth="2"
android:pathData="M10,60 a30,40 0 1,0 80,0 a30,40 0 1,0 -80,0Z" />
<!-- clip-path 用在 path 标签后, 不能裁剪上面的path图形 -->
<!-- <clip-path-->
<!-- android:name="clip1"-->
<!-- android:pathData="M0,0 h100 v80 h-100 Z" />-->
</group>
</vector>
<clip-path>必须放在用于裁剪<path>的

1.6 SVG导入
除了手写Android矢量图Vector文件,我们还可以使用Android Studio的Vector Asset创建工具使用系统自带的Vector,或从本地SVG中转换获取Vector。
通过project板块中右键->New->Vector Asset,或者菜单File->New->Vector Asset路径,我们可以打开Vector Asset创建工具,如下图:

选择 Clip art 类型,就可以从系统自带的Vector选择我们需要的图形,如下图所示:

选择完图形后,我们还可以设置中的大小,颜色和透明度属性,其效果可以在工具中预览,如下:

生成的baseline_add_reaction_24.xml文件,vector的alpha,tint,width和height等属性都设置了对应的值,代码如下:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:alpha="0.3"
android:tint="#FF0000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
android:height="24dp">
<path android:fillColor="@android:color/white"
android:pathData="M18,9V7h-2V2.84C14.77,2.3 13.42,2 11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12c0,-1.05 -0.17,-2.05 -0.47,-3H18zM15.5,8C16.33,8 17,8.67 17,9.5S16.33,11 15.5,11S14,10.33 14,9.5S14.67,8 15.5,8zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM12,17.5c-2.33,0 -4.31,-1.46 -5.11,-3.5h10.22C16.31,16.04 14.33,17.5 12,17.5zM22,3h2v2h-2v2h-2V5h-2V3h2V1h2V3z"/>
</vector>
在Android Studio的资源预览窗口中查看baseline_add_reaction_24.xml,其效果如下:

如果选择Local file类型,可以选择本地的SVG文件转换成Vector文件,如下图所示:

SVG文件转换为Vector文件时,如果SVG中只有Path标签,Vector Asset工具可以直接显示出SVG的预览效果。如果包括了其它标签,Vector Asset工具不能预览SVG的效果,转换后的文件可能有些问题,需要我们手工修正后使用。
我们以《矢量图SVG应用探索》中基本图形的SVG文件test2.svg为例,其代码如下:
<?xml version="1.0" standalone="no"?>
<svg width="250" height="150" viewBox="0 0 250 150"
version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="60" height="40"
stroke="black" fill="transparent" stroke-width="5"/>
<rect x="80" y="10" rx="10" ry="10" width="40" height="40"
stroke="black" fill="transparent" stroke-width="5"/>
<circle cx="150" cy="30" r="20"
stroke="red" fill="transparent" stroke-width="5"/>
<ellipse cx="200" cy="30" rx="20" ry="5"
stroke="red" fill="transparent" stroke-width="5"/>
<line x1="10" x2="50" y1="70" y2="110"
stroke="orange" stroke-width="5"/>
<polyline points="50 70 55 80 60 75 65 90 70 85 75 100 80 95 85 110 90 105"
stroke="orange" fill="transparent" stroke-width="5"/>
<polygon points="130 65 135 85 150 85 140 95 145 110 130 100 115 110 120 95 110 85 125 85"
stroke="green" fill="transparent" stroke-width="5"/>
<path d="M164 80Q183 42 200 80T232 80"
fill="none" stroke="blue" stroke-width="5"/>
</svg>
在浏览器打开test2.svg 文件,我们可以看到如下图形:

通过Android Studio的Vector Asset工具选择test.svg,因为svg中包含了非<path>

创建完成后,我们导出test6.xml文件,内容如下:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="150dp"
android:width="250dp"
android:viewportHeight="150"
android:viewportWidth="250">
<path android:fillColor="transparent"
android:pathData="M10,10h60v40h-60z"
android:strokeColor="#000000"
android:strokeWidth="5"/>
<path android:fillColor="transparent"
android:pathData="M90,10L110,10A10,10 0,0 1,120 20L120,40A10,10 0,0 1,110 50L90,50A10,10 0,0 1,80 40L80,20A10,10 0,0 1,90 10z"
android:strokeColor="#000000"
android:strokeWidth="5"/>
<path android:fillColor="transparent"
android:pathData="M150,30m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
android:strokeColor="#ff0000"
android:strokeWidth="5"/>
<path android:fillColor="transparent"
android:strokeWidth="5"
android:pathData="M180,30a20,5 0,1 0,40 0a20,5 0,1 0,-40 0z"
android:strokeColor="#ff0000" />
<path android:fillColor="#FF000000"
android:pathData="M10,70L50,110"
android:strokeColor="#ffa500"
android:strokeWidth="5"/>
<path android:fillColor="transparent"
android:pathData="M50,70l5,10l5,-5l5,15l5,-5l5,15l5,-5l5,15l5,-5"
android:strokeColor="#ffa500"
android:strokeWidth="5"/>
<path android:fillColor="transparent"
android:pathData="M130,65l5,20l15,0l-10,10l5,15l-15,-10l-15,10l5,-15l-10,-10l15,0z"
android:strokeColor="#008000"
android:strokeWidth="5"/>
<path android:fillColor="#00000000"
android:pathData="M164,80Q183,42 200,80T232,80"
android:strokeColor="#0000ff"
android:strokeWidth="5"/>
</vector>
从转换后的Vector文件可以看出,SVG中的基本形状元素,都被转换成了<path>

但是,转换后的vector文件编译时输出了一个错误提示,如下:projectpath/src/main/res/drawable/test6.xml:10: AAPT: error: 'transparent' is incompatible with attribute fillColor (attr) color.
我们把所有android:fillColor="transparent"改为android:fillColor="#00000000",可以编译运行成功。
由此可知,Android Studio的SVG转换功能比较弱,我们可以借助第三方工具进行转换,它们的转换能力可能更强大。当然,上面的基本图形都比较简单,也可以通过这款SVG Path编辑器(https://yqnn.github.io/svg-path-editor/)快速画出来。
1.7 使用矢量图
上面介绍了VectorDrawable对应的Vector文件的编写和转换方法,下面继续介绍如何在Android布局中使用VectorDrawable矢量图。
下面的test7.xml中,展示ImageView设置矢量图test_baterry2的方法:
<?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="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="top|left"
android:paddingTop="10dp"
android:background="#00FFFF">
<ImageView
android:id="@+id/img_10"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/test_baterry2"
android:layout_marginRight="10dp"
android:layout_marginLeft="10dp"/>
<ImageView
android:id="@+id/img_11"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@drawable/test_baterry2" />
</LinearLayout>
上面布局在App中可以查看到同一个Vector矢量图显示出的2个电池图标,一个是Vector的原尺寸10dp*20dp的电池图标,一个是放大到100dp*200dp的电池图标,2个图标显示都清晰无锯齿,如下图所示:

1.8 源码分析
了解了Vector的编写和使用,那么VectorDrawable矢量图的实现原理是什么呢?下面继续从源码来分析其实现原理。
首先ImageView从属性src中获取矢量图resourceId对应的Drawable对象,并设置给mDrawable变量,其代码如下:
///~/Android/sdk/sources/android-34/android/widget/ImageView.java
public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initImageView();
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
saveAttributeDataForStyleable(context, R.styleable.ImageView,
attrs, a, defStyleAttr, defStyleRes);
//从属性列表TypedArray的ImageView_src获取图片
final Drawable d = a.getDrawable(R.styleable.ImageView_src);
if (d != null) {
setImageDrawable(d);
}
//......
}
继续通过TypedArray.getDrawable来获取矢量图Drawable,其代码如下:
///~/Android/sdk/sources/android-34/android/content/res/TypedArray.java
public Drawable getDrawable(@StyleableRes int index) {
return getDrawableForDensity(index, 0);
}
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
if (mRecycled) {
thrownew RuntimeException("Cannot make calls to a recycled instance!");
}
final TypedValue value = mValue;
if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
//......
//从资源类mResources加载图片
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
returnnull;
}
继续跟踪代码,可以看到从Resources, ResourcesImpl,Drawable,DrawableInflater几个类的调用流程中,最终把矢量图对象VectorDrawable从xml文件解释中创建出来,代码如下:
//~/Android/sdk/sources/android-34/android/content/res/Resources.java
Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
throws NotFoundException {
return mResourcesImpl.loadDrawable(this, value, id, density, theme);
}
//~/Android/sdk/sources/android-34/android/content/res/ResourcesImpl.java
Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
int density, @Nullable Resources.Theme theme)
throws NotFoundException {
finalboolean useCache = density == 0 || value.density == mMetrics.densityDpi;
try {
//......
Drawable dr;
boolean needsNewDrawableAfterCache = false;
if (cs != null) {
dr = cs.newDrawable(wrapper);
} elseif (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
//加载图片,并放入缓存
dr = loadDrawableForCookie(wrapper, value, id, density);
}
//......
return dr;
} catch (Exception e) {
//......
}
}
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density) {
try {
//......
try {
if (file.endsWith(".xml")) {
final String typeName = getResourceTypeName(id);
if (typeName != null && typeName.equals("color")) {
dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
} else {
//根据资源id对应的文件后缀,加载图片
dr = loadXmlDrawable(wrapper, value, id, density, file);
}
} elseif (file.startsWith("frro://")) {
//......
} finally {
//......
}
} catch (Exception | StackOverflowError e) {
//......
}
return dr;
}
private Drawable loadXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density, String file)
throws IOException, XmlPullParserException {
try ( //使用try-with-resources 语句,加载xml资源到XmlResourceParser解释器中
XmlResourceParser rp =
loadXmlResourceParser(file, id, value.assetCookie, "drawable")
) { //使用Drawable的方法来转换图片
return Drawable.createFromXmlForDensity(wrapper, rp, density, null);
}
}
///~/Android/sdk/sources/android-34/android/graphics/drawable/Drawable.java
public static Drawable createFromXmlForDensity(@NonNull Resources r,
@NonNull XmlPullParser parser, int density, @Nullable Theme theme)
throws XmlPullParserException, IOException {
//......
// 从xml创建图片
Drawable drawable = createFromXmlInnerForDensity(r, parser, attrs, density, theme);
if (drawable == null) {
thrownew RuntimeException("Unknown initial tag: " + parser.getName());
}
return drawable;
}
static@NonNullDrawable createFromXmlInnerForDensity(@NonNull Resources r,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density,
@Nullable Theme theme) throws XmlPullParserException, IOException {
return r.getDrawableInflater().inflateFromXmlForDensity(parser.getName(), parser, attrs,
density, theme); //使用DrawableInflater来转换图片
}
//~/Android/sdk/sources/android-34/android/content/res/Resources.java
public final DrawableInflater getDrawableInflater() {
if (mDrawableInflater == null) {
mDrawableInflater = new DrawableInflater(this, mClassLoader);
}
return mDrawableInflater;
}
//~/Android/sdk/sources/android-34/android/graphics/drawable/DrawableInflater.java
@NonNullDrawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, int density, @Nullable Theme theme)
throws XmlPullParserException, IOException {
//......
//根据xml中的tag来创建对应的Drawable对象
Drawable drawable = inflateFromTag(name);
if (drawable == null) {
drawable = inflateFromClass(name);
}
drawable.setSrcDensityOverride(density);
//将xml中的属性解释到Drawable对象的对应属性里,
//并将XmlResourceParser解释器传入inflate方法
drawable.inflate(mRes, parser, attrs, theme);
return drawable;
}
private@NullableDrawable inflateFromTag(@NonNull String name) {
switch (name) {
case"selector":
returnnew StateListDrawable();
case"animated-selector":
returnnew AnimatedStateListDrawable();
case"level-list":
returnnew LevelListDrawable();
case"layer-list":
returnnew LayerDrawable();
case"transition":
returnnew TransitionDrawable();
case"ripple":
returnnew RippleDrawable();
case"adaptive-icon":
returnnew AdaptiveIconDrawable();
case"color":
returnnew ColorDrawable();
case"shape":
returnnew GradientDrawable();
case"vector":
//根据tag:vector创建矢量图对象VectorDrawable
returnnew VectorDrawable();
case"animated-vector":
//根据tag:animated-vector创建矢量动图对象AnimatedVectorDrawable
returnnew AnimatedVectorDrawable();
//......
}
}
通过VectorDrawable的创建过程,我们可以了解到所有图片资源,最终都会通过解释xml中的标签,在inflateFromTag方法中创建图片对象,如VectorDrawable,GradientDrawable,ColorDrawable等。
获得了VectorDrawable对象后,inflateFromXmlForDensity方法会继续调用对象的inflate方法将xml中个标签的属性设置到对象的对应变量中,代码如下所示:
//~/Android/sdk/sources/android-34/android/graphics/drawable/VectorDrawable.java
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
try {
//......
// 解释每个子标签
inflateChildElements(r, parser, attrs, theme);
state.onTreeConstructionFinished();
updateLocalState(r);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
private void inflateChildElements(Resources res, XmlPullParser parser, AttributeSet attrs,
Theme theme) throws XmlPullParserException, IOException {
//......
// 循环遍历解释三个标签:SHAPE_GROUP="group",SHAPE_PATH = "path",SHAPE_CLIP_PATH="clip-path";
while (eventType != XmlPullParser.END_DOCUMENT
&& (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
if (eventType == XmlPullParser.START_TAG) {
final String tagName = parser.getName();
final VGroup currentGroup = groupStack.peek();
if (SHAPE_PATH.equals(tagName)) {
final VFullPath path = new VFullPath();
// 解释pathName,pathData,mFillColors,mStrokeColors等属性到对应的VFullPath属性中
path.inflate(res, attrs, theme);
currentGroup.addChild(path);
if (path.getPathName() != null) {
state.mVGTargetsMap.put(path.getPathName(), path);
}
noPathTag = false;
state.mChangingConfigurations |= path.mChangingConfigurations;
} elseif (SHAPE_CLIP_PATH.equals(tagName)) {
final VClipPath path = new VClipPath();
path.inflate(res, attrs, theme);
currentGroup.addChild(path);
if (path.getPathName() != null) {
state.mVGTargetsMap.put(path.getPathName(), path);
}
state.mChangingConfigurations |= path.mChangingConfigurations;
} elseif (SHAPE_GROUP.equals(tagName)) {
VGroup newChildGroup = new VGroup();
newChildGroup.inflate(res, attrs, theme);
currentGroup.addChild(newChildGroup);
groupStack.push(newChildGroup);
if (newChildGroup.getGroupName() != null) {
state.mVGTargetsMap.put(newChildGroup.getGroupName(),
newChildGroup);
}
state.mChangingConfigurations |= newChildGroup.mChangingConfigurations;
}
} elseif (eventType == XmlPullParser.END_TAG) {
final String tagName = parser.getName();
if (SHAPE_GROUP.equals(tagName)) {
groupStack.pop();
}
}
eventType = parser.next();
}
//......
}
//VFullPath类的inflate方法
public void inflate(Resources r, AttributeSet attrs, Theme theme) {
final TypedArray a = obtainAttributes(r, theme, attrs,
R.styleable.VectorDrawablePath);
updateStateFromTypedArray(a); //把xml中的属性值读取到对象属性中
a.recycle();
}
//VFullPath类的updateStateFromTypedArray方法
private void updateStateFromTypedArray(TypedArray a) {
//......
//读取pathName
final String pathName = a.getString(R.styleable.VectorDrawablePath_name);
if (pathName != null) {
mPathName = pathName;
nSetName(mNativePtr, mPathName);
}
//读取pathData
final String pathString = a.getString(R.styleable.VectorDrawablePath_pathData);
if (pathString != null) {
mPathData = new PathParser.PathData(pathString);
nSetPathString(mNativePtr, pathString, pathString.length());
}
//读取fillColors
final ComplexColor fillColors = a.getComplexColor(R.styleable.VectorDrawablePath_fillColor);
if (fillColors != null) {
// If the colors is a gradient color, or the color state list is stateful, keep the
// colors information. Otherwise, discard the colors and keep the default color.
if (fillColors instanceof GradientColor) {
mFillColors = fillColors;
fillGradient = ((GradientColor) fillColors).getShader();
} elseif (fillColors.isStateful() || fillColors.canApplyTheme()) {
mFillColors = fillColors;
} else {
mFillColors = null;
}
fillColor = fillColors.getDefaultColor();
}
//......
}
VectorDrawable的inflate方法,会循环遍历三个标签:"group", "path", "clip-path"。三个标签通过内部静态类VGroup,VFullPath,VClipPath的inflate方法负责从TypedArray解释出对应标签的属性。
到这一步,ImageView中src对应的Vector文件就成功转换成了VectorDrawable对象,其流程可以总结如下:

获得了VectorDrawable对象和属性后,继续查看它如何显示在布局控件中,如下代码所示:
//~/Android/sdk/sources/android-34/android/widget/ImageView.java
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawable == null) {
return; // couldn't resolve the URI
}
if (mDrawableWidth == 0 || mDrawableHeight == 0) {
return; // nothing to draw (empty bounds)
}
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas); // 绘制src图片
//......
}
}
//~/Android/sdk/sources/android-34/android/graphics/drawable/VectorDrawable.java
public void draw(Canvas canvas) {
//......
final ColorFilter colorFilter = (mColorFilter == null ? mBlendModeColorFilter :
mColorFilter);
finallong colorFilterNativeInstance = colorFilter == null ? 0 :
colorFilter.getNativeInstance();
boolean canReuseCache = mVectorState.canReuseCache();
// 通过native库,将mVectorState绘制到canvas中
int pixelCount = nDraw(mVectorState.getNativeRenderer(), canvas.getNativeCanvasWrapper(),
colorFilterNativeInstance, mTmpBounds, needMirroring(),
canReuseCache);
//......
}
由于path绘制涉及到复杂的绘制命令,所以draw方法最后调用native库代码进行矢量绘制,无法在AS中直接查看native代码,这里不做展开说明。到这里,我们已经基本了解了从Vector.xml文件中,获得VectorDrawable对象,并将VectorDrawable绘制到ImageView的整体流程。
02 矢量动画AnimatedVectorDrawable
Android并不支持SVG中的动画标签,它通过AnimatedVectorDrawable支持矢量图动画,将Android属性动画引入到矢量图中。
2.1 元素和属性
AnimatedVectorDrawable也是树状层次结构的xml文件,其根元素为<animated-vector>标签,且支持的标签和属性非常少。通过系统的attrs.xml文件,可以查到所有标签和属性定义,如下代码所示:
<!-- Define the AnimatedVectorDrawable. -->
<declare-styleable name="AnimatedVectorDrawable">
<!-- The static vector drawable. -->
<attr name="drawable" />
</declare-styleable>
<!-- Defines the target used in the AnimatedVectorDrawable. -->
<declare-styleable name="AnimatedVectorDrawableTarget">
<!-- The name of the target path, group or vector drawable. -->
<attr name="name" />
<!-- The animation for the target path, group or vector drawable. -->
<attr name="animation" />
</declare-styleable>
其中<animated-vector>根标签只有drawable属性,其值必须是Vector drawable类型。
而<target>标签有2个属性:
name属性,是drawable属性对应的VectorDrawable的某个元素(group或path)的名字;
animation属性,是上面name元素对应的属性动画。
一个<animated-vector>矢量动画可以包含多个target元素,target的animation属性可以是一个ObjectAnimator,也可以是一个动画set。
2.2 使用矢量图动画
Android Studio的Vector Asset创建工具不支持将本地SVG中的动画标签转换成animated-vector对应的动画,也没有找到将SVG动画转换成Vector动画的三方工具,所以只能手写Android矢量动图文件。
下面的test8.xml文件,展示了AnimatedVectorDrawable的绘制方法:
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:name="root"
android:strokeWidth="2"
android:strokeLineCap="square"
android:strokeColor="#FF0000"
android:pathData="M4.8,13.4 L9,17.6 M10.4,16.2 L19.6,7" />
</vector>
</aapt:attr>
<target android:name="root">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="pathData"
android:valueFrom="M4.8,13.4 L9,17.6 M10.4,16.2 L19.6,7"
android:valueTo="M6.4,6.4 L17.6,17.6 M6.4,17.6 L17.6,6.4"
android:duration="6000"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:valueType="pathType" />
</aapt:attr>
</target>
<target android:name="root">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="strokeColor"
android:valueFrom="#FF0000"
android:valueTo="#0000FF"
android:duration="6000"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:valueType="colorType" />
</aapt:attr>
</target>
</animated-vector>
test8.xml中定义了一个矢量图形root path,并且给root指定了两个动画:path路径动画和stroke颜色动画。
编辑完test8.xml,我们还需要在布局中引用动画,其布局参考如下:
//~/projectpath/sample/src/main/res/layout/activity_second.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="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="top|left"
android:paddingTop="10dp"
android:background="#00FFFF">
<ImageView
android:id="@+id/img_2"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@drawable/test8" />
</LinearLayout>
布局中把test8赋值给了Imageview控件的src属性。接下来要启动动画,代码参考如下:
//~/projectpath/sample/src/main/java/com/test/demo/SecondActivity.java
publicclass SecondActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
final ImageView imageView_2 = findViewById(R.id.img_2);
AnimatedVectorDrawable animationDrawable2 = (AnimatedVectorDrawable) imageView_2.getDrawable();
if (animationDrawable2 != null) {
animationDrawable2.registerAnimationCallback(new Animatable2.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
//animationDrawable2.reverse(); //反向动画
//animationDrawable2.reset();//重置回初始画面
}
});
animationDrawable2.start(); //启动动画
}
}
}
ImageView中的动画默认会停在初始画面,需要在代码中通过start函数来启动动画。和其它Android动画一样,AnimatedVectorDrawable也可以注册动画回调来监听动画播放过程,还可以控制动画反向播放和重置回初始画面。
在App的ImageView中查看test8.xml的SVG动画效果,矢量图从开始的✔️形状,渐变成最后的✖️形状,颜色则从红色渐变到蓝色,其动画效果如下:

录屏转换的gif图片清晰度较低,所以看着模糊,实际APP上看到的动效非常清晰。
2.3 源码分析
了解了Vector矢量动画animated-vector文件的编写和动画的启动,那么它是怎么把Android属性动画引入到矢量图Vector的呢?以及它支持哪些Android动画呢?下面继续从源码分析来解答这些问题。
对象获取
从上一节“矢量图VectorDrawable”的源码分析中,我们可以知道DrawableInflater的inflateFromTag方法会把<vector>转换成VectorDrawable对象。
同样的,<animated-vector>标签最后也会在inflateFromTag方法中被转换成AnimatedVectorDrawable对象,其源码如下:
//~/Android/sdk/sources/android-34/android/graphics/drawable/DrawableInflater.java
@NonNullDrawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, int density, @Nullable Theme theme)
throws XmlPullParserException, IOException {
//......
//根据xml中的tag来创建对应的Drawable图片
Drawable drawable = inflateFromTag(name);
if (drawable == null) {
drawable = inflateFromClass(name);
}
drawable.setSrcDensityOverride(density);
//将xml中的属性解释到Drawable对象里,此时drawable为AnimatedVectorDrawable
drawable.inflate(mRes, parser, attrs, theme);
return drawable;
}
private@NullableDrawable inflateFromTag(@NonNull String name) {
switch (name) {
//......
case"vector":
//根据tag:vector创建矢量图对象VectorDrawable
returnnew VectorDrawable();
case"animated-vector":
//根据tag:animated-vector创建矢量动图对象AnimatedVectorDrawable
returnnew AnimatedVectorDrawable();
//......
}
}
创建完AnimatedVectorDrawable对象后,会继续调用其inflate方法解释所有属性,源码如下:
//~/Android/sdk/sources/android-34/android/graphics/drawable/AnimatedVectorDrawable.java
public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme)
throws XmlPullParserException, IOException {
final AnimatedVectorDrawableState state = mAnimatedVectorState;
//......
while (eventType != XmlPullParser.END_DOCUMENT
&& (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
if (eventType == XmlPullParser.START_TAG) {
final String tagName = parser.getName();
if (ANIMATED_VECTOR.equals(tagName)) {
final TypedArray a = obtainAttributes(res, theme, attrs,
R.styleable.AnimatedVectorDrawable);
//获取<animated-vector>中的drawable属性,
int drawableRes = a.getResourceId(
R.styleable.AnimatedVectorDrawable_drawable, 0);
if (drawableRes != 0) {//获取VectorDrawable,具体参考上面的VectorDrawable源码分析
VectorDrawable vectorDrawable = (VectorDrawable) res.getDrawable(
drawableRes, theme).mutate();
vectorDrawable.setAllowCaching(false);
vectorDrawable.setCallback(mCallback);
pathErrorScale = vectorDrawable.getPixelSize();
if (state.mVectorDrawable != null) {
state.mVectorDrawable.setCallback(null);
}
state.mVectorDrawable = vectorDrawable;
}
a.recycle();
} elseif (TARGET.equals(tagName)) {
final TypedArray a = obtainAttributes(res, theme, attrs,
R.styleable.AnimatedVectorDrawableTarget);
//获取<target>中的name属性
final String target = a.getString(
R.styleable.AnimatedVectorDrawableTarget_name);
//获取<target>中的animation属性对应的资源id
finalint animResId = a.getResourceId(
R.styleable.AnimatedVectorDrawableTarget_animation, 0);
if (animResId != 0) {
if (theme != null) {
// 从xml文件中解释出Animatior
final Animator animator = AnimatorInflater.loadAnimator(
res, theme, animResId, pathErrorScale);
// 根据target名字找到矢量图对应的path元素,并绑定到此属性动画上
updateAnimatorProperty(animator, target, state.mVectorDrawable,
state.mShouldIgnoreInvalidAnim);
state.addTargetAnimator(target, animator);
} else {
state.addPendingAnimator(animResId, pathErrorScale, target);
}
}
a.recycle();
}
}
eventType = parser.next();
}
//......
}
上面的代码对SVG动画xml进行解释,把标签和属性读取到vectorDrawable对象和animator对象中,并把对象保存到mAnimatedVectorState属性的mVectorDrawable和mAnimators变量中。
其中把drawable属性解释到mVectorDrawable变量,和上一节“矢量图VectorDrawable”的源码分析流程一样,不做展开说明。
target的name属性是字符解释比较简单。而target的animation属性会复用Android系统的AnimatorInflater类的loadAnimator方法,并最终调用到createAnimatorFromXml方法,如下代码所示:
//~/Android/sdk/sources/android-34/android/animation/AnimatorInflater.java
private static Animator createAnimatorFromXml(Resources res, Theme theme, XmlPullParser parser,
AttributeSet attrs, AnimatorSet parent, int sequenceOrdering, float pixelSize)
throws XmlPullParserException, IOException {
//......
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
//......
if (name.equals("objectAnimator")) {
// 加载ObjectAnimator动画,矢量动画只支持ObjectAnimator
anim = loadObjectAnimator(res, theme, attrs, pixelSize);
} elseif (name.equals("animator")) {
anim = loadAnimator(res, theme, attrs, null, pixelSize);
} elseif (name.equals("set")) {
anim = new AnimatorSet();
//......
//循环调用此方法,继续解释动画set中的每一个animation
createAnimatorFromXml(res, theme, parser, attrs, (AnimatorSet) anim, ordering, pixelSize);
} elseif (name.equals("propertyValuesHolder")) {
PropertyValuesHolder[] values = loadValues(res, theme, parser,
Xml.asAttributeSet(parser));
if (values != null && anim != null && (anim instanceof ValueAnimator)) {
((ValueAnimator) anim).setValues(values);
}
gotValues = true;
} else {
thrownew RuntimeException("Unknown animator name: " + parser.getName());
}
//......
}
//......
return anim;
}
从代码可以看出来,animation支持的标签只有四个:<objectAnimator>,<animator>,<set>,<propertyValuesHolder>。它们分别对应ObjectAnimator,ValueAnimator,AnimatorSet,和propertyValuesHolder类,属于Android系统动画类,其含义和属性不做展开说明。
但AnimatedVectorDrawable实际只支持<objectAnimator>和<set>标签,且<set>标签中只能包含<objectAnimator>,后面源码分析会解释这个部分。
现在不管是<objectAnimator>还是<set>标签,源码最终会走到loadObjectAnimator,其代码如下:
//~/Android/sdk/sources/android-34/android/animation/AnimatorInflater.java
private static ObjectAnimator loadObjectAnimator(Resources res, Theme theme, AttributeSet attrs,
float pathErrorScale) throws NotFoundException {
ObjectAnimator anim = new ObjectAnimator();//创建一个新的ObjectAnimator对象
// 传入ObjectAnimator对象,继续加载
loadAnimator(res, theme, attrs, anim, pathErrorScale);
return anim;
}
private static ValueAnimator loadAnimator(Resources res, Theme theme,
AttributeSet attrs, ValueAnimator anim, float pathErrorScale)
throws NotFoundException {
//......
//解释动画的属性
parseAnimatorFromTypeArray(anim, arrayAnimator, arrayObjectAnimator, pathErrorScale);
//根据xml的插值器资源id,设置动画的插值器
finalint resID = arrayAnimator.getResourceId(R.styleable.Animator_interpolator, 0);
if (resID > 0) {
final Interpolator interpolator = AnimationUtils.loadInterpolator(res, theme, resID);
//......
anim.setInterpolator(interpolator);
}
//......
}
private static void parseAnimatorFromTypeArray(ValueAnimator anim, TypedArray arrayAnimator, TypedArray arrayObjectAnimator, float pixelSize) {
//......
//创建动画的属性holder,此时传的propertyName为空字符串
PropertyValuesHolder pvh = getPVH(arrayAnimator, valueType,
R.styleable.Animator_valueFrom, R.styleable.Animator_valueTo, "");
if (pvh != null) {
anim.setValues(pvh);
}
anim.setDuration(duration);
anim.setStartDelay(startDelay);
//......
if (arrayObjectAnimator != null) {
//设置propertyName,优先pathData字段,然后才从propertyName字段读取
setupObjectAnimator(anim, arrayObjectAnimator, valueType, pixelSize);
}
}
private static PropertyValuesHolder getPVH(TypedArray styledAttributes, int valueType,
int valueFromId, int valueToId, String propertyName) {
//......
if (valueType == VALUE_TYPE_PATH) {
//读取xml中ObjectAnimation的下面2个值
//android:valueFrom="M4.8,13.4 L9,17.6 M10.4,16.2 L19.6,7"
//android:valueTo="M6.4,6.4 L17.6,17.6 M6.4,17.6 L17.6,6.4"
String fromString = styledAttributes.getString(valueFromId);
String toString = styledAttributes.getString(valueToId);
PathParser.PathData nodesFrom = fromString == null? null : new PathParser.PathData(fromString);
PathParser.PathData nodesTo = toString == null? null : new PathParser.PathData(toString);
//......
TypeEvaluator evaluator = new PathDataEvaluator();//默认的path估值器
//如果2个path的长度一致,对应数据类型一致,才支持数据变换。
if (!PathParser.canMorph(nodesFrom, nodesTo)) {
thrownew InflateException(" Can't morph from " + fromString + " to " +
toString);
}
//传入nodesFrom, nodesTo来创建一个PropertyValuesHolder
returnValue = PropertyValuesHolder.ofObject(propertyName, evaluator,
nodesFrom, nodesTo);
//.....
}
//......
}
//~/Android/sdk/sources/android-34/android/animation/PropertyValuesHolder.java
public static PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator, Object... values) {
PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
pvh.setObjectValues(values);//设置动画初始和结束的PathParser.PathData值
pvh.setEvaluator(evaluator);
return pvh;
}
public void setObjectValues(Object... values) {
mValueType = values[0].getClass();
//将初始和结束的PathParser.PathData值,转换成关键帧存储。
mKeyframes = KeyframeSet.ofObject(values);
if (mEvaluator != null) {
mKeyframes.setEvaluator(mEvaluator);
}
}
//~/Android/sdk/sources/android-34/android/animation/KeyframeSet.java
public static KeyframeSet ofObject(Object... values) {
int numKeyframes = values.length;
ObjectKeyframe keyframes[] = new ObjectKeyframe[Math.max(numKeyframes,2)];
if (numKeyframes == 1) {
keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f);
keyframes[1] = (ObjectKeyframe) Keyframe.ofObject(1f, values[0]);
} else {//numKeyframes==2会创建2个关键帧
keyframes[0] = (ObjectKeyframe) Keyframe.ofObject(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {
keyframes[i] = (ObjectKeyframe) Keyframe.ofObject((float) i / (numKeyframes - 1), values[i]);
}
}
returnnew KeyframeSet(keyframes);
}
loadObjectAnimator方法中直接创建了ObjectAnimator对象,并将动画的interpolate插值器和PropertyValuesHolder属性值Holder保存到动画对象中。
其中PropertyValuesHolder中包括了动画的默认估值器PathDataEvaluator,和path的关键帧ObjectKeyframe,而关键帧中存储了动画初始和结束时对应的PathParser.PathData值。
AnimatedVectorDrawable.inflate方法加载好ObjectAnimator对象后,会继续通过updateAnimatorProperty方法把target name对应的矢量图元素(group或path)绑定到属性动画的PropertyValuesHolder中,其实现代码如下:
//~/Android/sdk/sources/android-34/android/graphics/drawable/AnimatedVectorDrawable.java
private static void updateAnimatorProperty(Animator animator, String targetName,
VectorDrawable vectorDrawable, boolean ignoreInvalidAnim) {
if (animator instanceof ObjectAnimator) {
PropertyValuesHolder[] holders = ((ObjectAnimator) animator).getValues();
for (int i = 0; i < holders.length; i++) {
PropertyValuesHolder pvh = holders[i];
String propertyName = pvh.getPropertyName();
Object targetNameObj = vectorDrawable.getTargetByName(targetName);
Property property = null;
//targetNameObj可以是VObject的子类:VFullPath,VClipPath,VGroup
if (targetNameObj instanceof VectorDrawable.VObject) {
property = ((VectorDrawable.VObject) targetNameObj).getProperty(propertyName);
} elseif (targetNameObj instanceof VectorDrawable.VectorDrawableState) {
property = ((VectorDrawable.VectorDrawableState) targetNameObj)
.getProperty(propertyName);
}
if (property != null) {
if (containsSameValueType(pvh, property)) {
//将矢量元素的Property和属性动画的PropertyValuesHolder绑定
pvh.setProperty(property);
} elseif (!ignoreInvalidAnim) {
thrownew RuntimeException("Wrong valueType for Property: " + propertyName
+ ". Expected type: " + property.getType().toString() + ". Actual "
+ "type defined in resources: " + pvh.getValueType().toString());
}
}
}
} elseif (animator instanceof AnimatorSet) {
for (Animator anim : ((AnimatorSet) animator).getChildAnimations()) {
//循环去绑定动画set中的每一个ObjectAnimator对应的target Property
updateAnimatorProperty(anim, targetName, vectorDrawable, ignoreInvalidAnim);
}
}
}
从updateAnimatorProperty方法可以看出,Vector动画只支持ObjectAnimator和AnimatorSet,并不支持ValueAnimator和propertyValuesHolder,即target中的animation属性只支持<objectAnimator>和<set>标签,不支持<animator>和<propertyValuesHolder>标签。
updateAnimatorProperty方法把矢量图元素的Property绑定到属性动画的PropertyValuesHolder中。
到此,ImageView中配置的android:src被成功转换成AnimatedVectorDrawable对象,并设置到mDrawable变量了,其流程可以总结如下:

动画播放
AnimatedVectorDrawable创建好后,就可以通过ImageView获取到它,并调用start方法来播放矢量动画,其源码如下:
//~/projectpath/sample/src/main/java/com/test/demo/SecondActivity.java
publicclass SecondActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
final ImageView imageView_2 = findViewById(R.id.img_2);
AnimatedVectorDrawable animationDrawable2 = (AnimatedVectorDrawable) imageView_2.getDrawable();
if (animationDrawable2 != null) {
animationDrawable2.start(); //启动动画
}
}
}
//~/Android/sdk/sources/android-34/android/graphics/drawable/AnimatedVectorDrawable.java
public void start() {
ensureAnimatorSet();
if (DBG_ANIMATION_VECTOR_DRAWABLE) {
Log.w(LOGTAG, "calling start on AVD: " +
((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState)
getConstantState()).mVectorDrawable.getConstantState()).mRootName
+ ", at: " + this);
}
mAnimatorSet.start();
}
private void ensureAnimatorSet() {
if (mAnimatorSetFromXml == null) {ArrayList<Animator>
// 将mAnimatedVectorState中的mAnimators ArrayList放到mAnimatorSet变量中
mAnimatorSetFromXml = new AnimatorSet();
mAnimatedVectorState.prepareLocalAnimators(mAnimatorSetFromXml, mRes);
mAnimatorSet.init(mAnimatorSetFromXml);
mRes = null;
}
}
通过ImageView获取到AnimatedVectorDrawable后,调用其start方法播放动画。播放动画时,会先将之前保存在mAnimatedVectorState中的动画序列保存到mAnimatorSet中。
mAnimatorSet声明为VectorDrawableAnimator接口,有两种实现方案:
硬件加速版VectorDrawableAnimatorRT,在native代码中对动画序列AnimatorSet进行封装。这个是mAnimatorSet默认选择的方案,但是源码在native层,我们不展开分析;
非硬件加速版VectorDrawableAnimatorUI,在java代码中对AnimatorSet进行封装。
本文要针对VectorDrawableAnimatorUI方案进一步的分析,但硬件加速默认是开启的,为了方便对代码进行单步调试,需要先关闭硬件加速。
关闭硬件加速,只需要把AndroidManifest.xml文件的<application>标签加入如下属性设置:android:hardwareAccelerated = "false"。
关闭硬件加速后,可以单步跟踪代码,继续分析mAnimatorSet的start方法,其源码如下:
//~/Android/sdk/sources/android-34/android/graphics/drawable/AnimatedVectorDrawable.java
privatestaticclass VectorDrawableAnimatorUI implements VectorDrawableAnimator {
private AnimatorSet mSet = null;
privatefinal Drawable mDrawable;
//......
VectorDrawableAnimatorUI(@NonNull AnimatedVectorDrawable drawable) {
mDrawable = drawable; //将SVG对应的对象保存在mDrawable
}
@Override
public void init(@NonNull AnimatorSet set) {
//......
mSet = set.clone(); //将动画AnimatorSet克隆一份保存在mSet
mTotalDuration = mSet.getTotalDuration();
mIsInfinite = mTotalDuration == Animator.DURATION_INFINITE;
//......
}
@Override
public void start() {
if (mSet == null || mSet.isStarted()) {
return;
}
mSet.start(); //调用AnimatorSet的start方法
invalidateOwningView();
}
}
//~/Android/sdk/sources/android-34/android/animation/AnimatorSet.java
private void start(boolean inReverse, boolean selfPulse) {
//....
initAnimation();// 初始化动画的属性,比如,插值器,播放时长,播放依赖等
//....
boolean isEmptySet = isEmptySet(this);
if (!isEmptySet) {
startAnimation();//播放动画
}
//....
}
private void startAnimation() {
addAnimationEndListener();
// 注册动画的回调
addAnimationCallback(0);
//....
}
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
AnimationHandler handler = AnimationHandler.getInstance();
//把AnimatorSet加入到AnimationHandler单例的mAnimationCallbacks列表中
handler.addAnimationFrameCallback(this, delay);
}
//~/Android/sdk/sources/android-34/android/animation/AnimationHandler.java
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
//向Choreographer设置帧绘制回调:mFrameCallback
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
//把AnimatorSet加入到AnimationHandler的mAnimationCallbacks中
mAnimationCallbacks.add(callback);
}
if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}
publicinterface AnimationFrameCallback {
boolean doAnimationFrame(long frameTime);
void commitAnimationFrame(long frameTime);
}
AnimatorSet实现了AnimationFrameCallback接口,且startanimation方法中把自己加入到了AnimationHandler的动画帧回调列表mAnimationCallbacks中。
AnimationHandler是一个单例,所有动画都向它注册帧绘制回调AnimationFrameCallback,它则向MyFrameCallbakProvider添加了帧刷新回调mFrameCallback。
那么这些回调方法都会在什么时机被调用呢?答案是依靠系统的Choreographer机制来实现帧刷新回调。Choreographer接收来自显示系统的Vsync信号,收到VSYNC信号后,会调用Choreographer.FrameCallback的doFrame方法,其源码如下:
//~/Android/sdk/sources/android-34/android/animation/AnimationHandler.java
private AnimationFrameCallbackProvider getProvider() {
if (mProvider == null) {
mProvider = new MyFrameCallbackProvider();
}
return mProvider;
}
privateclass MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
//获取绘制时钟同步的编导单例
final Choreographer mChoreographer = Choreographer.getInstance();
@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
//Choreographer接收到Vsync信号后,会回调callback,不进一步不展开说明
mChoreographer.postFrameCallback(callback);
}
//......
}
privatefinal Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//Choreographer接收到Vsync信号后,回调callback的doFrame方法
doAnimationFrame(getProvider().getFrameTime());
//继续向Choreographer设置帧绘制回调,以确保下一帧绘制都能收到回调!
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
finalint size = mAnimationCallbacks.size();
for (int i = 0; i < size; i++) { //遍历mAnimationCallbacks
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (isCallbackDue(callback, currentTime)) {
// 回调AnimatorSet中的doAnimationFrame
callback.doAnimationFrame(frameTime);
//......
}
}
//......
}
Choreographer 收到VSYNC信号后,会回调AnimationHandler类mFrameCallback成员变量的doFrame方法。
doFrame方法中调用doAnimationFrame方法,它会遍历所有注册到其mAnimationCallbacks的动画帧回调,调用它们的doAnimationFrame方法,进行动画帧的绘制。
doFrame方法还会继续将自己加入到Choreographer下一帧的绘制回调里,以确保每一个帧绘制都能被回调到。
上面分析了系统animator的帧绘制回调时机,接下来回到注册动画帧回调的AnimatorSet对象,继续查看它是如何根据frameTime设置VectorDrawable对应属性值的,其源码如下:
//~/Android/sdk/sources/android-34/android/animation/AnimatorSet.jav
public boolean doAnimationFrame(long frameTime) {
//......
// 遍历所有动画node
for (int i = 0; i < mPlayingSet.size(); i++) {
Node node = mPlayingSet.get(i);
if (!node.mEnded) {
// Node动画没有结束,则生成动画的一帧
pulseFrame(node, getPlayTimeForNodeIncludingDelay(unscaledPlayTime, node));
}
}
//......
}
private void pulseFrame(Node node, long animPlayTime) {
if (!node.mEnded) {
float durationScale = ValueAnimator.getDurationScale();
durationScale = durationScale == 0 ? 1 : durationScale;
// 继续调用ValueAnimator的pulseAnimationFrame
node.mEnded = node.mAnimation.pulseAnimationFrame(
(long) (animPlayTime * durationScale));
}
}
//~/Android/sdk/sources/android-34/android/animation/ValueAnimator.java
boolean pulseAnimationFrame(long frameTime) {
//......
return doAnimationFrame(frameTime);
}
public final boolean doAnimationFrame(long frameTime) {
//......
if (!mRunning) {
if (mStartTime > frameTime && mSeekFraction == -1) {
returnfalse;
} else {
mRunning = true;
startAnimation(); //先启动动画,初始化相关属性值
}
}
//......
finallong currentTime = Math.max(frameTime, mStartTime);
//根据当前时间来生成动画帧
boolean finished = animateBasedOnTime(currentTime);
if (finished) {
endAnimation();
}
return finished;
}
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
//......
// 对应AnimatedVectorDrawable,会调用到ObjectAnimator的animateValue方法
animateValue(currentIterationFraction);
}
return done;
}
AnimatorSet中可能包含多个动画,会对它们进行依赖排序,然后遍历绘制所有进行中的动画,最后会调用ValueAnimation的animateValue方法进行绘制。
对于AnimatedVectorDrawable,则会调用到ValueAnimation子类ObjectAnimator类的animateValue方法进行动画属性的计算。
animateValue方法主要负责动画属性值计算和矢量图形属性设置,以<path>
//~/Android/sdk/sources/android-34/android/animation/ObjectAnimator.java
void animateValue(float fraction) {
final Object target = getTarget();
//......
super.animateValue(fraction); //先调用ValueAnimator父类,进行属性估值
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
//为target的每个PropertyValuesHolder更新属性值
mValues[i].setAnimatedValue(target);
}
}
//~/Android/sdk/sources/android-34/android/animation/ValueAnimator.java
void animateValue(float fraction) {
//......
fraction = mInterpolator.getInterpolation(fraction);//获取动画插值
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);//根据插值,计算属性估值
}
//......
}
//~/Android/sdk/sources/android-34/android/animation/PropertyValuesHolder.java
void calculateValue(float fraction) {
Object value = mKeyframes.getValue(fraction);//从关键帧计算估值
//估值完成后的PathParser.PathData值,保存到mAnimatedValue变量
mAnimatedValue = mConverter == null ? value : mConverter.convert(value);
}
//~y/Android/sdk/sources/android-34/android/animation/KeyframeSet.java
public Object getValue(float fraction) {
if (mNumKeyframes == 2) {
//......
//使用估值器,根据关键帧的开始和结束path值获取估算的path值
return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),
mLastKeyframe.getValue());
}
//......
}
//~/Android/sdk/sources/android-34/android/animation/AnimatorInflater.java
privatestaticclass PathDataEvaluator implements TypeEvaluator<PathParser.PathData> {
privatefinal PathParser.PathData mPathData = new PathParser.PathData();
@Override
public PathParser.PathData evaluate(float fraction, PathParser.PathData startPathData,
PathParser.PathData endPathData) {
if (!PathParser.interpolatePathData(mPathData, startPathData, endPathData, fraction)) {
thrownew IllegalArgumentException("Can't interpolate between"
+ " two incompatible pathData");
}
return mPathData;
}
}
//~/Android/sdk/sources/android-34/android/util/PathParser.java
public static boolean interpolatePathData(PathData outData, PathData fromData, PathData toData,
float fraction) {//path绘制涉及到复杂的绘制命令,如贝塞尔曲线,其估值计算也放在了native层。
return nInterpolatePathData(outData.mNativePathData, fromData.mNativePathData,
toData.mNativePathData, fraction);
}
上面代码中,ObjectAnimator遍历所有PropertyValuesHolder,调用它们的calculateValue方法,根据播放时间,插值器和估值器计算出新的PathParser.PathData值存储到各自的mAnimatedValue变量中。因为Path的估值转换涉及到复杂的绘制命令,Android系统在native层中对其进行实现,本文就不展开说明了。
PropertyValuesHolder计算出了动画属性值mAnimatedValue后,下一步就把这个值设置到target矢量形状的相应属性上,其代码如下:
//~/Android/sdk/sources/android-34/android/animation/PropertyValuesHolder.java
void setAnimatedValue(Object target) {
if (mProperty != null) {
//由上面的updateAnimatorProperty方法可知,此时mProperty为
//static静态属性PATH_DATA,即Property<VPath, PathParser.PathData>
//而getAnimatedValue获取的正是估值后的属性值mAnimatedValue
mProperty.set(target, getAnimatedValue());
}
//......
}
//~/Android/sdk/sources/android-34/android/graphics/drawable/VectorDrawable.java
staticabstractclass VPath extends VObject {
//......
privatestaticfinal Property<VPath, PathParser.PathData> PATH_DATA =
new Property<VPath, PathParser.PathData>(PathParser.PathData.class, "pathData") {
@Override
public void set(VPath object, PathParser.PathData data) {
object.setPathData(data);//把计算后的PathData设置给VPath
}
@Override
public PathParser.PathData get(VPath object) {
return object.getPathData();
}
};
Property getProperty(String propertyName) {
if (PATH_DATA.getName().equals(propertyName)) {
return PATH_DATA;
}
returnnull;
}
//......
public void setPathData(PathParser.PathData pathData) {
mPathData.setPathData(pathData);
if (isTreeValid()) {
//path的绘制在native层,所以将数据更新到底层
nSetPathData(getNativePtr(), mPathData.getNativePtr());
}
}
}
给PropertyValuesHolder的mProperty设置估算好的mAnimatedValue值,实际就是给VPath对象设置PathData,因为Path的绘制涉及到复杂的绘制命令,也是在native层中的实现的。
更新完VectorDrawable中VPath的值后,新的VPath就会在VectorDrawable的draw方法被调用时被绘制到Canvas,和上节分析的VectorDrawable绘制流程一样。
到此,把ImageView中的Vector文件成功转换AnimatedVectorDrawable对象,并播放属性动画的源码就分析完了,其流程可以总结如下:

03 总结
本文介绍了SVG在Android中的实现方案Vector,讲解了VectorDrawable和AnimatedVectorDrawable的使用方法,同时通过源码进一步分析说明了Android矢量图和动画的原理,最后总结和梳理如下:
VectorDrawable是Android支持的静态矢量图,它属于树状结构,标签简单,比SVG支持的标签和属性都少很多,主要支持集中在基本的形状和颜色上。但正因为其简化实现,渲染时性能较好,适用于应用中的图标,按钮背景等简单图形;
AnimatedVectorDrawable是Android支持的动态矢量图,可以对VectorDrawable中的group,path元素进行动画,如路径变化、颜色变化等。它通过复用Android ObjectAnimator实现矢量图元素的属性动画。它适合用于按钮点击、加载动画等需要用户交互时,图形发生动态变化的场景。
VectorDrawable体积小,能在多种屏幕分辨率保存清晰度,而AnimatedVectorDrawable也很小,可以通过动画增强用户体验,使界面更加生动和吸引人。我们可以根据具体需求选择合适的矢量图方案,以提升应用的视觉效果和用户体验。