Android動畫:行雲流水的向量動畫
前言
我們在日常使用各種app的時候,會發現原來越多下面這型別的向量圖示動畫。圖示動畫是material design所推薦的圖示效果。當然對我來說,炫酷的效果就是我學習向量圖示動畫的一個很充分理由。

adp-delightful-details

adp-delightful-details
VectorDrawable
SVG和VectorDrawable
- 向量圖:向量圖和傳統的png、jpg等圖片格式,是典型的漁和魚的區別。向量圖儲存的是圖片畫出來的方法,而不是畫素點的排列,所以無論向量圖放大多少倍,只要按照等比例縮放後的路徑把圖示畫出來即可,不存在馬賽克的問題。我們電腦中顯示的文字就是這麼一個原理。
- svg是最常見的向量圖格式,而在Android裡面,我們使用的是VectorDrawable。
- 一般來說,向量圖的生成是不需要我們去關心的,如果需要自己去找向量圖的話,可以去 ofollow,noindex">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檔案畫出來的,我們來解讀下這個檔案:
- vector標籤:表示這是一個向量圖。
- viewportHeight/viewWidth:向量圖的長寬,之後畫圖也是按此長寬來畫。圖示的左上角是(0,0),右下角是(viewWidth,viewHeight)。
- group標籤:group有一些path沒有的屬性,如果要用這些屬性做動畫,那就只能path外巢狀多一層group標籤了。
- name:動畫會通過name尋找到此物件。
- rotation|scaleX|pivotX..:這些屬性都很熟悉了吧
- path標籤:連續的線或面,向量圖就是有一個或多個path組成的。
- name:動畫會通過name尋找到此物件。
- storkeColor: 線段的顏色。
- strokeWidth: 線段的寬度。
- strokeAlpha: 線段的透明度。
- strokeLineCap: 線段末端的樣式 butt(斷開)|round(圓角)|square(直角)
- fillColor: 填充的顏色。
- fillAlpha:填充透明度。
- 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()
}
···
然後效果就出來了。

--
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>
接下來,噔噔噔噔噔。

圈圈勾勾圈圈
咳咳。由於時間關係,我們就不在外觀上深究了,大家意會意會。
但是你會發現,我的勾變成圓之後,再也變不回來了,動畫不能倒過來做。於是乎我們需要引入最後一個概念,animatedSelecotr。
animated-selector
animated-selector允許定義有多個vector,根據不同狀態使用不同的vector,並且通過animated-vector定義不同vector之前切換的動畫。
所以我們接下來的步驟是:
- 定義兩個vector:勾和圓
- 定義兩個animated-vector:勾轉化為圓,圓轉化為勾
- 定義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檔案繁瑣等缺點。只能說有得就有失了。
與其感慨路難行,不如馬上出發。
最後的最後,感謝大家的閱讀,歡迎留言。
參考資料
- adp-delightful-details :牛逼的圖示動畫庫,本文中使用了他的一些圖示效果。
- An Introduction to Icon Animation Techniques
- Android高階動畫(2)