Android矢量动画实践

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

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

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

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

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

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

原谅我盗了一张图

补充 6 关于文末福利

本文Demo完整项目地址

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容