1.前言
SVG,即Scalable Vector Graphics 可伸縮矢量圖形。這種圖像格式在前端中已經(jīng)使用的非常廣泛了,而在移動端的開發(fā)中,遇到一些復(fù)雜的自定義控件或者動畫效果,我們就可以考慮讓美工出套SVG圖,再按照固定的套路去解析即可。
2.Vector Drawable
2.1 矢量圖與位圖
先介紹下矢量圖像和位圖圖像的區(qū)別
1.矢量圖像:SVG是W3C 推出的一種開放標準的文本式矢量圖形描述語言,他是基于XML的專門為網(wǎng)絡(luò)而設(shè)計的圖像格式
SVG是一種采用XML來描述二維圖形的語言,所以它可以直接打開xml文件來修改和編輯。
2.位圖圖像:位圖圖像的存儲單位是圖像上每一點的像素值,因而文件會比較大,像GIF、JPEG、PNG等都是位圖圖像格式。
也就是說,如果使用矢量圖,就不需要針對不同dpi的設(shè)備展示不同精度的圖片了,是不是很方便???
2.2 Vector Drawable簡介
在Andoird中,SVG的實現(xiàn)方式就是Vector Drawable。這是個在5.0時增加的新類,所以對之前版本的兼容會有些問題,之后會單獨拎出來講。
相對于普通的Drawable來說,Vector Drawable有以下幾個好處:
(1)Vector圖像可以自動進行適配,不需要通過分辨率來設(shè)置不同的圖片。
(2)Vector圖像可以大幅減少圖像的體積,同樣一張圖,用Vector來實現(xiàn),可能只有PNG的幾十分之一。
(3)使用簡單,很多設(shè)計工具,都可以直接導出SVG圖像,從而轉(zhuǎn)換成Vector圖像 功能強大。
(4)不用寫很多代碼就可以實現(xiàn)非常復(fù)雜的動畫 成熟、穩(wěn)定,前端已經(jīng)非常廣泛的進行使用了。
2.3 Vector Drawable基本語法
Vector Drawable實際上是一個XML文件,咱們先來看一個vector的例子
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="400dp"
android:height="400dp"
android:viewportHeight="400"
android:viewportWidth="400">
<path
android:pathData="M 100 100 L 300 100 L 200 300 z"
android:strokeColor="#000000"
android:strokeWidth="5"
android:fillColor="#FF0000"
/>
</vector>
這個vector畫了一個三角形,對照著上面的代碼,咱們來學習Vector Drawable的基本語法。首先說明一下,這些語法開發(fā)者不需要全部精通,只要能夠看懂即可,這些path標簽及數(shù)據(jù)生成都可以交給工具來實現(xiàn)。
2.3.1 pathData標簽
先看pathData標簽,這里定義了vector中path的繪制,也是最重要的一部分。語法如下,注意,’M’處理時,只是移動了畫筆, 沒有畫任何東西。
M = moveto(M X,Y) :將畫筆移動到指定的坐標位置,相當于 android Path 里的moveTo()
L = lineto(L X,Y) :畫直線到指定的坐標位置,相當于 android Path 里的lineTo()
H = horizontal lineto(H X):畫水平線到指定的X坐標位置
V = vertical lineto(V Y):畫垂直線到指定的Y坐標位置
C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三次貝賽曲線
S = smooth curveto(S X2,Y2,ENDX,ENDY) 同樣三次貝塞爾曲線,更平滑
Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二次貝賽曲線
T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射 同樣二次貝塞爾曲線,更平滑
A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧線 ,相當于arcTo()
Z = closepath():關(guān)閉路徑(會自動繪制鏈接起點和終點)
2.3.2 path標簽
接著看下path標簽的內(nèi)容。稍微有個印象即可,需要時再對照著去理解。
android:name 定義該 path 的名字,這樣在其他地方可以通過名字來引用這個路徑
android:pathData 和 SVG 中 d 元素一樣的路徑信息。
android:fillColor 定義填充路徑的顏色,如果沒有定義則不填充路徑
android:strokeColor 定義如何繪制路徑邊框,如果沒有定義則不顯示邊框
android:strokeWidth 定義路徑邊框的粗細尺寸
android:strokeAlpha 定義路徑邊框的透明度
android:fillAlpha 定義填充路徑顏色的透明度
android:trimPathStart 從路徑起始位置截斷路徑的比率,取值范圍從 0 到1
android:trimPathEnd 從路徑結(jié)束位置截斷路徑的比率,取值范圍從 0 到1
android:trimPathOffset 設(shè)置路徑截取的范圍
android:strokeLineCap 設(shè)置路徑線帽的形狀,取值為 butt, round, square.
android:strokeLineJoin 設(shè)置路徑交界處的連接方式,取值為 miter,round,bevel.
android:strokeMiterLimit 設(shè)置斜角的上限
2.3.4 vector標簽
根元素 vector標簽是用來定義這個矢量圖的,該元素包含如下屬性:
android:name 定義該drawable的名字
android:width 定義該 drawable 的內(nèi)部(intrinsic)寬度,支持所有 Android 系統(tǒng)支持的尺寸,通常使用 dp
android:height 定義該 drawable 的內(nèi)部(intrinsic)高度,支持所有 Android 系統(tǒng)支持的尺寸,通常使用 dp
android:viewportWidth 定義矢量圖視圖的寬度,視圖就是矢量圖 path 路徑數(shù)據(jù)所繪制的虛擬畫布
android:viewportHeight 定義矢量圖視圖的高度,視圖就是矢量圖 path 路徑數(shù)據(jù)所繪制的虛擬畫布
android:tint 定義該 drawable 的 tint 顏色。默認是沒有 tint 顏色的
android:tintMode 定義 tint 顏色的 Porter-Duff blending 模式,默認值為 src_in
android:autoMirrored 設(shè)置當系統(tǒng)為 RTL (right-to-left) 布局的時候,是否自動鏡像該圖片。比如 阿拉伯語。
android:alpha 該圖片的透明度屬性
2.3.5 group標簽
有時候我們需要對幾個路徑一起處理,這樣就可以使用 group 元素來把多個 path 放到一起。 group 支持的屬性如下:
android:name 定義 group 的名字
android:rotation 定義該 group 的路徑旋轉(zhuǎn)多少度
android:pivotX 定義縮放和旋轉(zhuǎn)該 group 時候的 X 參考點。該值相對于 vector 的 viewport 值來指定的。
android:pivotY 定義縮放和旋轉(zhuǎn)該 group 時候的 Y 參考點。該值相對于 vector 的 viewport 值來指定的。
android:scaleX 定義 X 軸的縮放倍數(shù)
android:scaleY 定義 Y 軸的縮放倍數(shù)
android:translateX 定義移動 X 軸的位移。相對于 vector 的 viewport 值來指定的。
android:translateY 定義移動 Y 軸的位移。相對于 vector 的 viewport 值來指定的。
通過上面的屬性可以看出, group 主要是用來設(shè)置路徑做動畫的關(guān)鍵屬性的。
2.4 一些常用的工具
上面的這些語法只要能看懂就可以了。我們會用一些成熟的工具來輔助SVG在移動端的開發(fā)。
1.先說美工這個最好的工具,SVG圖一般直接讓美工來幫你搞定就行了!像PS、Illustrator等等都支持導出SVG圖片
2.獲取到SVG后,我們要將其轉(zhuǎn)換為vector drawable對象,svg2android這個網(wǎng)站可以幫你輕松完成。
3.如果沒有SVG圖片怎么辦?可以使用SVG的編輯器來進行SVG圖像的創(chuàng)作和編寫。
4.獲取到資源后,使用AndroidStudio插件完成SVG添加,AS會自動生成兼容性圖片(高版本會生成xxx.xml的SVG圖片;低版本會自動生成xxx.png圖片)。具體過程看Vector Asset Studio的使用
5.最后介紹幾個可以獲取SVG資源的網(wǎng)站
http://www.shejidaren.com/8000-flat-icons.html
http://www.flaticon.com/
http://www.iconfont.cn/plus
2.5 適配中的一些坑
在正式開始擼代碼前,先解決適配問題。
由于vector drawable是5.0之后才出來的東西,所以我們需要對之前的版本進行兼容。假設(shè)大家都使用Android Studio 2.2以上的版本,并且gradle版本在2.0以上(應(yīng)該沒有原始人吧)。下面是配置的步驟:
1.1、添加
· defaultConfig {
vectorDrawables.useSupportLibrary = true
}
1.2、添加
compile 'com.android.support:appcompat-v7:25.3.1' //需要是23.2 版本以上的
1.3、Activity需要繼承與AppCompatActivity
1.4、布局文件當中添加
xmlns:app="http://schemas.android.com/apk/res-auto"
1.5、使用在Actvity前面添加一個flag設(shè)置
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
Vector Drawable可以理解為一張圖片,所以能設(shè)置到其他的控件之中。
1 ImageView、ImageButton
XML app:srcCompat(5.0以上可以直接使用background)
代碼里面使用無區(qū)別,直接setBackground即可。
2. Button
不支持app:srcCompat
Xm使用在Button的selector中
3. RadioButton
直接使用
4. textview的drawable
直接使用
3.Vector Drawable的使用
3.1 Vector Drawable靜態(tài)使用
結(jié)合上文內(nèi)容,去阿里svg平臺隨便找張svg的圖片,既可以通過svg2android也可以通過AS自帶的插件將其轉(zhuǎn)化為vector drawable,接著配置項目兼容環(huán)境,再將這個vector在資源xml中用app:srcCompat賦值給ImageView,最后的結(jié)果就是這樣,無論怎樣放大都不會失真。

如果你在自己的安卓機上也實現(xiàn)了這樣的效果,恭喜!關(guān)于Vector Drawable最基本的靜態(tài)使用已經(jīng)被你掌控了!
3.2 Vector Drawable動態(tài)使用
大聲告訴我,android中有幾種動畫的實現(xiàn)方式?除了幀、補間、屬性動畫以外,vector drawable也可以用來完成動畫效果,還記得之前講的path標簽嗎,這里面的屬性都可以作為動畫的變化條件,我們再展示一下:
android:name 定義該 path 的名字,這樣在其他地方可以通過名字來引用這個路徑
android:pathData 和 SVG 中 d 元素一樣的路徑信息。
android:fillColor 定義填充路徑的顏色,如果沒有定義則不填充路徑
android:strokeColor 定義如何繪制路徑邊框,如果沒有定義則不顯示邊框
android:strokeWidth 定義路徑邊框的粗細尺寸
android:strokeAlpha 定義路徑邊框的透明度
android:fillAlpha 定義填充路徑顏色的透明度
android:trimPathStart 從路徑起始位置截斷路徑的比率,取值范圍從 0 到1
android:trimPathEnd 從路徑結(jié)束位置截斷路徑的比率,取值范圍從 0 到1
android:trimPathOffset 設(shè)置路徑截取的范圍
android:strokeLineCap 設(shè)置路徑線帽的形狀,取值為 butt, round, square.
android:strokeLineJoin 設(shè)置路徑交界處的連接方式,取值為 miter,round,bevel.
android:strokeMiterLimit 設(shè)置斜角的上限
剩下就是滿滿的套路了。
首先,獲取到一張vector圖片,比如這次使用的是一個對勾。我們給path標簽附上了name屬性,這是為了之后在動畫中找到這條path。
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:name="path_check"
android:fillColor="#FF000000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>
接著,用動畫vector包裝原來的vector圖片,其創(chuàng)建方式和vector相似,只不過最外層的標簽為animated-vector。我們還要為target標簽賦值,name屬性是前面命名的、vector中需要變化的地方,而animation自然就是屬性動畫了。
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_done_black_24dp">
<target
android:name="path_check"
android:animation="@animator/check_animator"/>
</animated-vector>
歸根結(jié)底還是需要用到屬性動畫,我們通過xml的方式來完成它。注意要在res下創(chuàng)建animator文件夾,再將xml放入其中。這里變化的屬性是path標簽中trimPathEnd屬性。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:duration="500"
android:propertyName="trimPathEnd"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"/>
</set>
最后,只要在代碼中將Drawable轉(zhuǎn)化為Animatable,并調(diào)用其start()方法開啟動畫即可。
<ImageView
android:id="@+id/iv"
android:layout_width="240dp"
android:layout_height="240dp"
app:srcCompat="@drawable/check_animator"
/>
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Animatable animatable = (Animatable) imageView.getDrawable();
animatable.start();
}
});
來看看效果圖吧

4.交互式中國地圖
我們已經(jīng)掌握了靜態(tài)與動態(tài)的SVG使用,接下來要學習更具挑戰(zhàn)性的交互式應(yīng)用。原圖長這樣:

我們要實現(xiàn)的效果就是每個省份都能被點擊并凸顯出來。很顯然,這么一個復(fù)雜的圖形是android中其他的知識所不能解決的。先看看效果圖

下面分析思路。首先,解析SVG圖片,由于每個省份都是一個path,因此可以獲取到34個Path。又因為每個省份都有不同的顏色、被點擊時有不同的繪制方式,所以可以創(chuàng)建provinceItem對象來封裝這些參數(shù)和方法。最后是點擊事件的控制與判斷,如果當前觸摸點在某個省份內(nèi),就將其輪廓突出。
4.1 ProvinceItem
我們以小博大,先從ProvinceItem對象開始介紹。該對象有2個參數(shù),分別是從SVG中解析出來的path以及該path需要填充的顏色color。每個“省”都提供了繪制方法,用來讓外部的地圖控件調(diào)用,以此繪制普通狀態(tài)或者選中狀態(tài)。
/**
* 是否被選擇
*
* @param canvas
* @param paint
* @param isSelected
*/
public void draw(Canvas canvas, Paint paint, boolean isSelected) {
if (isSelected) {
paint.setStrokeWidth(3);
paint.setColor(Color.BLACK);
}else {
paint.setStrokeWidth(1);
paint.setColor(0xFFD0E8F4);
}
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path,paint);
paint.setColor(drawColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawPath(path, paint);
}
普通狀態(tài)和選中狀態(tài)的區(qū)別只是外部輪廓的顏色和粗細,使用paint分別繪制其輪廓和填充顏色即可。
重點在于判斷觸摸點是否在某個省的范圍內(nèi)。每個省都是不規(guī)則的圖形,說到不規(guī)則,是否想起之前講Canvas時介紹的Region?Region代表一塊區(qū)域,其面積的計算是使用微積分的原理,正好在此派上用場。
public boolean isTouch(int x, int y) {
RectF rectF=new RectF();
path.computeBounds(rectF,true);
Region region=new Region();
region.setPath(path,new Region((int)rectF.left,(int)rectF.top,(int)rectF.right,(int)rectF.bottom));
return region.contains(x,y);
}
無論是多么不規(guī)則的圖形,總會有頂點的上下左右,我們通過path.computeBounds()計算出這個上下左右的邊界,再通過region.setPath()將path和上下左右傳入,即可獲取path所對應(yīng)的那塊region。
4.2 MapView
下面介紹外層的地圖控件MapView,在初始化方法中,loadThread用來從SVG中加載數(shù)據(jù),GestureDetectorCompat用來代理onTounch()中的觸摸事件,沒什么多余的意思,就是簡單不用謝swich語句而已……
private void init(Context context) {
this.mContext = context;
mProvinceItems = new ArrayList<>();
mPaint = new Paint();
mPaint.setAntiAlias(true);
loadThread.start();
mGestureDetectorCompat = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
handleTouch(e.getX(), e.getY());
return true;
}
});
}
先看在子線程中加載數(shù)據(jù)的操作,由于svg是以xml的形式展現(xiàn)的,所以先要解析xml。這里使用了dom解析,當然你喜歡sax或者pull或者別的什么都無所謂。
Thread loadThread = new Thread() {
@Override
public void run() {
List<ProvinceItem> items = new ArrayList<>();//用新的list防止加載時沖突導致crash
InputStream inputStream = mContext.getResources().openRawResource(R.raw.map_china);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(inputStream);
Element root = doc.getDocumentElement();
NodeList list = root.getElementsByTagName("path");
for (int i = 0; i < list.getLength(); i++) {
Element element = (Element) list.item(i);
String pathData = element.getAttribute("android:pathData");
Path path = PathParser.createPathFromPathData(pathData);
ProvinceItem item = new ProvinceItem(path);
items.add(item);
}
} catch (Exception e) {
e.printStackTrace();
}
mProvinceItems = items;
mHandler.sendEmptyMessage(1);
}
};
這段代碼的重點其實在PathParser.createPathFromPathData(pathData)這行,其作用是將vector drawable中的path語法轉(zhuǎn)化為android中的Path類。這不是一件簡單的差事,但這又是一件需求很廣泛的差事,所以我選擇使用開源的類比如CSDN上就有的下載,這里限于篇幅我只把這個方法單獨拉出來溜溜,有興趣的同學去找個工具類自己學習吧:
public static Path createPathFromPathData(String pathData) {
Path path = new Path();
PathDataNode[] nodes = createNodesFromPathData(pathData);
if (nodes != null) {
try {
PathDataNode.nodesToPath(nodes, path);
} catch (RuntimeException e) {
throw new RuntimeException("Error in parsing " + pathData, e);
}
return path;
}
return null;
}
回到我們的代碼中,loadThread在最后給handler發(fā)送了消息,handler作用很簡單,只是給不同的path隨機賦予顏色值
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mProvinceItems == null) {
return;
}
int totalNumber = mProvinceItems.size();
for (int i = 0; i < totalNumber; i++) {
int color;
int flag = i % 4;
switch (flag) {
case 1:
color = colorArray[1];
break;
case 2:
color = colorArray[2];
break;
case 3:
color = colorArray[3];
break;
default:
color = colorArray[0];
break;
}
mProvinceItems.get(i).setDrawColor(color);
}
postInvalidate();
}
};
handler的最后,postInvalidate()會導致重繪,進而調(diào)用onDraw()方法。這里又涉及到scale放大倍數(shù),由于svg本身的優(yōu)點就是隨便拉伸,因此給予MapView控件這個scale屬性是理所當然的。剩下就是分別繪制普通省份和被選中省份。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mProvinceItems != null) {
//放大倍數(shù)
canvas.scale(scale, scale);
for (ProvinceItem item : mProvinceItems) {
if (item != selectedItem) {
item.draw(canvas, mPaint, false);
}
}
if (selectedItem != null) {
selectedItem.draw(canvas, mPaint, true);
}
}
}
最后來看看觸摸事件,onTouchEvent中直接回調(diào)mGestureDetectorCompat的方法。
@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetectorCompat.onTouchEvent(event);
}
這里為了好看封裝了下
mGestureDetectorCompat = new GestureDetectorCompat(mContext, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
handleTouch(e.getX(), e.getY());
return true;
}
});
最后一個重點,由于之前的canvas拉伸過,所以在處理點擊位置時需要還原。
private void handleTouch(float x, float y) {
if (mProvinceItems == null) {
return;
}
ProvinceItem tmpItem = null;
for (ProvinceItem item : mProvinceItems) {
if (item.isTouch((int) (x / scale), (int) (y / scale))) {
tmpItem = item;
break;
}
}
if (tmpItem != null) {
selectedItem = tmpItem;
postInvalidate();
}
}
5.總結(jié)
代碼分析完畢,是不是還挺簡單的?其實最難的部分美工已經(jīng)幫我們解決了,我們只要解析SVG獲取到相應(yīng)的屬性,在通過path啊,paint啊之流去處理這些屬性,就可以輕松的完成一些復(fù)雜的自定義控件了。
最后祝大家不會被美工分而食之!