Android矢量动画实践

4,174 阅读8分钟

之前的文章里,有朋友评论说饿了么的动画是使用AnimatedVectorDrawable来实现的。这个东西虽然原来也知道,但是一直没有切实的使用过。刚好昨天有看到一个蛮帅的矢量动画(文末福利),有了兴趣,特意来抽空撸了一个demo来体验下。

先来看看一些我撸的一些demo(部分svg资源及动画搜集自网络)。

2017-08-17_21-26-00.gif
2017-08-17_21-26-00.gif

效果不错对不对,不仅如此,这些效果完全使用资源文件即可完成,java代码里只要简单的startAnimator即可。通过网络资源撸出了这些效果之后,要开始系统的认知一下了。

SVG 和 VectorDrawable,AnimatedVectorDrawable

相较我们通常使用的png,jpg等格式的位图(Bitmap),SVG拥有体积相对较小,通过描述的形式记录形状,因此可以适应各种大小分辨率而不会失真。

而在Android中,我们不能直接使用原始的 .svg 格式图片,而是需要将其转化为 VectorDrawable,可以理解为一个XML格式的svg文件,即矢量图形在android中的原始资源。

如果只是单纯的运用VectorDrawable,似乎作用就只有缩小apk资源文件体积了,还要考虑svg运行时才计算所造成的额外cpu消耗(将形状描述转化为图形)。但是有了AnimatedVectorDrawable之后,就完全不一样了。

AnimatedVectorDrawable通过ObjectAnimator属性动画控制VectorDrawable,利用矢量图形的特性,从而达成各种炫酷的动画效果。

略丑的关系图
略丑的关系图

通过上述撸的Demo,我大概把它的主要动画效果分为以下三种。

1.两个图形之间的无缝切换。

2.按路径绘制图像。

3.分组控制图像不同部分。

而以上三种特性又可以互相组合,搭配其他的属性动画,实现更复杂的效果。

图三的机器人实际上就是控制了头部和手臂进行y轴的平移动画,而这头,手,身体是属于同一张SVG图片。Demo中前两个案例也是在切换动画的同时配合了旋转的属性动画。

VectorDrawable的格式

我们先比较一下小机器人矢量图的SVG代码和其VectorDrawable代码。

SVG源码

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948)  -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     width="500px" height="500px" viewBox="0 0 500 500" enable-background="new 0 0 500 500" xml:space="preserve">
<g id="max_width__x2F__height" display="none">
    <path display="inline" d="M499.001,1v498H1V1H499.001 M500.001,0H0v500h500.001V0L500.001,0z"/>
</g>
<g id="androd">
    <path fill="#9FBF3B" d="M301.314,83.298l20.159-29.272c1.197-1.74,0.899-4.024-0.666-5.104c-1.563-1.074-3.805-0.543-4.993,1.199
        L294.863,80.53c-13.807-5.439-29.139-8.47-45.299-8.47c-16.16,0-31.496,3.028-45.302,8.47l-20.948-30.41
        c-1.201-1.74-3.439-2.273-5.003-1.199c-1.564,1.077-1.861,3.362-0.664,5.104l20.166,29.272
        c-32.063,14.916-54.548,43.26-57.413,76.34h218.316C355.861,126.557,333.375,98.214,301.314,83.298"/>
    <path fill="#FFFFFF" d="M203.956,129.438c-6.673,0-12.08-5.407-12.08-12.079c0-6.671,5.404-12.08,12.08-12.08
        c6.668,0,12.073,5.407,12.073,12.08C216.03,124.03,210.624,129.438,203.956,129.438"/>
    <path fill="#FFFFFF" d="M295.161,129.438c-6.668,0-12.074-5.407-12.074-12.079c0-6.673,5.406-12.08,12.074-12.08
        c6.675,0,12.079,5.409,12.079,12.08C307.24,124.03,301.834,129.438,295.161,129.438"/>
    <path fill="#9FBF3B" d="M126.383,297.598c0,13.45-10.904,24.354-24.355,24.354l0,0c-13.45,0-24.354-10.904-24.354-24.354V199.09
        c0-13.45,10.904-24.354,24.354-24.354l0,0c13.451,0,24.355,10.904,24.355,24.354V297.598z"/>
    <path fill="#9FBF3B" d="M140.396,175.489v177.915c0,10.566,8.566,19.133,19.135,19.133h22.633v54.744
        c0,13.451,10.903,24.354,24.354,24.354c13.451,0,24.355-10.903,24.355-24.354v-54.744h37.371v54.744
        c0,13.451,10.902,24.354,24.354,24.354s24.354-10.903,24.354-24.354v-54.744h22.633c10.569,0,19.137-8.562,19.137-19.133V175.489
        H140.396z"/>
    <path fill="#9FBF3B" d="M372.734,297.598c0,13.45,10.903,24.354,24.354,24.354l0,0c13.45,0,24.354-10.904,24.354-24.354V199.09
        c0-13.45-10.904-24.354-24.354-24.354l0,0c-13.451,0-24.354,10.904-24.354,24.354V297.598z"/>
</g>
</svg>

作为VectorDrawable

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:viewportWidth="500"
    android:viewportHeight="500"
    android:width="500px"
    android:height="500px">
    <group android:name="android">
        <group android:name="head_eyes">
            <path
                android:name="head"
                android:fillColor="#9FBF3B"
                android:pathData="M301.314,83.298l20.159-29.272c1.197-1.74,0.899-4.024-0.666-5.104c-1.563-1.074-3.805-0.543-4.993,1.199L294.863,80.53c-13.807-5.439-29.139-8.47-45.299-8.47c-16.16,0-31.496,3.028-45.302,8.47l-20.948-30.41c-1.201-1.74-3.439-2.273-5.003-1.199c-1.564,1.077-1.861,3.362-0.664,5.104l20.166,29.272c-32.063,14.916-54.548,43.26-57.413,76.34h218.316C355.861,126.557,333.375,98.214,301.314,83.298" />
            <path
                android:name="left_eye"
                android:fillColor="#FFFFFF"
                android:pathData="M203.956,129.438c-6.673,0-12.08-5.407-12.08-12.079c0-6.671,5.404-12.08,12.08-12.08c6.668,0,12.073,5.407,12.073,12.08C216.03,124.03,210.624,129.438,203.956,129.438" />
            <path
                android:name="right_eye"
                android:fillColor="#FFFFFF"
                android:pathData="M295.161,129.438c-6.668,0-12.074-5.407-12.074-12.079c0-6.673,5.406-12.08,12.074-12.08c6.675,0,12.079,5.409,12.079,12.08C307.24,124.03,301.834,129.438,295.161,129.438" />
        </group>
        <group android:name="arms">
            <path
                android:name="left_arm"
                android:fillColor="#9FBF3B"
                android:pathData="M126.383,297.598c0,13.45-10.904,24.354-24.355,24.354l0,0c-13.45,0-24.354-10.904-24.354-24.354V199.09c0-13.45,10.904-24.354,24.354-24.354l0,0c13.451,0,24.355,10.904,24.355,24.354V297.598z" />
            <path
                android:name="right_arm"
                android:fillColor="#9FBF3B"
                android:pathData="M372.734,297.598c0,13.45,10.903,24.354,24.354,24.354l0,0c13.45,0,24.354-10.904,24.354-24.354V199.09c0-13.45-10.904-24.354-24.354-24.354l0,0c-13.451,0-24.354,10.904-24.354,24.354V297.598z" />
        </group>
        <path
            android:name="body"
            android:fillColor="#9FBF3B"
            android:pathData="M140.396,175.489v177.915c0,10.566,8.566,19.133,19.135,19.133h22.633v54.744c0,13.451,10.903,24.354,24.354,24.354c13.451,0,24.355-10.903,24.355-24.354v-54.744h37.371v54.744c0,13.451,10.902,24.354,24.354,24.354s24.354-10.903,24.354-24.354v-54.744h22.633c10.569,0,19.137-8.562,19.137-19.133V175.489H140.396z" />
    </group>
</vector>

我们明显能看到一些共通的标签。类似 width,height, g 和 group,path等。 其中svg源码中标签比较多,我们不需要去关心。SVG最重要的一部分,就是其中的path——路径了。

之前提到过svg是通过描述形状来记录图形,即path。玩过画笔的都了解吧,从一个点画到另一个点。而宽高则标识了画布的大小,我们路径的坐标应该在相应的宽高之内,超出去就看不到了。

通常Path是一个完整的路径。机器人中,有手臂,身体等不同的部分,因此也就有了多个Path来描述。这个VectorDrawable中给各部分都命名并分组,看起来是很清楚的。而通常简单的形状使用一个Path即可描述。

因此,只要有了对应的Path,我们就能把一个SVG图像转化为Android可用的VectorDrawable了。

让VectorDrawable动起来

以demo中的第一个效果为例。

先看看它的XML中组成。

<ImageView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:onClick="btnClick"
        android:src="@drawable/animated_play_pause"
        android:background="@color/colorPrimary"
        android:layout_marginTop="20dp"
        />

ImageView引用的图片资源为 animated_play_pause

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vector_play">

    <!-- 变化内容 -->
    <target
        android:animation="@animator/animator_play_pause"
        android:name="play"/>

    <!-- 旋转 -->
    <target
        android:animation="@animator/animator_rotate"
        android:name="playgroup"/>

</animated-vector>

这个就是传说中的 AnimatedVectorDrawablele。这个XML中,首先用drawable标签声明了一个默认显示的VectorDrawable对象 vector_play,这个是我们的矢量播放键。

其中定义了两个target目标,有两个参数,分别是animation,和name,前者定义了使用的动画效果,后者则是动画效果针对的目标对象。这个name必须与VectorDrawable对象的path和group声明的name相同,否则在开始动画时会找不到对象而报错。

两个动画

<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:valueType="pathType"
    android:propertyName="pathData"
    android:valueFrom="M 3,2 L 7,5 L7,5 L3,5z M 3,8 L7,5 L7,5 L3,5z"
    android:valueTo="M 2,2 L 8,2 L8,4 L2,4z M 2,8 L8,8 L8,6 L2,6z"
    />
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:propertyName="rotation"
    android:valueType="floatType"
    android:valueFrom="0"
    android:valueTo="-90"/>

第一个就是从播放键切换到暂停键的动画了,和使用普通的属性动画一样,只不过valueType和propertyName分别为pathType,pathData,表明动画针对路径变化。
是的,矢量动画的切换效果只是从一个形状的路径切换到另外一个路径而已。
ValueFrom的参数实际上就是播放键的路径参数,valueTo是暂停键路径。

旋转动画就不多讲了,不过一个需要注意的点是,在target中的两个name是不同的。其中 play 是 path 的name,而 palygroup则是path所在group的name。

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="100dp"
    android:height="100dp"
    android:viewportHeight="10"
    android:viewportWidth="10">

    <group
        android:name="playgroup"
        android:pivotX="5"
        android:pivotY="5">

        <path
            android:name="play"
            android:fillColor="#fff"
            android:pathData="M 3,2 L 7,5 L7,5 L3,5z M 3,8 L7,5 L7,5 L3,5z"/>
    </group>

</vector>

在使用时,我们不能直接针对Path使用例如旋转平移等属性动画,而是要将目标定位包裹path的group,否则会出现如下错误。FullPath不支持的动画属性。


这样,我们一个形状变化加旋转的AnimatedVectorDrawable就完成了,点击触发动画,在java中如下:

    public void btnClick(View view) {
        ImageView imageView = (ImageView) view;
        Drawable drawable = imageView.getDrawable();
        if (drawable instanceof Animatable) {
            ((Animatable) drawable).start();
        }
    }

补充1 路径绘制

与变换动画对应的还有一个绘制路径的动画。实际上也相当简单,是指将对应的参数变为了trimPathEnd,值得变化是0到1,代表完全绘制。以上都是些比较基础的运用,可以下载文末我的Demo获取完整的svg资源,自己尝试。

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="trimPathEnd"
    android:valueFrom="0"
    android:valueTo="1"
    android:duration="5000"
    android:valueType="floatType"
    android:interpolator="@android:interpolator/linear"/>

补充 2 animated-selector 多个animator-vector集合。

当我们使用animator_vector时,不论是绘制路劲还是形状切换,都是只有from to两个状态,那么在我们切换完成之后呢,如果想要继续切换第三个路径,或者是切换回去,这时候就需要用到animator-selector。

类似于选择器,通过不同的状态来执行不同的animator-vector,从而达到在几个不同路径的来回切换效果。如同demo中的爱心与twitter来回切换效果。


demo中searchbar,则是结合trimPath的效果。



补充 3 Path语法

上述代码中有列出许多Path,虽然我们并不需要手动计算矢量图的路径,但是还是需要清除相关的含义。

M: move to 移动绘制起点(Mx,y)
L:line to 直线画到点(Lx,y)
H:横向连线 (Hx)
V:纵向连线 (Vy)
Z:close 闭合首尾无参
C:cubic bezier 三次贝塞尔曲线 (x1,y1,x2,y2)
Q:quatratic bezier 二次贝塞尔曲线(x1,y1,x2,y2,x3,y3)
A: ellipse 圆弧

每个命令都有大小写形式,大写代表后面的参数是绝对坐标,小写表示相对坐标。参数之间用空格或逗号隔开。
感兴趣自己看官方文档,玩死人不偿命系列。

补充 4 关于坐标匹配

实际上并不是任何两个SVG都可以无缝切换,如果想要让两个图形能够合理过渡,开始找了两个图形想要切换时,通常会出现 Can't morph from x to y 的错误。因此必须保持两个路径的格式匹配。

image.png
image.png

通常软件生成的路径千奇百怪,路径如果复杂了则非常难改。
VectAlign 是github上的一个开源项目,主要功能就是通过计算修改两个SVG的路径使其可以无缝切换。


补充 5 一个非常棒的矢量动画库RichPath

效果很棒,在代码中自如的控制Path和属性,比起纯资源文件可操作性更高, 嗯,效果比我的炫酷多了...

原谅我盗了一张图
原谅我盗了一张图

补充 6 关于文末福利

本文Demo完整项目地址

自制矢量动画实践之如何摆脱UI ???....这里

参考内容及部分svg素材来源

VectorDrawable系列
简书同好的《高级动画》
文末福利,坑了我的炫酷效果《anime.js 实战:实现一个 SVG 形变(morphing)动画》