矢量图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>标签,支持的标签很少,主要包括:<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>标签支持设置形状内的颜色,轮廓线的颜色和宽度,其最重要的pathData属性可设置图形绘制命令。

下面的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>的属性和SVG中的属性基本一一对应,其中pathData对应了SVG中的d属性,绘制命令也完全一致,包括了10个命令:M,L,H,V,C,S,Q,T,A,Z。同样的,命令L可以省略不写。命令的具体含义,可以参考之前的文章《矢量图SVG应用探索》。

将上面的文件拷贝到 projectpath/src/main/res/drawable/test1.xml目录,可以在Android Studio的资源预览窗口中查看,其效果如下:

1.3 图形变换

通过attrs.xml文件中的属性定义可以发现,<group>标签支持旋转,平移和缩放变换,而<path>标签不支持变换属性。<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>就不需要<group>来包裹。

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>的标签上面,否则不能对<path>进行裁剪。如test5.xml中,如果放<clip-path>放在<path>下面,oval1还是保持原样,而放在<path>上面,则只有在<clip-path>区域内的oval1才会显示出来。我们在Android Studio的资源预览窗口中查看test5.xml, 其效果如下:

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>标签,导致预览窗口中不能正确显示出SVG的形状,效果如下:

创建完成后,我们导出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>标签,在预览窗口中看到转换后的矢量图形,其效果和SVG文件一样,显示如下:

但是,转换后的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>标签的pathData属性为例说明,其源码如下:

//~/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也很小,可以通过动画增强用户体验,使界面更加生动和吸引人。我们可以根据具体需求选择合适的矢量图方案,以提升应用的视觉效果和用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值