前言
矢量圖也稱為面向?qū)ο蟮膱D像或繪圖圖像,是根據(jù)幾何特性來繪制的圖形,在安卓開發(fā)中可以使用失量圖代替原來的圖片資源,矢量圖具有占用空間小和可以隨意縮放但不失真的優(yōu)勢,在我的多個項目中都有運用。
通過學(xué)習(xí)和實踐,我總結(jié)了一些與矢量圖相關(guān)的知識,方便今后更好的使用矢量圖,同時也可以供大家查閱參考。
矢量圖的繪制
繪制矢量圖之前需要先定義畫布的寬高,后續(xù)的繪制效果都展示在這個畫布上。在繪制過程中需要輸入的坐標(biāo)就是這個畫布上的點。
安卓的矢量圖常見于drawable文件夾下,是一個xml文件,由vector標(biāo)簽包裹,在vector標(biāo)簽中可包含多個path標(biāo)簽,依次疊加顯示。
vector標(biāo)簽的常用屬性:
android:width="200dp"- 預(yù)覽寬度
android:height="200dp"- 預(yù)覽高度
android:viewportWidth="1024"- 畫布寬度(下面path路徑中的點位位于畫布中)
android:viewportHeight="1024"- 畫布高度
android:tint="#FFFFFF"- 矢量圖著色(會對矢量圖的全部內(nèi)容進(jìn)行統(tǒng)一著色,覆蓋原有顏色)
path標(biāo)簽的常用屬性
android:fillColor="#FFFFFF"- 填充顏色
android:pathData="M82,500H942V524H82V500"- 矢量圖路徑
android:strokeColor="#FFFFFF"- 邊框顏色,默認(rèn)的邊框是透明的
android:strokeWidth="20"- 邊框的寬度
在矢量圖中最重要的就是path屬性,圖像的樣式就是由path屬性中的數(shù)據(jù)繪制而成,這些數(shù)據(jù)由不同的命令組合而成,下面就介紹一些矢量圖的繪制命令。
- M 設(shè)置畫筆的位置,語法:M坐標(biāo)
M200,10- 將畫筆移動到200,10的位置。 - L 從當(dāng)前位置連接一條直線至指定位置,語法:L坐標(biāo)
L300,100- 從當(dāng)前位置連接一條直線到300,100位置。 - H 保持縱坐標(biāo)不變,畫一條橫線至目標(biāo)橫坐標(biāo)位置,語法:H橫坐標(biāo)
H200- 畫一條橫線至橫坐標(biāo)為200的點上。 - V 保持橫坐標(biāo)不變,畫一條豎線至目標(biāo)縱坐標(biāo)位置,語法:V縱坐標(biāo)
V200- 畫一條豎線至縱坐標(biāo)為200的點上。 - Q 連接目標(biāo)點位并對連線做二次貝塞爾曲線處理,語法:Q貝塞爾坐標(biāo),目標(biāo)點坐標(biāo)
Q300,100,400,200- 連接當(dāng)前位置和400,200做一條直線,在直線上做貝塞爾曲線變換,變換的錨點為300,100。 - C 連接目標(biāo)點位并對連線做三次貝塞爾曲線處理,語法:C貝塞爾坐標(biāo)1,貝塞爾坐標(biāo)2,目標(biāo)點坐標(biāo)
C300,250,500,350,400,400- 連接當(dāng)前位置和400,400做一條直線,先用貝塞爾坐標(biāo)1對直線做一次變換,再用貝塞爾坐標(biāo)2對之前的結(jié)果做一次變換,最終會得到一個S型的線段。 - A 連接起始點與目標(biāo)點,再根據(jù)指定的半徑和角度繪制一個圓,使線段的兩個端點都在圓的邊上,然后根據(jù)圓弧參數(shù)和繪制方向參數(shù)畫出所需的圓弧,語法:A橫向半徑,縱向半徑,旋轉(zhuǎn)角度,圓弧大小,繪制方向,目標(biāo)坐標(biāo)
A500,300,0,0,1,0,400- 繪制圓弧的過程比較復(fù)雜,下面分步模擬一下。- 根據(jù)所指定的橫向半徑和縱向半徑先繪制一個橢圓,如果兩個半徑一致就是一個正圓。
- 根據(jù)指定的旋轉(zhuǎn)角度對剛剛繪制的橢圓進(jìn)行旋轉(zhuǎn),所傳入的角度與0-360度一一對應(yīng),0=沒有旋轉(zhuǎn),90=把橢圓豎過來,360=轉(zhuǎn)了一整圈,720=轉(zhuǎn)了兩圈。
- 此時一個目標(biāo)橢圓已經(jīng)形成,然后需要用起點和終點連成線段去切割這個橢圓,使起點和終點恰好都在橢圓的邊上,如果橢圓很小,會將橢圓等比放大至兩點位置。
- 在上一步中有兩種情況,一個是橢圓比較小,放大后兩點連線為橢圓的直徑,這時圓弧大小的參數(shù)就沒用了,寫1或0都行。如果橢圓比較大,那么符合兩點都在橢圓邊上情況的位置就有兩個,此時如果從起點到終點順時針去畫圓弧就會有一個大圓弧一個小圓弧,如果我們需要大圓弧的情況就把圓弧大小的參數(shù)設(shè)置為1,反之則設(shè)置為0。
- 接下來的一個參數(shù)是繪制順序,繪制順序為1的時候是順時針繪制,繪制順序為0的時候是逆時針繪制,一般我們都會先根據(jù)圖像的需求指定繪制順序,再根據(jù)繪制順序指定圓弧大小參數(shù)。
- 最后的一個參數(shù)是目標(biāo)坐標(biāo),這個坐標(biāo)在第三步的時候已經(jīng)使用了,此時我們需要的圓弧就已經(jīng)畫完了。
- Z 閉合圖形,將當(dāng)前位置與路徑起始點連接起來
將前面的命令示例連接起來就可以生成一個完整的圖像,它大概長這個樣子:

畫布的尺寸為500x500,圖上的頂點是200,10的位置,也是我們開始作圖的起點。通過這個圖片可以更好的理解每一個繪圖命令。
以上的繪制命令均有其對應(yīng)的小寫版本,
M-m L-l H-h V-v Q-q C-c A-a,小寫版本的功能與大寫版本一致,但其中的坐標(biāo)都替換成了相對位置,使用相對位置的好處是起點發(fā)生變化后,后面的路徑自動跟隨起點移動,不需要所有的路徑都調(diào)整參數(shù)。安卓矢量圖的路徑會自動閉合,結(jié)尾沒有加Z命令也和添加了Z命令的效果一致。
矢量圖動畫
安卓中可以為矢量圖添加動畫效果,這樣用戶就可以看到一個動的圖片,可以一定程度的提高app的交互效果。矢量圖動畫是圖形內(nèi)部的變化,可以做到View動畫無法實現(xiàn)的效果。
pathData動畫
這種動畫針對的是矢量圖中path字段的值,通過連續(xù)改變path字段的值而達(dá)到產(chǎn)生動畫的效果。
注:pathData動畫所需的AnimatedVectorDrawable最低要求API等級為25
實現(xiàn)一個矢量圖動畫需要以下幾步:
1. 準(zhǔn)備起始狀態(tài)和結(jié)束狀態(tài)的矢量圖兩張。
2. 創(chuàng)建動畫配置文件。
3. 創(chuàng)建動畫矢量圖文件。
4. 啟動動畫。
- 首先,我們準(zhǔn)備用于動畫的兩張矢量圖,分別代表動畫的起始樣式和結(jié)束樣式,而且這兩張圖執(zhí)行動畫的path必須是同形path。
通過對矢量圖繪制的了解,我們知道矢量圖就是一個一個的點位進(jìn)行直線或曲線連接形成的一個圖形,所以在做矢量圖變化的時候,系統(tǒng)是把我們初始各個點位的坐標(biāo)通過系統(tǒng)計算后逐漸的改變成了結(jié)束時的坐標(biāo),這就要求我們做動畫的兩個矢量圖點位的個數(shù)是一致的,同時由于系統(tǒng)比較傻,只能是直線轉(zhuǎn)直線,曲線轉(zhuǎn)曲線,所以這兩張圖的路徑命令及順序也要一致。這就是傳說中的同形path。
基于這種要求,我準(zhǔn)備了兩個矢量圖:
// icon_filter_off.xml 未啟用篩選功能時的樣式
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:tint="#000000"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M102,112L282,382V912Q282,942,322,912L482,792V382L812,100Q832,82,812,82H122Q82,82,102,112" />
<path
android:name="status"
android:fillColor="#000000"
android:pathData="M582,490L582,530L882,530L882,490ZM582,640L582,680L882,680L882,640ZM582,790L582,830L882,830L882,790Z" />
</vector>

// icon_filter_on.xml 啟用了篩選功能時的樣式
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:tint="#000000"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#000000"
android:pathData="M102,112L282,382V912Q282,942,322,912L482,792V382L812,100Q832,82,812,82H122Q82,82,102,112" />
<path
android:name="status"
android:fillColor="#000000"
android:pathData="M592,670L562,700L662,800L692,770ZM662,800L692,830L792,730L762,700ZM762,700L792,730L892,630L862,600Z" />
</vector>

我的目標(biāo)是在狀態(tài)切換時把右下角的小圖標(biāo)做一個動畫轉(zhuǎn)換,所以右下角的路徑是單獨寫了一個path,同時把path命名為status。
這兩個矢量圖是我通過代碼敲出來的,也滿足了同形path的要求,為了這兩個矢量圖,我把小學(xué)和初中的數(shù)學(xué)知識又都搬了出來,一通計算,才算出了這些坐標(biāo),可惜還是沒能達(dá)到我的心理預(yù)期,只能先湊合用。
- 接下來開始創(chuàng)建動畫配置。
在res\animator文件夾下創(chuàng)建filter_turn_on.xml文件,代碼如下:
// filter_turn_on.xml 動畫的配置文件
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:propertyName="pathData"
android:valueFrom="M582,490L582,530L882,530L882,490ZM582,640L582,680L882,680L882,640ZM582,790L582,830L882,830L882,790Z"
android:valueTo="M592,670L562,700L662,800L692,770ZM662,800L692,830L792,730L762,700ZM762,700L792,730L892,630L862,600Z"
android:valueType="pathType" />
控制動畫運行的是一個objectAnimator,此處把objectAnimator包裹在一個set中也是可以的,說白了就是執(zhí)行這個動畫文件。
duration用來指定動畫的持續(xù)時間。
propertyName中的pathData指的就是矢量圖中的pathData。
valueFrom和valueTo一個是起始路徑,一個是結(jié)束路徑,可以想到,這個動畫就是在持續(xù)修改pathData,從而達(dá)到展示動畫的效果。而valueFrom和valueTo的值是直接從先前準(zhǔn)備的矢量圖中復(fù)制過來的,所以那個結(jié)束狀態(tài)的矢量圖中唯一有用的東西就是pathData屬性,沒有那個文件也無所謂。
valueType這里必須填寫pathType,這是專門用來計算path的類型。
- 動畫創(chuàng)建好了之后就開始創(chuàng)建動畫矢量圖文件。
之前我們通過@drawable/icon_filter_off的方式就可以引用矢量圖,此時的矢量圖是靜態(tài)的,因為這個矢量圖最外層由vector包裹,現(xiàn)在需要一個動態(tài)的矢量圖,所以我們在res\drawable文件夾下創(chuàng)建animated_filter_on.xml,代碼如下:
// animated_filter_on.xml 動畫矢量圖文件
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/icon_filter_off">
<target
android:name="status"
android:animation="@animator/filter_turn_on" />
</animated-vector>
此時,文件的最外層由animated-vector包裹,同時需要添加一個drawable參數(shù),這個drawable用于指定動畫應(yīng)用于那個矢量圖上,我們是要從未啟用狀態(tài)變成啟用狀態(tài),所以是在未啟用狀態(tài)開始執(zhí)行動畫,在動畫未開始的時候展示的也是未啟用狀態(tài)。此處我們指定為@drawable/icon_filter_off。
內(nèi)部有一個target標(biāo)簽,這個標(biāo)簽可以有多個,分別對應(yīng)不同的動畫,但同一個path只能應(yīng)用一個動畫。
name用于指定要執(zhí)行動畫的path。status正是我們?yōu)橛蚁陆切D標(biāo)path設(shè)置的名稱。
animation用于指定需要執(zhí)行的動畫。此處引用我們剛剛創(chuàng)建的動畫資源@animator/filter_turn_on。
當(dāng)我們創(chuàng)建好動畫矢量圖之后,頁面中引用的資源就不再是之前的靜態(tài)矢量圖了,需要把ImageView的圖片替換成@drawable/animated_filter_on
- 準(zhǔn)備工作已經(jīng)就緒,接下來就讓我們的矢量圖動起來。
在點擊事件中將ImageView的drawable提取出來,將其轉(zhuǎn)換成android.graphics.drawable.AnimatedVectorDrawable類型,然后調(diào)用start()方法即可。
// java
AnimatedVectorDrawable drawable= (AnimatedVectorDrawable) imageViewFilter.getDrawable();
drawable.start();
// kotlin
(imageViewFilter.drawable as AnimatedVectorDrawable).start()
經(jīng)過這么多的步驟,我們終于做出了一個矢量圖動畫,而且是一個。說實話,有點累,然而我這個狀態(tài)切換的動畫一套就要兩個,所以我又加了一個回來的動畫和對應(yīng)的動畫矢量圖,一共六個文件,完成了篩選狀態(tài)的兩個切換動畫。這還是比較簡單的實現(xiàn)方式,對于兩種狀態(tài)切換的動畫,網(wǎng)上還有一種使用selector的方式,這種方式更麻煩,而且使用方法并沒有簡單一些,所以我的選擇是在需要切換狀態(tài)的時候更改ImageView的圖片資源,然后再執(zhí)行動畫。
// kotlin
// 這是點擊事件中切換狀態(tài)樣式的代碼,實際業(yè)務(wù)中可能會根據(jù)業(yè)務(wù)數(shù)據(jù)判斷是否需要切換。
imageViewFilter.run {
if (isSelected) {
setImageResource(R.drawable.animated_filter_off)
(drawable as AnimatedVectorDrawable).start()
}else{
setImageResource(R.drawable.animated_filter_on)
(drawable as AnimatedVectorDrawable).start()
}
isSelected = !isSelected
}
trimPath動畫
trimPath動畫相當(dāng)于是改變了矢量圖繪制的位置,是從頭開始畫還是從80%的位置開始畫,然后再動態(tài)的修改這個百分比,從而達(dá)到動畫的效果。理解起來倒不是很難。
先放一個我使用trimPath動畫做的loading效果,這個動畫效果被我用在LoadingDialog中,在界面加載的時候會重復(fù)播放這個動畫。

為了做到這種效果,我們一共需要以下幾個步驟:
1. 設(shè)計矢量圖,并將其添加到安卓項目中。
2. 根據(jù)動畫需求,添加動畫配置文件。
3. 配置動畫矢量圖文件,在使用的地方進(jìn)行引用。
4. 開始執(zhí)行動畫。
- 首先是設(shè)計一個自己的線狀動畫,我的靈感來自于心電圖,一個人活著的話會有一個周期性的心電圖波動,而我用這個形狀來代表APP正在干活。在學(xué)習(xí)了矢量圖的繪制之后,一個心電圖的形狀并不難,只是計算坐標(biāo)比較累。
// icno_loading.xml loading的完全體樣式
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="200dp"
android:height="200dp"
android:tint="#000000"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:name="load"
android:pathData="M82,512h280l60,-172l60,430l80,-602l70,430l30,-86h280"
android:strokeWidth="20"
android:strokeColor="#000000"
android:trimPathStart="0"
android:trimPathEnd="0"
tools:trimPathEnd="1" />
</vector>
android:name="load"不用多說,這個是我們做動畫時路徑名稱。這里為了讓心電圖路徑更清晰,我設(shè)置了描邊寬度為20(android:strokeWidth="20"),同時還要設(shè)置描邊的顏色才能展示出來。后面的android:trimPathStart="0"和android:trimPathEnd="0"是本次trimPath動畫的重點。
android:trimPathStart="0"- 路徑繪制的起始點,范圍[0,1]。
android:trimPathEnd="0"- 路徑繪制的終點,范圍[0,1]。實際展示的效果就是從起點畫到終點的路徑。當(dāng)繪制范圍超過[0,1]時也可以畫出來,類似于循環(huán)繪制。具體效果就自行體會吧。
這兩個屬性都設(shè)置為0是因為動畫的起始幀都為0,然后通過objectAnimator慢慢把這兩個屬性變?yōu)?,這樣一個慢慢增長的動畫就形成了。
網(wǎng)絡(luò)上一個橫線變成搜索按鈕的示例是將這兩個屬性分別應(yīng)用到了兩個path上,而我是將兩個屬性同時應(yīng)用到一個path上,原理都是一樣的。
- 接下來為剛剛預(yù)留的屬性配置動畫效果,要做到和心電圖差不多的掃描效果,要同時修改路徑的起始點和終點,這樣可以只展示整個路徑中的某一段。而且兩個動畫要有一定間隔,如果同步執(zhí)行,起點和終點肯定是一致的,這樣什么都畫不出來。
// loading.xml loading動畫配置
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="3000"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="trimPathStart"
android:repeatCount="infinite"
android:startOffset="1000"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="3000"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="trimPathEnd"
android:repeatCount="infinite"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
在配置文件中,我將兩個動畫都設(shè)置為3秒且循環(huán)播放,起始點的動畫慢于終點的動畫1秒,達(dá)到只畫中間1秒間隔線段的效果。和路徑變形動畫的區(qū)別是android:valueType="floatType",我們只需要計算從0到1的數(shù)字,然后應(yīng)用到trimPathStart和trimPathEnd字段上。至此,loading的動畫就配置完了。
- 所需的資源都準(zhǔn)備好了之后就創(chuàng)建一個動畫矢量圖文件,在這個文件中將靜態(tài)的矢量圖和動畫配置綁定到一起。
// animated_loading.xml 用于界面的動畫矢量圖文件
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/icon_loading">
<target
android:name="load"
android:animation="@animator/loading" />
</animated-vector>
這一步已經(jīng)沒什么可說的了,就是將指定的矢量圖中指定的路徑設(shè)置一個指定的動畫。
- 最后,將animated_loading設(shè)置到目標(biāo)
ImageView中,在合適的時機(jī)開始動畫。啟動動畫的方式和路徑變換一致,然后就可以欣賞自己制作的動畫效果了。
結(jié)語
通過幾天的學(xué)習(xí),已經(jīng)大致掌握了矢量圖的展示及動畫的制作,但這一套流程下來成本比較高,是程序員方式的動畫制作流程。除了制作成本,創(chuàng)意成本也是相當(dāng)高的,一個好的創(chuàng)意能極大的提升用戶體驗,而好多時候我們的創(chuàng)意能夠被實現(xiàn)也是很困難的。希望以后能實現(xiàn)一些更好的效果,讓用戶使用起來更舒服。
參考文章
SVG—最簡單的SVG動畫
SVG路徑(path)中的圓弧(A)指令的語法說明及計算邏輯
Android中的矢量圖
Android高級動畫(2)