原文 :An Introduction to Icon Animation Techniques
Creative customization是material design的众多原则之一;微妙的图标动画的加入可以给用户体验带来神奇的效果,让app更生动,更自然。不幸的是,使用 VectorDrawable
来创建图标动画是一件困难的事情。这不仅仅是因为它需要花费大量的工作,还因为需要知道最终的结果是什么样子的。如果你对几种常用的动画技术不熟悉,在设计的时候是很困难的。
本文将涵盖几种创建图标动画的技术。最佳的学习方式就是通过例子来学习,因此在你阅读本文的时候你会遇到帮助你理解各个技术的demo。我希望本文至少能让你见识到图标动画的底层是如何表现的,因为我相信理解它们的工作方式是创建自己图标动画的第一步。
本文将分为以下几个小节:
- Drawing
path
s - Transforming
group
s ofpath
s - Trimming stroked
path
s - Morphing
path
s - Clipping
path
s - Conclusion: putting it all together
本文的所有动画在 GitHub上都有相应的AnimatedVectorDrawable版本。
Drawing paths
在我们开始创建图标动画之前,我们首先需要知道它们是如何绘制的。在安卓中,我们将使用相对较新的VectorDrawable
类来创建图标。VectorDrawable类类似于web上的SVG:它们允许我们通过一些列被称为path的线条和形状来创建可缩放的,分辨率无关的资源文件。每个path的形状由一个绘制命令序列决定,这些命令是由空格和“,”组成的字符串,与SVG path data 节点是相同的。这个节点定义了许多不同类型的命令,其中一些总结如下:
Command | Description |
---|---|
M x,y |
Begin a new subpath by moving to (x,y) . |
L x,y |
Draw a line to (x,y) . |
C x1,y1 x2,y2 x,y |
Draw a cubic bezier curve to (x,y) using control points (x1,y1) and (x2,y2) . |
Z |
Close the path by drawing a line back to the beginning of the current subpath. |
所有的path都以两种形式出现:填充或者线条。如果是填充,形状的内部将被涂色;如果path是线条,则只是勾勒形状的轮廓。两种类型的path都有自己的一套动画特征属性,以修改它们的外观:
Property name | Element type | Value type | Min | Max |
---|---|---|---|---|
android:fillAlpha |
<path> |
float |
0 |
1 |
android:fillColor |
<path> |
integer |
- - - | - - - |
android:strokeAlpha |
<path> |
float |
0 |
1 |
android:strokeColor |
<path> |
integer |
- - - | - - - |
android:strokeWidth |
<path> |
float |
0 |
- - - |
让我们用一个例子来说明这是怎么工作的。假设我们想为一个音乐应用创建一个播放,暂停,和录制图标。我们可以用一个path来代表每个图标:
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportHeight="12"
android:viewportWidth="12">
<!-- This path draws an orange triangular play icon. -->
<path
android:fillColor="#FF9800"
android:pathData="M 4,2.5 L 4,9.5 L 9.5,6 Z"/>
<!-- This path draws two green stroked vertical pause bars. -->
<path
android:pathData="M 4,2.5 L 4,9.5 M 8,2.5 L 8,9.5"
android:strokeColor="#0F9D58"
android:strokeWidth="2"/>
<!-- This path draws a red circle. -->
<path
android:fillColor="#DB4437"
android:pathData="M 2,6 C 2,3.8 3.8,2 6,2 C 8.2,2 10,3.8 10,6 C 10,8.2 8.2,10 6,10 C 3.8,10 2,8.2 2,6"/>
</vector>
三角形的播放图标和圆形的录制图标都是填充效果的,分别用橙色和红色填充。而暂停按钮,则是粗细为2的绿色线条。图1描绘了每个path命令在12x12的网格中的样子:
-
Path commands:
M 4,2.5
L 4,9.5
L 9.5,6
Z
-
Path commands:
M 4,2.5
L 4,9.5
M 8,2.5
L 8,9.5
-
Path commands:
M 2,6
C 2,3.8 3.8,2 6,2
C 8.2,2 10,3.8 10,6
C 10,8.2 8.2,10 6,10
C 3.8,10 2,8.2 2,6
图 1. 理解如何使用 path command绘制path。图解中的数字跟命令执行后path的位置是相匹配的。左上角坐标是(0,0),右下角坐标是(12,12)。每个图标的源码可以在 GitHub上找到。
就如我们前面所提到的,VectorDrawable的好处之一就是提供分辨率无关的图片,可以随意缩放大小而不丢失质量。这样既可以提高效率,又能带来方便:开发者不再需要为不同分辨率的设备配置不同大小的png图片,这也间接减小了APK的大小。但是我们使用VectorDrawable的原因是可以使用AnimatedVectorDrawable
类对path应用动画。AnimatedVectorDrawable是把VectorDrawable和ObjectAnimator联系在一起的纽带:VectorDrawable为每个path(或者一组path)设置一个唯一的名称,AnimatedVectorDrawable把这些名称映射为相应的ObjectAnimator。就如我们即将看到的那样,对VectorDrawable中的元素应用动画可以产生非常强大的效果。
Transforming groups of paths
在前面的小节中我们学到了如何通过直接修改path的属性改变它的外观,比如透明度和颜色。除此之外,VectorDrawable还支持使用
Property name | Element type | Value type |
---|---|---|
android:pivotX |
<group> |
float |
android:pivotY |
<group> |
float |
android:rotation |
<group> |
float |
android:scaleX |
<group> |
float |
android:scaleY |
<group> |
float |
android:translateX |
<group> |
float |
android:translateY |
<group> |
float |
理解嵌套group transformation执行的顺序是非常重要的。有两条规则:(1)子group继承父group所应用的变换,(2)同一group上的变换是按照scale, rotation, 和translation的顺序执行的。举个例子,下面是应用到前面提到的播放,暂停,录制图标上的group transformation:
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportHeight="12"
android:viewportWidth="12">
<!-- Translate the canvas, then rotate, then scale, then draw the play icon. -->
<group android:scaleX="1.5" android:pivotX="6" android:pivotY="6">
<group android:rotation="90" android:pivotX="6" android:pivotY="6">
<group android:translateX="2">
<path android:name="play_path"/>
</group>
</group>
</group>
<!-- Rotate the canvas, then translate, then scale, then draw the pause icon. -->
<group android:scaleX="1.5" android:pivotX="6" android:pivotY="6">
<group
android:rotation="90" android:pivotX="6" android:pivotY="6"
android:translateX="2">
<path android:name="pause_path"/>
</group>
</group>
<!-- Scale the canvas, then rotate, then translate, then draw the record icon. -->
<group android:translateX="2">
<group
android:rotation="90"
android:scaleX="1.5"
android:pivotX="6"
android:pivotY="6">
<path android:name="record_path"/>
</group>
</group>
</vector>
变换后的图标如图2。你可以改变复选框来看看不同的变换组合对显示结果的影响!
图 2. 不同的 <group>
transformation结合起来应用到播放,暂停,录制图标上的效果。复选框的顺序和前面示例代码中transformations应用的顺序是一致的。每个图标的安卓代码可以在 GitHub上获取。
把group transformation链式的组合在一起让实现各种各样的酷炫效果成为了可能。图3演示了三个这样的例子:
-
expand/collapse 图标是用两个矩形path绘制的,当被点击的时候,这两个path同时旋转90度并垂直移动,从而形成了图中的效果。
-
闹钟图标使用两个矩形path来绘制闹铃,当被点击的时候,包含了这两个path的
围绕圆心前后旋转,制造出响铃的效果。 -
radio button图标动画是我最喜欢的动画,因为它简单,只使用了两个path:内部填充效果的点以及外部的圆环。当radio button在unchecked 和checked状态之间变换的时候,有三个属性动画:
Time Outer ring strokeWidth
Outer ring scale{X,Y}
Inner dot scale{X,Y}
0 2 1 0 0.333 18 0.5 0 0.334 2 0.9 1.5 1 2 1 1 特别注意动画的前三分之一部分,当外部圆环的线条宽度增大和scale缩小同时进行的时候,让它看起来就像圆环往中心缩进去似的,非常酷的效果!
图 3。理解如何 <group>
transformation创建图标动画。每个图标的安卓源码可以在GitHub上获取: (a) expand to collapse, (b) alarm clock, 和 (c) radio button。点击图标开始动画。
最后一个使用 group transformation的例子是横向的indeterminate模式的进度条。一个横向的indeterminate进度条由三部分组成:一个半透明的背景和两个内部矩形path。在动画期间,两个矩形path不同程度的做横向移动和缩放。点击图4的复选框可以看到每个transformation是如何对动画起作用的!
图 4。理解如何使用 scale 和 translation让horizontal indeterminate progress indicator动画起来(source code)。
Trimming stroked paths
描边路径(stroked path)的一个不太知名的特点是它们可以被trimmed。 给定一个描边路径,在绘制之前,我们可以选择只显示其一部分。在安卓中,这是使用下面的动画属性来完成的:
Property name | Element type | Value type | Min | Max |
---|---|---|---|---|
android:trimPathStart |
<path> |
float |
0 |
1 |
android:trimPathEnd |
<path> |
float |
0 |
1 |
android:trimPathOffset |
<path> |
float |
0 |
1 |
trimPathStart决定了path可见部分是从何处开始的,trimPathEnd决定从何处结束。如果愿意,也可以加上trimPathOffset在开始和结束值上追加一个值。图 5 演示了这是如何工作的-滑动滑杆查看不同值对显示结果的影响!注意,trimPathStart 大于trimPathEnd也是被允许的;如果出现了这种情况,可见部分将从后面绕一圈回到前面。
图 5. 理解描边路径中 trimPathStart
, trimPathEnd
, 和 trimPathOffset
属性的效果
能对这样的三个属性做动画打开了一个无限可能的世界。图 6 展示了四个这样的例子:
-
指纹图标包含了5个描边路径,每个路径的trim path的开始和结束值分别初始化为0和1。当隐藏的时候,快速过渡到0 知道图标不可见,当再次显示的时候再快速过渡回1。手写效果的图标与之类似,只不过不是所有path一起动画,而是依次执行,就像手写出单词的效果。
-
搜索/返回图标使用了一种trim path动画的机智组合来做到搜索图标区域和返回图标区域之间的无缝切换。选中 ‘show trim paths’复选框你会看到在动画过渡到新状态的时候,
trimPathStart
和trimPathEnd
的值是如何影响区域的相对位置的。选中‘slow animation’复选框你还能观察到区域的可见长度随着时间的变换:开始稍微有点扩展,最后缩小,这样就能制造出不太明显的“伸展”效果,看起来更自然。这个效果的实现很简单,只不过是让其中一个trim属性稍作延迟,使其看起来就像path的一端动画速度快于另一端。 -
Google IO 2016图标中的每一个数字包含4个path,每一个都有不同的描边颜色,trim path的start和end值覆盖了数字总长度的1/4。然后每个path的trimPathOffset执行从0到1的动画,从而制造出图中的效果。
图 6. 理解如何使用trimming stroked paths制造图标动画。每个图标的安卓源码可以在github上得到: (a) fingerprint, (b) search to back arrow, (c) cursive handwriting, and (d) Google IO 2016。点击图标可以开始各自的动画。
最后, 图 7 展示了一个描边的 trim path 是如何用于实现我们熟悉的圆形indeterminate进度条的。这个图标包含了单个圆形的描边路径,按照下面的方式动画:
-
包含了进度条path的<group>在4,444毫秒内从0° 到 720°旋转。
-
进度条的trim path offset 在1,333毫秒内执行从0过渡到0.25的动画。
-
进度条的一部分在1,333毫秒内截取,动画经历了下面的这些值:
Time trimPathStart
trimPathEnd
trimPathOffset
0 0 0.03 0 0.5 0 0.75 0.125 1 0.75 0.78 0.25 在t=0.0的时候进度条最小(只有3%可见)。在t = 0.5的时候进度条伸展到最大(75%可见)。在t = 1.0的时候进度条缩小到最小值,就像动画重新开始了一样。
图 7.理解如何使用旋转和 trim path 制造出圆形indeterminate进度指示的效果(source code)。
Morphing paths
本文将讲到的最高级的图标动画技术是path morphing。目前只有5.0及以上的系统支持,path morphing可以让我们通过对两个path的绘制命令(android:pathData属性)做过渡动画实现path形状的无缝切换。有了path morphing,我们可以将一个+号变成减号,将播放图标变成一个暂停图标,或者一个溢出菜单图标变成返回箭头,就像 图 8 中那样。
Property name | Element type | Value type |
---|---|---|
android:pathData |
<path> |
string |
要想实现 path morphing动画所要考虑的第一件事就是path是否相互兼容。要让path A 变成 path B
,必须满足以下条件:
- A和B具有相同数量的绘制命令。
- A的第i个绘制命令的类型必须和B的第i 个绘制命令的类型相同。
- A的第i个绘制命令的参数必须和B的第i 个绘制命令的参数相同。
如果其中任意一个条件不满足(比如企图将L命令变为C命令,或者一个两个坐标的L命令变成4个坐标的L命令),应用将返回异常并崩溃。这些规则必须强制满足的原因来自path morphing动画的底层实现。在动画开始之前,framework从每个path的 android:pathData
属性提取出命令类型和它的坐标。如果上面的条件都满足,那么framework可以假设两个path之间的唯一区别就是绘制命令中的坐标。在这个条件之下,framework可以在每个新的帧绘制相同顺序的绘制命令,根据当前的动画进度重新计算每个坐标的值。图 8很好的演示了这个概念。首先取消选中 ‘animate rotation’,然后选中‘show path coordinates’ 和 ‘slow animation’复选框。注意每个path的红色坐标在动画期间是如何变化的:它们从path A中的开始位置直线移动到path B。Path morphing 动画就是这么简单!
图 8. 理解如何使用 path morphing创建图标动画。每个图标动画的安卓源码在GitHub上: (a) plus to minus, (b) cross to tick, (c) drawer to arrow, (d) overflow to arrow, (e) play to pause to stop, and (f) animating digits. 点击每个图标开始动画。
虽然从概念上讲很简单,但是path morphing动画有时实现起来却复杂又费时。比如,为了让两个path相兼容,你通常需要手动调整开始和结束path,根据path的复杂程度,这里往往是最花时间的地方。下面列出的是我总结的几个技巧:
-
为了让一个简单的path和复杂的path相兼容,通常需要添加冗余坐标。在图 8中几乎所有的例子都添加了冗余的坐标。比如在+号到-号的动画中,我们可以只用4个命令就能绘制出-号,但是+号则需要12个命令,因此为了让这两个path兼容我们在-号的命令中增加了8个无用的绘制命令。比较这两个path的drawing command strings ,看看你能否识别出这些冗余的坐标!
-
一个三阶贝塞尔曲线可以用来绘制一条直线,方法是让它的两个控制点分别和起点和终点相等。当我们需要把L命令转变成c命令的时候,这是很有用的(比如上面例子中溢出图标到箭头图标的动画)。还可以使用一个或者多个三阶贝塞尔曲线来绘制模拟elliptical arc command ,就如我在这里here讨论的那样。如果你发现自己遇到了要把C命令转变成A命令的情况,这也是很有用的。
-
有时不管你怎么做,path morphing动画都不好看。根据我的经验,在动画中添加一个180度或者360度的旋转可以显著提高动画的美感:额外添加的旋转可以转移眼睛的注意力。而新增的动画层可以让动画看起来更响应用户的触摸动作。
-
path morphing 动画最终是由每个path绘制命令的坐标的相对位置决定的。最好尽可能的让坐标需要移动的距离更小:距离越小,morphing 动画就越自然。
Clipping paths
我们将要讲的最后一个技术是<clip-path> (剪裁)动画。一个 clip path限制了画布上可以被绘制的区域-clip path区域之外的所有元素都不会被绘制。通过对区域的边界做动画,可以做出很多效果来。
Property name | Element type | Value type |
---|---|---|
android:pathData |
<clip-path> |
string |
一个 <clip-path>
的边界可以通过对它的path命令的差别来做动画。图9可以帮助你更好的理解这些动画是如何工作的。选中‘show clip paths’复选框将显示代表当前 <clip-path>
的红色遮罩层。 Clip path对于实现填充动画尤其有用,就如下面演示的hourglass和心形的填充/破碎效果中那样。
Figure 9. Understanding how <clip-path>
s can be used to create icon animations. Android source code for each is available on GitHub: (a) hourglass, (b) eye visibility, and (c) heart fill/break. Click each icon to start its animation.
总结: putting it all together
如果你读到了这里,意味着你已经有了从头开始创建图标动画所需要的所有基础知识!让我们用一个例子来为这篇冗长的文章做个总结吧!考虑 图 10中的进度图标,它对以下6个属性做了动画:
- Fill alpha (at the end when fading out the downloading arrow).
- Stroke width (during the progress indicator to check mark animation).
- Translation and rotation (at the beginning to create the ‘bouncing arrow’ effect).
- Trim path start/end (at the end when transitioning from the progress bar to the check mark).
- Path morphing (at the beginning to create the ‘bouncing line’ effect, and at the end while transitioning the check mark back into an arrow).
- Clip path (vertically filling the contents of the downloading arrow to indicate indeterminate progress).
Figure 10. A downloading progress icon animation that demonstrates a combination of several techniques discussed in this blog post. Android source code is available on GitHub: (a) in-progress download and (b) download complete. Click the icon to start its animation.
这就是本文的所有内容了...感谢阅读!记得为文章点赞或者有问题在下面留言。注意本文的所有图标动画在GitHub上都有(还有些其它的)相应的AnimatedVectorDrawable版本。如果你想偷偷用在自己的app中尽管用吧!
bugs报告和反馈
如果你发现本文的demo存在错误,在这里 here报告。所有的动画在我的Chrome上都是可以运行的。我才学JavaScript几周而已,如果哪里出现了错误也不必大惊小怪。我想让这篇博客做到完美,所以真的非常感激能给我指出错误。
特别感谢
我想对Nick Butcher表示非常感谢,没有他的帮助和建议我可能根本不会写这篇文章!本文有几个动画是从他的开源项目Plaid借来的,如果你还没有看过这个项目,强烈建议你去看一遍。我还要感谢 Roman Nurik 提供的 Android Icon Animator 工具以及为图10中的动画带来的灵感。最后我还要感谢 Sriram Ramani 的 blog post on number tweening一文对图8中数字动画demo的启发。