搜狐技术产品

矢量图Vector安卓详解

在之前分享的文章《矢量图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>

1.1 元素和属性

VectorDrawable支持的标签比较少,每个标签支持的属性也比SVG少。通过系统的attrs.xml文件,可以查看到所有VectorDrawable的元素和属性定义,如下代码所示:

<!-- Drawable used to draw vector paths. -->
<declare-styleablename="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. -->

<attrname="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. -->

<attrname="tintMode" />
<!-- Indicates if the drawable needs to be mirrored when its layout direction is
         RTL (right-to-left). -->

<attrname="autoMirrored" />
<!-- The intrinsic width of the Vector Drawable. -->
<attrname="width" />
<!-- The intrinsic height of the Vector Drawable. -->
<attrname="height" />
<!-- The width of the canvas the drawing is on. -->
<attrname="viewportWidth"format="float"/>
<!-- The height of the canvas the drawing is on. -->
<attrname="viewportHeight"format="float"/>
<!-- The name of this vector drawable. -->
<attrname="name" />
<!-- The opacity of the whole vector drawable, as a value between 0
         (completely transparent) and 1 (completely opaque). -->

<attrname="alpha" />
<!-- Left optical inset. -->
<attrname="opticalInsetLeft"format="dimension" />
<!-- Top optical inset. -->
<attrname="opticalInsetTop"format="dimension" />
<!-- Right optical inset. -->
<attrname="opticalInsetRight"format="dimension" />
<!-- Bottom optical inset. -->
<attrname="opticalInsetBottom"format="dimension" />
</declare-styleable>


<!-- Defines the group used in VectorDrawables. -->
<declare-styleablename="VectorDrawableGroup">
<!-- The name of this group. -->
<attrname="name" />
<!-- The amount to rotate the group. -->
<attrname="rotation" />
<!-- The X coordinate of the center of rotation of a group. -->
<attrname="pivotX" />
<!-- The Y coordinate of the center of rotation of a group. -->
<attrname="pivotY" />
<!-- The amount to translate the group on X coordinate. -->
<attrname="translateX"format="float"/>
<!-- The amount to translate the group on Y coordinate. -->
<attrname="translateY"format="float"/>
<!-- The amount to scale the group on X coordinate. -->
<attrname="scaleX" />
<!-- The amount to scale the group on X coordinate. -->
<attrname="scaleY" />
</declare-styleable>


<!-- Defines the path used in VectorDrawables. -->
<declare-styleablename="VectorDrawablePath">
<!-- The name of this path. -->
<attrname="name" />
<!-- The width a path stroke. -->
<attrname="strokeWidth"format="float" />
<!-- The color to stroke the path if not defined implies no stroke. -->
<attrname="strokeColor"format="color" />
<!-- The opacity of a path stroke, as a value between 0 (completely transparent)
         and 1 (completely opaque). -->

<attrname="strokeAlpha"format="float" />
<!-- The color to fill the path if not defined implies no fill. -->
<attrname="fillColor"format="color" />
<!-- The alpha of the path fill, as a value between 0 (completely transparent)
         and 1 (completely opaque). -->

<attrname="fillAlpha"format="float" />
<!-- The specification of the operations that define the path. -->
<attrname="pathData"format="string" />
<!-- The fraction of the path to trim from the start from 0 to 1. -->
<attrname="trimPathStart"format="float" />
<!-- The fraction of the path to trim from the end from 0 to 1 . -->
<attrname="trimPathEnd"format="float" />
<!-- Shift trim region (allows visible region to include the start and end) from 0 to 1. -->
<attrname="trimPathOffset"format="float" />
<!-- sets the linecap for a stroked path. -->
<attrname="strokeLineCap"format="enum">
<enumname="butt"value="0"/>
<enumname="round"value="1"/>
<enumname="square"value="2"/>
</attr>
<!-- sets the lineJoin for a stroked path. -->
<attrname="strokeLineJoin"format="enum">
<enumname="miter"value="0"/>
<enumname="round"value="1"/>
<enumname="bevel"value="2"/>
</attr>
<!-- sets the Miter limit for a stroked path. -->
<attrname="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. -->

<attrname="fillType"format="enum">
<enumname="nonZero"value="0"/>
<enumname="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文件,展示了一些基本图形的绘制方法:

<vectorxmlns: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的资源预览窗口中查看,其效果如下:

Image

1.3 图形变换

通过attrs.xml文件中的属性定义可以发现,<group>标签支持旋转,平移和缩放变换,而<path>

下面的test2.xml示例中,修改了test1.xml中<group>的变换属性和<path>

<vectorxmlns: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>

Image

由此也可以看出<group>标签的作用就是对图形组合进行变换,所以不需要变换的<path>

1.4 渐变色

VectorDrawable中有2个色彩属性:fillColor和strokeColor。上面的例子中,所有图形都设置了单色值,但其实fillColo和strokeColor都支持渐变色。

下面的test3.xml中,展示fillColor的线性渐变和放射状渐变的颜色设置方法:

<vectorxmlns: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:attrname="android:fillColor">
<gradient
android:startX="0"
android:startY="0"
android:endX="0"
android:endY="100"
android:type="linear">

<itemandroid:offset="0"android:color="#FF0000"/>
<itemandroid: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:attrname="android:fillColor">
<gradient
android:centerX="100"
android:centerY="60"
android:gradientRadius="80"
android:type="radial">

<itemandroid:offset="0"android:color="#FF0000"/>
<itemandroid: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">

<itemandroid:offset="0"android:color="#FF0000"/>
<itemandroid:offset="1"android:color="#0000FF"/>
</gradient>


<!-- res/drawable/gradient_oval2.xml -->
<gradient
android:centerX="100"
android:centerY="60"
android:gradientRadius="80"
android:type="radial">

<itemandroid:offset="0"android:color="#FF0000"/>
<itemandroid:offset="1"android:color="#0000FF"/>
</gradient>


<!-- res/drawable/test4.xml -->
<vectorxmlns: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)为中心点,开始由红色发射状渐变为蓝色,其效果如下:

Image

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

Image

1.5 clipPath

<clipPath>定义当前绘制的剪切路径,对当前的 group 和子 group 有效。通过系统的attrs.xml文件,我们能查到<clipPath>只有两个属性,Name和PathData,其声明如下:

<!-- Defines the clip path used in VectorDrawables. -->
<declare-styleablename="VectorDrawableClipPath">
<!-- The Name of this path. -->
<attrname="name" />
<!-- The specification of the operations that define the path. -->
<attrname="pathData"/>
</declare-styleable>

下面的test5.xml中,展示clip-path的图形裁剪设置方法:

<vectorxmlns: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>的

Image

1.6 SVG导入

除了手写Android矢量图Vector文件,我们还可以使用Android Studio的Vector Asset创建工具使用系统自带的Vector,或从本地SVG中转换获取Vector。

通过project板块中右键->New->Vector Asset,或者菜单File->New->Vector Asset路径,我们可以打开Vector Asset创建工具,如下图:

Image

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

Image

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

Image

生成的baseline_add_reaction_24.xml文件,vector的alpha,tint,width和height等属性都设置了对应的值,代码如下:

<vectorxmlns: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">


<pathandroid: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,其效果如下:

Image

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

Image

SVG文件转换为Vector文件时,如果SVG中只有Path标签,Vector Asset工具可以直接显示出SVG的预览效果。如果包括了其它标签,Vector Asset工具不能预览SVG的效果,转换后的文件可能有些问题,需要我们手工修正后使用。

我们以《矢量图SVG应用探索》中基本图形的SVG文件test2.svg为例,其代码如下:

<?xml version="1.0" standalone="no"?>
<svgwidth="250"height="150"viewBox="0 0 250 150"
version="1.1"xmlns="http://www.w3.org/2000/svg">



<rectx="10"y="10"width="60"height="40"
stroke="black"fill="transparent"stroke-width="5"/>

<rectx="80"y="10"rx="10"ry="10"width="40"height="40"
stroke="black"fill="transparent"stroke-width="5"/>



<circlecx="150"cy="30"r="20"
stroke="red"fill="transparent"stroke-width="5"/>


<ellipsecx="200"cy="30"rx="20"ry="5"
stroke="red"fill="transparent"stroke-width="5"/>



<linex1="10"x2="50"y1="70"y2="110"
stroke="orange"stroke-width="5"/>


<polylinepoints="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"/>



<polygonpoints="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"/>



<pathd="M164 80Q183 42 200 80T232 80"
fill="none"stroke="blue"stroke-width="5"/>

</svg>


在浏览器打开test2.svg 文件,我们可以看到如下图形:

Image

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

Image

创建完成后,我们导出test6.xml文件,内容如下:

<vectorxmlns:android="http://schemas.android.com/apk/res/android"
android:height="150dp"
android:width="250dp"
android:viewportHeight="150"
android:viewportWidth="250">


<pathandroid:fillColor="transparent"
android:pathData="M10,10h60v40h-60z"
android:strokeColor="#000000"
android:strokeWidth="5"/>


<pathandroid: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"/>


<pathandroid: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"/>


<pathandroid:fillColor="transparent"
android:strokeWidth="5"
android:pathData="M180,30a20,5 0,1 0,40 0a20,5 0,1 0,-40 0z"
android:strokeColor="#ff0000" />


<pathandroid:fillColor="#FF000000"
android:pathData="M10,70L50,110"
android:strokeColor="#ffa500"
android:strokeWidth="5"/>


<pathandroid:fillColor="transparent"
android:pathData="M50,70l5,10l5,-5l5,15l5,-5l5,15l5,-5l5,15l5,-5"
android:strokeColor="#ffa500"
android:strokeWidth="5"/>


<pathandroid: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"/>


<pathandroid:fillColor="#00000000"
android:pathData="M164,80Q183,42 200,80T232,80"
android:strokeColor="#0000ff"
android:strokeWidth="5"/>


</vector>

从转换后的Vector文件可以看出,SVG中的基本形状元素,都被转换成了<path>

Image

但是,转换后的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"?>
<LinearLayoutxmlns: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个图标显示都清晰无锯齿,如下图所示:

Image

1.8 源码分析

了解了Vector的编写和使用,那么VectorDrawable矢量图的实现原理是什么呢?下面继续从源码来分析其实现原理。

首先ImageView从属性src中获取矢量图resourceId对应的Drawable对象,并设置给mDrawable变量,其代码如下:

///~/Android/sdk/sources/android-34/android/widget/ImageView.java
publicImageView(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
publicstatic 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
publicfinal 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
publicvoidinflate(@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);
    }
}


privatevoidinflateChildElements(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方法
publicvoidinflate(Resources r, AttributeSet attrs, Theme theme){
final TypedArray a = obtainAttributes(r, theme, attrs,
            R.styleable.VectorDrawablePath);
    updateStateFromTypedArray(a); //把xml中的属性值读取到对象属性中
    a.recycle();
}


//VFullPath类的updateStateFromTypedArray方法
privatevoidupdateStateFromTypedArray(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对象,其流程可以总结如下:

Image

获得了VectorDrawable对象和属性后,继续查看它如何显示在布局控件中,如下代码所示:

//~/Android/sdk/sources/android-34/android/widget/ImageView.java
protectedvoidonDraw(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
publicvoiddraw(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-styleablename="AnimatedVectorDrawable">
<!-- The static vector drawable. -->
<attrname="drawable" />
</declare-styleable>


<!-- Defines the target used in the AnimatedVectorDrawable. -->
<declare-styleablename="AnimatedVectorDrawableTarget">
<!-- The name of the target path, group or vector drawable. -->
<attrname="name" />
<!-- The animation for the target path, group or vector drawable. -->
<attrname="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:attrname="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>
<targetandroid:name="root">
<aapt:attrname="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>
<targetandroid:name="root">
<aapt:attrname="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"?>
<LinearLayoutxmlns: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
publicclassSecondActivityextendsActivity{
@Override
protectedvoidonCreate(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
publicvoidonAnimationEnd(Drawable drawable){
super.onAnimationEnd(drawable);
//animationDrawable2.reverse(); //反向动画
//animationDrawable2.reset();//重置回初始画面
                }
            });
            animationDrawable2.start(); //启动动画
        }
    }
}

ImageView中的动画默认会停在初始画面,需要在代码中通过start函数来启动动画。和其它Android动画一样,AnimatedVectorDrawable也可以注册动画回调来监听动画播放过程,还可以控制动画反向播放和重置回初始画面。

在App的ImageView中查看test8.xml的SVG动画效果,矢量图从开始的✔️形状,渐变成最后的✖️形状,颜色则从红色渐变到蓝色,其动画效果如下:

Image

录屏转换的gif图片清晰度较低,所以看着模糊,实际APP上看到的动效非常清晰。

2.3 源码分析

了解了Vector矢量动画animated-vector文件的编写和动画的启动,那么它是怎么把Android属性动画引入到矢量图Vector的呢?以及它支持哪些Android动画呢?下面继续从源码分析来解答这些问题。

  • 对象获取

从上一节“矢量图VectorDrawable”的源码分析中,我们可以知道DrawableInflater的inflateFromTag方法会把<vector>

同样的,<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
publicvoidinflate(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
privatestatic 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>

但AnimatedVectorDrawable实际只支持<objectAnimator>和<set>

现在不管是<objectAnimator>还是<set>

//~/Android/sdk/sources/android-34/android/animation/AnimatorInflater.java
privatestatic 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;
}


privatestatic 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);
    }
//......
}


privatestaticvoidparseAnimatorFromTypeArray(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);
    }
}


privatestatic 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
publicstatic PropertyValuesHolder ofObject(String propertyName, TypeEvaluator evaluator, Object... values){
    PropertyValuesHolder pvh = new PropertyValuesHolder(propertyName);
    pvh.setObjectValues(values);//设置动画初始和结束的PathParser.PathData值
    pvh.setEvaluator(evaluator);
return pvh;
}


publicvoidsetObjectValues(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
publicstatic 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
privatestaticvoidupdateAnimatorProperty(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>

updateAnimatorProperty方法把矢量图元素的Property绑定到属性动画的PropertyValuesHolder中。

到此,ImageView中配置的android:src被成功转换成AnimatedVectorDrawable对象,并设置到mDrawable变量了,其流程可以总结如下:

Image
  • 动画播放

AnimatedVectorDrawable创建好后,就可以通过ImageView获取到它,并调用start方法来播放矢量动画,其源码如下:

//~/projectpath/sample/src/main/java/com/test/demo/SecondActivity.java
publicclassSecondActivityextendsActivity{
@Override
protectedvoidonCreate(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
publicvoidstart(){
    ensureAnimatorSet();
if (DBG_ANIMATION_VECTOR_DRAWABLE) {
        Log.w(LOGTAG, "calling start on AVD: " +
                ((VectorDrawable.VectorDrawableState) ((AnimatedVectorDrawableState)
                getConstantState()).mVectorDrawable.getConstantState()).mRootName
                + ", at: " + this);
    }
    mAnimatorSet.start();
}


privatevoidensureAnimatorSet(){
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
privatestaticclassVectorDrawableAnimatorUIimplementsVectorDrawableAnimator{
private AnimatorSet mSet = null;
privatefinal Drawable mDrawable;
//......


    VectorDrawableAnimatorUI(@NonNull AnimatedVectorDrawable drawable) {
        mDrawable = drawable; //将SVG对应的对象保存在mDrawable
    }


@Override
publicvoidinit(@NonNull AnimatorSet set){
//......
        mSet = set.clone(); //将动画AnimatorSet克隆一份保存在mSet
        mTotalDuration = mSet.getTotalDuration();
        mIsInfinite = mTotalDuration == Animator.DURATION_INFINITE;
//......
    }


@Override
publicvoidstart(){
if (mSet == null || mSet.isStarted()) {
return;
        }
        mSet.start(); //调用AnimatorSet的start方法
        invalidateOwningView();
    }
}


//~/Android/sdk/sources/android-34/android/animation/AnimatorSet.java
privatevoidstart(boolean inReverse, boolean selfPulse){
//....
    initAnimation();// 初始化动画的属性,比如,插值器,播放时长,播放依赖等
//....
boolean isEmptySet = isEmptySet(this);
if (!isEmptySet) {
        startAnimation();//播放动画
    }
//....
}


privatevoidstartAnimation(){
    addAnimationEndListener();


// 注册动画的回调
    addAnimationCallback(0);
//....
}


privatevoidaddAnimationCallback(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
publicvoidaddAnimationFrameCallback(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));
    }
}


publicinterfaceAnimationFrameCallback{
booleandoAnimationFrame(long frameTime);
voidcommitAnimationFrame(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;
}


privateclassMyFrameCallbackProviderimplementsAnimationFrameCallbackProvider{
//获取绘制时钟同步的编导单例
final Choreographer mChoreographer = Choreographer.getInstance();


@Override
publicvoidpostFrameCallback(Choreographer.FrameCallback callback){
//Choreographer接收到Vsync信号后,会回调callback,不进一步不展开说明
        mChoreographer.postFrameCallback(callback);
    }
//......
}


privatefinal Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
publicvoiddoFrame(long frameTimeNanos){
//Choreographer接收到Vsync信号后,回调callback的doFrame方法
        doAnimationFrame(getProvider().getFrameTime());
//继续向Choreographer设置帧绘制回调,以确保下一帧绘制都能收到回调!
if (mAnimationCallbacks.size() > 0) {
            getProvider().postFrameCallback(this);
        }
    }
};


privatevoiddoAnimationFrame(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
publicbooleandoAnimationFrame(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));
        }
    }
//......
}


privatevoidpulseFrame(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
booleanpulseAnimationFrame(long frameTime){
//......
return doAnimationFrame(frameTime);
}


publicfinalbooleandoAnimationFrame(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;
}


booleananimateBasedOnTime(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
voidanimateValue(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
voidanimateValue(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
voidcalculateValue(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
privatestaticclassPathDataEvaluatorimplementsTypeEvaluator<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
publicstaticbooleaninterpolatePathData(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
voidsetAnimatedValue(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
staticabstractclassVPathextendsVObject{
//......
privatestaticfinal Property<VPath, PathParser.PathData> PATH_DATA =
new Property<VPath, PathParser.PathData>(PathParser.PathData.class, "pathData") {
@Override
publicvoidset(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;
    }

//......
publicvoidsetPathData(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对象,并播放属性动画的源码就分析完了,其流程可以总结如下:

Image

03 总结

本文介绍了SVG在Android中的实现方案Vector,讲解了VectorDrawable和AnimatedVectorDrawable的使用方法,同时通过源码进一步分析说明了Android矢量图和动画的原理,最后总结和梳理如下:

  • VectorDrawable是Android支持的静态矢量图,它属于树状结构,标签简单,比SVG支持的标签和属性都少很多,主要支持集中在基本的形状和颜色上。但正因为其简化实现,渲染时性能较好,适用于应用中的图标,按钮背景等简单图形;

  • AnimatedVectorDrawable是Android支持的动态矢量图,可以对VectorDrawable中的group,path元素进行动画,如路径变化、颜色变化等。它通过复用Android ObjectAnimator实现矢量图元素的属性动画。它适合用于按钮点击、加载动画等需要用户交互时,图形发生动态变化的场景。

VectorDrawable体积小,能在多种屏幕分辨率保存清晰度,而AnimatedVectorDrawable也很小,可以通过动画增强用户体验,使界面更加生动和吸引人。我们可以根据具体需求选择合适的矢量图方案,以提升应用的视觉效果和用户体验。