背景
維基百科中的定義:
可縮放向量圖形(Scalable Vector Graphics,SVG)是一種基于可擴(kuò)展標(biāo)記語(yǔ)言(XML),用于描述二維向量圖形的圖形格式。SVG由W3C制定,是一個(gè)開放標(biāo)準(zhǔn)。
1,SVG何以可以任意縮放而不會(huì)失真,drawable-(m|h|xh|xxh|xxxh)dpi和mipmap-(m|h|xh|xxh|xxxh)dpi這倆貨就可以省省了;2,SVG文件一般都比較小,省去很去資源達(dá)到apk縮包的目的;3,SVG占用內(nèi)存非常小,性能高。但是SVG明顯的缺點(diǎn)是沒有位圖表達(dá)的色彩豐富。
Android API 21(5.0)引入了一個(gè)Drawable的子類VectorDrawable目的就是用來(lái)渲染矢量圖,AnimatedVectorDrawable用來(lái)播放矢量動(dòng)畫。之前老的小于21的API設(shè)備可以分別使用VectorDrawableCompat和AnimatedVectorDrawableCompat這兩個(gè)兼容包來(lái)同樣達(dá)到渲染矢量圖的目的。本文只討論矢量圖,不討論矢量動(dòng)畫。
準(zhǔn)備
使用矢量圖要根據(jù)minSdkVersion來(lái)分3中不同的情況:
-
minSdkVersion>=21:用xml文件或者代碼定義VectorDrawable,和普通的Drawable用法一樣,不再需要額外任何東西;如何編寫矢量圖,下文有介紹; -
minSdkVersion<21:如果想要渲染矢量圖的話必須在app模塊的build.gralde文件里添加一行代碼:
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
-
minSdkVersion<21以及更多:上面的第二種情況是使用兼容包,但是兼容包僅支持AppCompatImageView和AppCompatImageButton及其子類矢量圖,而且矢量圖的引用必須放在app:srcCompat屬性中才會(huì)被識(shí)別并生效,代碼必須這樣寫才行:
<android.support.v7.widget.AppCompatImageView
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_oval"/>
ic_oval.xml是我們使用xml編寫的矢量圖,如果想要TextView的drawableTop或者其他額外方式使用矢量圖渲染,那么必須在Activity中加入代碼:
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
同時(shí)這個(gè)Activity必須繼承AppCompatActivity這個(gè)compat兼容包屬性才會(huì)生效。
minSdkVersion<21情況下在非app:srcCompat屬性的地方使用矢量圖時(shí),需要將矢量圖用drawable容器(如StateListDrawable, InsetDrawable, LayerDrawable, LevelListDrawable, 和RotateDrawable)包裹起來(lái)使用。否則會(huì)在低版本的情況下報(bào)錯(cuò)org.xmlpull.v1.XmlPullParserException: Binary XML file line #0: invalid drawable tag vector。minSdkVersion>=21則沒有任何限制。
矢量圖使用
準(zhǔn)備工作做好之后,我們就需要自己動(dòng)手編輯矢量圖了。VectorDrawable類在xml中對(duì)應(yīng)的是標(biāo)簽是vector。我目前所知道的是只有xml文件才能決定矢量圖的樣子(也就是編輯pathData、fillColor等屬性),貌似無(wú)法使用代碼來(lái)決定矢量圖的繪制邏輯,而只能使用代碼加載編輯好的xml文件,這個(gè)xml文件有兩種方法來(lái)創(chuàng)建:
- 右擊drawable-->Drawable resource file-->設(shè)置root element為vector,這樣的矢量圖繪制邏輯完全掌握在開發(fā)者手里;
- 右擊drawable-->Vector Asset,選擇SVG或者PSD文件直接生成根標(biāo)簽為vector的xml文件,可以百度或者Google怎樣把png轉(zhuǎn)換成SVG。
寫了這么多字,一直在瞎扯淡而沒談重點(diǎn),下面我們看下根標(biāo)簽為vector的xml文件的真面目,代碼:

上圖中標(biāo)簽vector使用了四個(gè)屬性:android:width="24dp"、android:height="24dp"、android:viewportHeight="300.0"、android:viewportWidth="300.0"。
- width和height:當(dāng)使用這個(gè)矢量圖的View的寬高是wrap_content 的時(shí)候這兩個(gè)屬性才生效;
- viewportWidth和viewportHeight:決定畫布的寬高,是定義的一個(gè)虛擬空間,方便編輯pathData屬性,如果pathData中的點(diǎn)超出了這個(gè)虛擬空間,超出的部分將不會(huì)展現(xiàn)給用戶;虛擬空間的原點(diǎn)仍然還是在左上角(R點(diǎn)就是原點(diǎn))。
path標(biāo)簽是vector標(biāo)簽的子標(biāo)簽,它使用了以下屬性:
-
android:name:類似View的id屬性,方便path被引用,如上圖的edge是虛擬空間四個(gè)邊界的path,oval是一個(gè)橢圓的path; -
android:fillColor:填充path的顏色,如果沒有定義則不填充path -
android:strokeColor:path邊框顏色,如果沒有定義則不顯示邊框 -
android:strokeWidth:path邊框的粗細(xì)尺寸 -
android:pathData:path指令,決定path的移動(dòng)和繪制邏輯,這個(gè)是最主要的屬性,下面詳細(xì)討論。
更多path屬性請(qǐng)參考鏈接。
pathData的指令和Path類的API方法基本差不多,比如M指令對(duì)應(yīng)moveTo方法,m指令對(duì)應(yīng)rMoveTo方法,下面是一些基本的指令:
- Mx,y:移動(dòng)到點(diǎn)(x,y)
- Lx,y:直線連到點(diǎn)x,y,簡(jiǎn)化命令H(x)水平連接和V(y)垂直連接;
- Qx1,y1 x2,y2:二階貝塞爾曲線,控制點(diǎn)(x1,y1),終點(diǎn)x2,y2;
- Cx1,y1 x2,y2 x3,y3:三階貝塞爾曲線,控制點(diǎn)(x1,y1)( x2,y2),終點(diǎn)x3,y3;
- Tx y:平滑的二階貝塞爾曲線,參數(shù)只有一個(gè)點(diǎn)(x,y),這個(gè)點(diǎn)是結(jié)束點(diǎn),控制點(diǎn)是前一個(gè)二階貝塞爾曲線的控制點(diǎn)相對(duì)于前一個(gè)貝塞爾曲線的結(jié)束點(diǎn)的鏡像點(diǎn)。
- Sx2,y2 x,y:平滑的三階貝塞爾曲線,參數(shù)為(x2,y2 x,y) ,x2,y2 為第二個(gè)控制點(diǎn),x,y為繪制終點(diǎn),那么第一個(gè)控制點(diǎn)則是前一個(gè)三階曲線的第二個(gè)控制點(diǎn)相對(duì)于前一個(gè)三階曲線終點(diǎn)的鏡像點(diǎn)。
- Arx,ry x-axis-rotation large-arc-flag,sweep-flag x,y:ellipse arc圓弧曲線
- z:close閉合
......
- z:close閉合
每個(gè)指令都有大小寫形式,大寫表示后面的參數(shù)是絕對(duì)坐標(biāo),小寫表示相對(duì)于上一個(gè)點(diǎn)的相對(duì)坐標(biāo)位置,參數(shù)可以用逗號(hào)或者空格分離。
只要掌握上面5個(gè)基本指令就能編輯pathData并且繪制一些酷炫的SVG。更詳細(xì)全面的path指令請(qǐng)參閱鏈接。
估計(jì)你已經(jīng)發(fā)現(xiàn)了,圓弧曲線指令A(yù)竟然那么多參數(shù),這直接嚇跑了很多的程序員,其實(shí)也并不難,且慢慢道來(lái)。
先根據(jù)圖1里的代碼來(lái)分析pathData指令。如圖一所示,edge這個(gè)path使用了四個(gè)相對(duì)指令,首先指令h300 0相對(duì)向右水平移動(dòng)300到點(diǎn)S,然后指令v0 300相對(duì)向下垂直移動(dòng)300到T,再次指令h-300 0相對(duì)向左水平移動(dòng)300到U,最后指令v0 -300相對(duì)向上垂直移動(dòng)300到起點(diǎn)R,這樣就根據(jù)屬性strokeColor和strokeWidth繪制了四條直線,最后一個(gè)指令可以使用z代替。這很簡(jiǎn)單吧?!
再來(lái)看oval這個(gè)path。它使用了三條指令。第一條指令移動(dòng)到點(diǎn)M處,第二條指令a75,75 0 1,1 150,0繪制M-N-O的弧線,第三條指令a75,75 0 1,1 -150,0繪制O-P-M的弧線。a指令共有7個(gè)參數(shù):rx和ry表示橢圓的兩個(gè)半徑,x-axis-rotation表示x軸的旋轉(zhuǎn)角度,x和y表示繪制橢圓弧線的終點(diǎn),這5個(gè)參數(shù)很簡(jiǎn)單很好理解,large-arc-flag和sweep-flag這兩個(gè)參數(shù)有點(diǎn)唬人。
解釋large-arc-flag和sweep-flag這兩個(gè)參數(shù)之前先考慮下這個(gè)題目:已知橢圓的半徑rx和ry,請(qǐng)繪制若干條從起始點(diǎn)A到終點(diǎn)B的橢圓弧線。題目中是若干條,那到底幾條?。恳话闱闆r下會(huì)有四條橢圓弧線(特殊情況是rx=線段AB的一半或者ry=線段AB的一半,這時(shí)候的橢圓弧線只有兩條),而large-arc-flag和sweep-flag這兩個(gè)參數(shù)就從這四個(gè)橢圓弧線中選取了最終的一條進(jìn)行繪制。large-arc-flag決定是大弧線還是小弧線,1大0小,sweep-flag決定是順時(shí)針弧線還是逆時(shí)針弧線,1順0逆。

有人可能會(huì)問(wèn),圖1的oval path是個(gè)圓,竟然使用了兩個(gè)a指令,使用一個(gè)a指令就能繪制圓的,只要終點(diǎn)回到起始點(diǎn)就能繪制圓的path了,剛開始我也是這樣認(rèn)為的,比如下面的代碼:
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="300.0"
android:viewportWidth="300.0">
<path
android:name="circle"
android:fillColor="@android:color/holo_green_light"
android:pathData="
M150,150
a75,75 0 1,1 0,0"
android:strokeColor="#00000000" />
</vector>
上面的代碼的path從起始點(diǎn)又回到了起始點(diǎn),不會(huì)繪制任何東西,終點(diǎn)x y需要和起始點(diǎn)錯(cuò)開幾個(gè)像素比如android:pathData="M150,150 a75,75 0 1,1 0,1"就大約是一個(gè)圓path,為什么說(shuō)是大約一個(gè)圓?因?yàn)槠鹗键c(diǎn)和終點(diǎn)不在一起,這只是一個(gè)圓的大弧線部分。推薦使用兩條a指令繪制圓path,因?yàn)橐粭la指令繪制的不是真正的圓path。
group標(biāo)簽
path沒有scale、rotate和translate這三種屬性,因此也不能執(zhí)行這三種屬性動(dòng)畫,要達(dá)到這樣的目的需要借助group這個(gè)標(biāo)簽。group標(biāo)簽也是vector的一個(gè)子標(biāo)簽,它可以作為path或者其他group的父標(biāo)簽使用,將path和group組合成一個(gè)組來(lái)附加一些變換操作,這些變換操作包括scale、rotate和translate共三種。這張圖3是來(lái)自android官網(wǎng)的vector標(biāo)簽樹型圖:

- android:name:group的名字;
- android:rotation:group的旋轉(zhuǎn)角度,默認(rèn)0。
- android:pivotX:scale和rotation變換中心點(diǎn)的X坐標(biāo),默認(rèn)0;
- android:pivotY:scale和rotation變換中心點(diǎn)的Y坐標(biāo),默認(rèn)0;
- android:scaleX:X軸方向的縮放,默認(rèn)1;
- android:scaleY:Y軸方向的縮放,默認(rèn)1;
- android:translateX:X軸方向的移動(dòng)距離,默認(rèn)0;
- android:translateY:Y軸方向的移動(dòng)距離,默認(rèn)0。
這是group的全部屬性了,屬性都很簡(jiǎn)單,不需要解釋。
clip-path標(biāo)簽
<clip-path>定義當(dāng)前繪制的剪切路徑,就是圖像的一部分剪切下來(lái)。注意,clip-path只對(duì)當(dāng)前的vector和group以及當(dāng)前vector和group的孩子有效。這個(gè)標(biāo)簽僅有兩個(gè)屬性:
- android:name:clip-path的名字;
- android:pathData:clip-path的路徑。
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="300.0"
android:viewportWidth="300.0">
<clip-path android:name="clip_one" android:pathData="
M0 20a20 20 0 0 1 20 -20
l260 0a20 20 0 0 1 20 20
l0 260a20 20 0 0 1 -20 20
l-260 0a20 20 0 0 1 -20 -20
l0 -260"/>
<path
android:name="edge"
android:pathData="h300v300h-300v-300
M150 0 v300
M0 150 h300"
android:fillColor="@android:color/holo_green_light"
android:strokeColor="@android:color/holo_red_dark"
android:strokeWidth="1" />
<group>
<clip-path android:name="clip_two" android:pathData="M0 150h300v150h-300v-150"/>
<path
android:name="oval"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M20 20 l260,260M280 20 l-260,260h100"
android:strokeColor="#000000"
android:strokeWidth="15"/>
</group>
</vector>
上面代碼定義了兩個(gè)clip-path,其效果如圖4所示。
build.gradle中vectorDrawables.useSupportLibrary屬性
build.gradle中的vectorDrawables.useSupportLibrary默認(rèn)是false,不設(shè)置為true的話會(huì)有什么問(wèn)題嗎?討論這個(gè)問(wèn)題也需要根據(jù)minSdkVersion具體分析:
-
minSdkVersion>=21:這么高的API根本就不需要兼容包,仍然可以渲染矢量圖; -
minSdkVersion<21:不再使用矢量圖兼容包,不能渲染矢量圖,但是有趣的是vector標(biāo)簽仍然可以使用,低版本的API完全把VectorDrawable當(dāng)作Drawable使用了,VectorDrawable的特性完全失效。原理是vector xml文件會(huì)生成對(duì)應(yīng)的png文件,使用png方式渲染圖片,和矢量圖沒有任何關(guān)系。值得注意的是生成的png圖片size很小而且會(huì)忽略vector標(biāo)簽的android:tint屬性(貌似只忽略這個(gè)屬性,我試過(guò)vector標(biāo)簽的android:alpha屬性在生成的png圖片中仍然有效,生成的png文件目錄是app/build/generated/res/pngs/debug,minSdkVersion>=21或者vectorDrawables.useSupportLibrary=true的話不會(huì)生成這些png圖片)。而且path標(biāo)簽的color相關(guān)的屬性不能引用colors.xml的值,android:strokeColor="@android:color/holo_red_dark"這樣寫的話會(huì)編譯失敗,提示錯(cuò)誤:Can't process attribute android:strokeColor="@android:color/holo_red_dark": references to other resources are not supported by build-time PNG generation,而只能寫原生的16進(jìn)制color值比如android:strokeColor="#234aac"。
想看具體信息請(qǐng)查看這篇文章。
SVG實(shí)戰(zhàn)
我做的項(xiàng)目中一張撲克png資源大小2k左右,我試著用矢量圖畫這些撲克牌。代碼如下:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="400dp"
android:height="550dp"
android:viewportHeight="550"
android:viewportWidth="400.0">
<group android:name="poker_diamond_a">
<path
android:name="border"
android:strokeWidth="7"
android:strokeColor="#96999c"
android:fillColor="@android:color/white"
android:pathData="M5 25a20 20 0 0 1 20 -20
h350a20 20 0 0 1 20 20v500a20 20 0 0 1 -20 20h-350a20 20 0 0 1 -20 -20v-500"/>
<path android:name="a"
android:strokeWidth="8"
android:strokeColor="#cc0000"
android:strokeLineJoin="bevel"
android:pathData="M40 120
l40 -90
l40 90
l-16-35
h-48"/>
<path android:name="small_diamond" android:fillColor="#cc0000" android:pathData="M80 130l41 41l-41 41l-41 -41z"/>
<path android:name="big_diamond" android:fillColor="#cc0000" android:pathData="M260 310l100 100l-100 100l-100 -100z"/>
</group>
</vector>

代碼很簡(jiǎn)單,只有4條path。border路徑順序是1-2-3-4-5-6-7-8-1, a的路徑是a-b-c-d-e,small_diamond的路徑是e-f-g-h,big_diamond的路徑是i-j-k-l。這個(gè)xml文件只有1k。
文章有錯(cuò)誤的地方希望指正。
本文內(nèi)容都是一些基礎(chǔ)的東西,應(yīng)該都能掌握,主要介紹了vector、group、path、clip-path這些標(biāo)簽常用的屬性以及pathData屬性對(duì)應(yīng)的常用的指令,工作中掌握這些常用的知識(shí)就能比較熟練使用矢量圖了。后續(xù)文章會(huì)剖析一些不常用的屬性。