Android动画:行云流水的矢量图标动画

8,447 阅读10分钟

前言

我们在日常使用各种app的时候,会发现原来越多下面这类型的矢量图标动画。图标动画是material design所推荐的图标效果。当然对我来说,炫酷的效果就是我学习矢量图标动画的一个很充分理由。

adp-delightful-details
adp-delightful-details

VectorDrawable

SVG和VectorDrawable
  • 矢量图:矢量图和传统的png、jpg等图片格式,是典型的渔和鱼的区别。矢量图存储的是图片画出来的方法,而不是像素点的排列,所以无论矢量图放大多少倍,只要按照等比例缩放后的路径把图标画出来即可,不存在马赛克的问题。我们电脑中显示的文字就是这么一个原理。
  • svg是最常见的矢量图格式,而在Android里面,我们使用的是VectorDrawable。
  • 一般来说,矢量图的生成是不需要我们去关心的,如果需要自己去找矢量图的话,可以去iconfont找一找。
  • SVG2VectorDrawable是一个很有用的AndroidStudio上面的插件,帮助我们把svg转化为vectorDrawable。
VectorDrawable文件和svg指令

了解一些svg指令,知道矢量图是怎么画出来的,对我们以后的开发有好处,我们可以从一个简单的VecotrDrawable文件入手。

一个绿色的小勾

<?xml version="1.0" encoding="utf-8"?>
<!--res/drawable/vd_check.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="100dp"
    android:height="100dp"
    android:viewportHeight="24"
    android:viewportWidth="24">
    <path
        android:name="check"
        android:pathData="M4,10 L9,16 L20,4"
        android:strokeColor="#35931d"
        android:strokeWidth="3" />
</vector>

这个绿色和谐的小勾是我用上面的vd_check文件画出来的,我们来解读下这个文件:

  1. vector标签:表示这是一个矢量图。
    • viewportHeight/viewWidth:矢量图的长宽,之后画图也是按此长宽来画。图标的左上角是(0,0),右下角是(viewWidth,viewHeight)。
  2. group标签:group有一些path没有的属性,如果要用这些属性做动画,那就只能path外嵌套多一层group标签了。
    • name:动画会通过name寻找到此对象。
    • rotation|scaleX|pivotX..:这些属性都很熟悉了吧
  3. path标签:连续的线或面,矢量图就是有一个或多个path组成的。
    • name:动画会通过name寻找到此对象。
    • storkeColor: 线段的颜色。
    • strokeWidth: 线段的宽度。
    • strokeAlpha: 线段的透明度。
    • strokeLineCap: 线段末端的样式 butt(断开)|round(圆角)|square(直角)
    • fillColor: 填充的颜色。
    • fillAlpha:填充透明度。
  4. pathData属性:pathData是Path的一个属性,他里面便是用来描绘path的svg语言。我们只需要认识几个关键词就可以看懂了。
关键字 解释
M x,y 把画笔移动到从(x,y)这个点。一般代表着一段path的开始。
L x,y 画一条连接到(x,y)的线段。
Q x1,y1 x,y 贝塞尔二阶曲线。经过(x1,y1)到达(x,y)。
C x1,y1 x2,y2 x,y 贝赛尔三阶线。经过(x1,y1)和(x2,y2)到达(x,y)。
Z 闭合path。画一段到起点的线段。

现在回过头看和谐小勾的pathData,就很简单了:

M4,10 L9,16 L20,4

从(4,10)开始,画一条到(9,16)的线段,再画一条到(20,4)的线段。一顿一拉,绿色小勾跃然纸上。

当然,如果遇到比小勾更加复杂的情况,比如一个完美的心形,或者广州塔的图标,那还是乖乖的找ui帮你生成svg比较好。

animated-vector

既然我们有了矢量图,那就让矢量图动起来吧。说起做动画,当然是属性动画来一发啦!

  • group和path各有一些独自的属性,所以按需去嵌套group吧。
  • 注意加name属性,我们的动画会通过name去找到对应的作用对象。 这是我修改后的vector,增加了一个group。
<?xml version="1.0" encoding="utf-8"?><!--vd_check.xml-->
<!--vd_check.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="100dp"
    android:height="100dp"
    android:viewportHeight="24"
    android:viewportWidth="24">
    <group
        android:name="g_rotation"
        android:pivotX="12"
        android:pivotY="12"
        android:rotation="0">
            <path
                android:name="check"
                android:pathData="M4,10 L9,16 L20,4"
                android:strokeAlpha="1.0"
                android:strokeColor="@color/colorPrimary"
                android:strokeLineCap="round"
                android:strokeWidth="1" />
    </group>
</vector>

我们要加什么动画呢?嗯、、旋转,透明度,颜色,我全都要!

<?xml version="1.0" encoding="utf-8"?>
<!--/res/animator/rotation_round.xml-->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:propertyName="rotation"
    android:valueFrom="0"
    android:valueTo="360" />
<?xml version="1.0" encoding="utf-8"?>
<!--/res/animator/alpha_animator.xml-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">
    <objectAnimator
        android:duration="500"
        android:propertyName="strokeAlpha"
        android:valueFrom="1f"
        android:valueTo="0f" />
    <objectAnimator
        android:duration="500"
        android:propertyName="strokeAlpha"
        android:valueFrom="0f"
        android:valueTo="1f" />
</set>
<?xml version="1.0" encoding="utf-8"?>
<!--res/animator/stroke_color_animator.xml-->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="strokeColor"
    android:valueFrom="@color/colorPrimary"
    android:valueTo="@color/colorAccent"
    android:duration="1000"/>

AnimatedVector华丽登场,把vector和动画文件黏合在一起。使用起来很简单,先通过drawable属性指定vector,然后通过target标签把动画和对象绑定在一起。

<?xml version="1.0" encoding="utf-8"?>
<!--avd_check.xml-->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vd__check">
    <target
        android:name="g_rotation"
        android:animation="@animator/rotation_around" />
    <target
        android:name="check"
        android:animation="@animator/stroke_color_animator" />
    <target
        android:name="check"
        android:animation="@animator/alpha_animator" />
</animated-vector>

最后需要在代码中触发。把avd_check.xml当做图片赋给ImageView,需要调用动画时,得到ImageView的drawable,强转为Animatable后,调用start()方法。

 <ImageView
   android:id="@+id/img_check"
   android:layout_width="48dp"
   android:layout_height="48dp"
   app:srcCompat="@drawable/avd_check" />

··· img_check.setOnClickListener { val drawable = img_check.drawable (drawable as Animatable).start() } ···

然后效果就出来了。

--

当然,如果你只是求方便的话,动画不需要单独写一个文件,直接写在target标签里面也是可以的。

trimPath 路径裁剪

trimPath其实和上面的动画一模一样,只是运用了几个矢量图标特有的属性而已。我们先来看看trimPath能做什么。

adp-delightful-details

trimPath一共有三个相关的属性:trimPathStart,trimPathEnd,trimPathOffset,都是float类型的数值,数值范围从0到1。分别表示path从哪里开始,到哪里结束,距离起点多远。至于怎么用,就看我们的想象力了。

接下来,用我们的小勾来做下实验吧。

照旧也是需要写一个动画文件

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

修改一下animatedVector文件

<?xml version="1.0" encoding="utf-8"?><!--avd_check.xml-->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vd__check">
    <target
        android:name="check"
        android:animation="@animator/trim_path_animator" />
</animated-vector>

bang!

2018.10.23_15.06.09.gif

ps:pathTrim只能对一条完整的path做动画,如果你的pathdata是有断开的,比如(省略坐标):“M,L,L M,L Z”,出现了两个m,那path就会分成了两段,这时候pathTrim只会作用于第一条线段了。

Morphing paths

重头戏来了,path变幻。我们想一想,既然strokeAplha,rotation这些属性都能做动画,那pathData这个属性,肯定也能做动画啦。于是有了下面这些效果。

adp-delightful-details(资源缺乏,重复利用)
*
adp-delightful-details

简单来说就是给属性动画里面的valueFrom和valueTo分别写两条不一样的path,那path就会自动变幻了。 需要注意的是,两条path的绘制指令需要在数量和结构上都相同。比如第一条path的指令(省略了坐标)是"M,L,L,C,Z",那第二条path的指令也应该是"M,L,L,C,Z"这种形式。

好,我们可以来试一试手。由于现在的勾的指令太少了,不好发挥我的小宇宙,所以我多加了几个指令。而目标,就是把小勾变成小圆圈吧。于是乎我就创造了以下两条path。他们都用了一个m指令和4个c指令(是的,c只能也能画直线的)。 为了方便管理,我把这两个path都放在一个xml里面了。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="check_path">M4,10 C10,16 10,16 10,16 C13,13 13,13 13,13 C16,10 16,10 16,10 C20,6 20,6 20,6</string>
    <string name="circle_path">M4,12 C4,7.6 7.6,4 12,4 C16.4,4 20,7.6 20,12 C20,16.4 16.4,20 12,20 C 7.6,20 4,16.4 4,12</string>
</resources>

然后也是动画和animatedVector:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:interpolator="@android:interpolator/linear"
    android:propertyName="pathData"
    android:valueFrom="@string/check_path"
    android:valueTo="@string/circle_path"
    android:valueType="pathType" />
<?xml version="1.0" encoding="utf-8"?><!--avd_check.xml-->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vd__check">
    <target
        android:name="check"
        android:animation="@animator/path_animator" />
</animated-vector>

接下来,噔噔噔噔噔。

2018.10.22_15.38.48.gif
咳咳。由于时间关系,我们就不在外观上深究了,大家意会意会。

但是你会发现,我的勾变成圆之后,再也变不回来了,动画不能倒过来做。于是乎我们需要引入最后一个概念,animatedSelecotr。

animated-selector

animated-selector允许定义有多个vector,根据不同状态使用不同的vector,并且通过animated-vector定义不同vector之前切换的动画。 所以我们接下来的步骤是:

  1. 定义两个vector:勾和圆
  2. 定义两个animated-vector:勾转化为圆,圆转化为勾
  3. 定义animated-selector把上述的文件组合起来。

动手动手: 圆的vector文件。和勾的大同小异。注意,我把name改成了circle。

<?xml version="1.0" encoding="utf-8"?><!--vd_circle.xml-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="100dp"
    android:height="100dp"
    android:viewportHeight="24"
    android:viewportWidth="24">
    <path
        android:name="circle"
        android:pathData="@string/circle_path"
        android:strokeAlpha="1.0"
        android:strokeColor="@color/colorPrimary"
        android:strokeLineCap="round"
        android:strokeWidth="1" />
</vector>

圆和勾的相互转化,需要两个文件。由于勾转化为圆已经在上面写过了(avd_check.xml,为了更名副其实,已经改名为avd_check2circl.xml)。这里是圆转化为勾。可以看到,动画是可以直接写在animated-vector里面的。

<?xml version="1.0" encoding="utf-8"?>
<!--huan -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:drawable="@drawable/vd_circle">
    <target android:name="circle">
        <aapt:attr name="android:animation">
            <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
                android:duration="500"
                android:interpolator="@android:interpolator/fast_out_slow_in"
                android:propertyName="pathData"
                android:valueFrom="@string/circle_path"
                android:valueTo="@string/check_path"
                android:valueType="pathType" />
        </aapt:attr>
    </target>
</animated-vector>

接下来就剩下animated-selector了。

  • 两个item分别指定两个vector,并且通过state_checked表示两种状态。实际上还有stated_checkable,state_selected等系统定义的状态,也可以执行定义新的状态变量。
  • transition则是表示不同vector之间转换的动画。属性很清晰明了,fromid和toId表示变换前后的两个item的id。drawable是antemator-vector。
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/check"
        android:drawable="@drawable/vd__check"
        android:state_checked="true" />
    <item
        android:id="@+id/circle"
        android:drawable="@drawable/vd_circle"
        android:state_checked="false" />

    <transition
        android:drawable="@drawable/avd_check2circle"
        android:fromId="@id/check"
        android:toId="@id/circle" />

    <transition
        android:drawable="@drawable/avd_circle2check"
        android:fromId="@id/circle"
        android:toId="@id/check" />
</animated-selector>

使用的时候需要放在app:srcCompat里面。

  <ImageView
    android:id="@+id/img_check_selector"
    android:layout_width="48dp"
    android:layout_height="48dp"
    app:srcCompat="@drawable/asl_check" />

然后再代码中通过setImageState方法设置不同的状态,图标就会自行变化了。

  img_check_selector.setOnClickListener {
            isCheckSelect = !isCheckSelect
            img_check_selector.setImageState(intArrayOf(if (isCheckSelect) android.R.attr.state_checked else -android.R.attr.state_checked), true)
        }

2018.10.22_17.17.21.gif

app:srcCompat

srcCompat是专门针对vector drawable的,所以最好还是使用srcCompat代替android:src。

后语

到这里,我们可以看到矢量图标动画的强大之处,无视马赛克,充满想象力,让我们的app更生动,更符合Material Design。但是也有vector Drawable的生成麻烦,编写各种animated-selector,animated-vector文件繁琐等缺点。只能说有得就有失了。

与其感慨路难行,不如马上出发。

*最后的最后,感谢大家的阅读,欢迎留言。 *

参考资料