SVG相關知識與應用

SVG,即Scalable Vector Graphics 可伸縮矢量圖形,這種圖像格式在前端中已經使用的非常廣泛了。
首先要解釋下什麼是矢量圖像,什麼是位圖圖像?
1、矢量圖像:SVG是W3C 推出的一種開放標準的文本式矢量圖形描述語言,他是基於XML的、專門爲網絡而設計的圖像格式,SVG是一種採用XML來描述二維圖形的語言,所以它可以直接打開xml文件來修改和編輯。
2、位圖圖像:位圖圖像的存儲單位是圖像上每一點的像素值,因而文件會比較大,像GIF、JPEG、PNG等都是位圖圖像格式。
Vector,在Android中指的是VectorDrawable,也就是Android中的矢量圖,可以說Vector就是Android中的SVG實現(並不是支持全部的SVG語法),Vector圖像剛發佈的時候,是隻支持Android 5.0+的,自從AppCompat 23.2之後,Vector可以使用於Android 2.1以上的所有系統,只需要引用com.android.support:appcompat-v7:23.2.0以上的版本就可以了。(所謂的兼容即低版本非真實使用SVG,而是生成PNG圖片)

Vector Drawable相對於普通的Drawable來說,有以下幾個好處:
(1)Vector圖像可以自動進行適配,不需要通過分辨率來設置不同的圖片。
(2)Vector圖像可以大幅減少圖像的體積,同樣一張圖,用Vector來實現,可能只有PNG的幾十分之一。
(3)使用簡單,很多設計工具,都可以直接導出SVG圖像,從而轉換成Vector圖像 功能強大。
(4)不用寫很多代碼就可以實現非常複雜的動畫 成熟、穩定,前端已經非常廣泛的進行使用了。

Vector 語法簡介
通過使用它的Path標籤,幾乎可以實現SVG中的其它所有標籤,雖然可能會複雜一點,但這些東西都是可以通過工具來完成的,所以,不用擔心寫起來會很複雜。
Path標籤中的android:pathData數據則是由以下指令組成的:
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():關閉路徑(會自動繪製鏈接起點和終點)
注意:關於這些語法,開發者不需要全部精通,而是能夠看懂即可,這些path標籤及數據生成都可以交給工具來實現。
(一般美工來幫你搞定!PS、Illustrator等等都支持導出SVG圖片);沒必要去學習使用這些設計工具,開發者可以利用一些工具,自己轉換一些比較基礎的圖像,可以先用http://editor.method.ac/ 生成SVG圖片,然後用http://inloop.github.io/svg2android/ 生成 VectorDrawable xml代碼;我們也可以使用Android studio生成SVG,打開一個項目,在Android視圖中,右擊res文件夾,選擇New->Vector Asset

Android中的VectorDrawable的xml代碼如下:

<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:fillColor="#FF000000"
        android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"/>
</vector>

vector標籤是用來定義這個矢量圖的,該元素包含如下屬性:
android:name 定義該drawable的名字
android:width 定義該 drawable 的內部(intrinsic)寬度,支持所有 Android 系統支持的尺寸,通常使用 dp
android:height 定義該 drawable 的內部(intrinsic)高度,支持所有 Android 系統支持的尺寸,通常使用 dp
android:viewportWidth 定義矢量圖視圖的寬度,視圖就是矢量圖 path 路徑數據所繪製的虛擬畫布
android:viewportHeight 定義矢量圖視圖的高度,視圖就是矢量圖 path 路徑數據所繪製的虛擬畫布
android:tint 定義該 drawable 的 tint 顏色。默認是沒有 tint 顏色的
android:tintMode 定義 tint 顏色的 Porter-Duff blending 模式,默認值爲 src_in
android:autoMirrored 設置當系統爲 RTL (right-to-left) 佈局的時候,是否自動鏡像該圖片。比如 阿拉伯語。
android:alpha 該圖片的透明度屬性

path標籤中的pathData就是矢量圖的路徑數據,除此之外還可以設置其他屬性。 path 元素一共包含如下屬性:
android:name 定義該 path 的名字,這樣在其他地方可以通過名字來引用這個路徑
android:pathData 和 SVG 中 d 元素一樣的路徑信息。
android:fillColor 定義填充路徑的顏色,如果沒有定義則不填充路徑
android:strokeColor 定義如何繪製路徑邊框,如果沒有定義則不顯示邊框
android:strokeWidth 定義路徑邊框的粗細尺寸
android:strokeAlpha 定義路徑邊框的透明度
android:fillAlpha 定義填充路徑顏色的透明度
android:trimPathStart 從路徑起始位置截斷路徑的比率,取值範圍從 0 到1
android:trimPathEnd 從路徑結束位置截斷路徑的比率,取值範圍從 0 到1
android:trimPathOffset 設置路徑截取的範圍 Shift trim region (allows showed region to include the start and end), in the range from 0 to 1.
android:strokeLineCap 設置路徑線帽的形狀,取值爲 butt, round, square.
android:strokeLineJoin 設置路徑交界處的連接方式,取值爲 miter,round,bevel.
android:strokeMiterLimit 設置斜角的上限,Sets the Miter limit for a stroked path. 注:當strokeLineJoin設置爲 “miter” 的時候, 繪製兩條線段以銳角相交的時候,所得的斜面可能相當長。當斜面太長,就會變得不協調。strokeMiterLimit 屬性爲斜面的長度設置一個上限。這個屬性表示斜面長度和線條長度的比值。默認是 10,意味着一個斜面的長度不應該超過線條寬度的 10 倍。如果斜面達到這個長度,它就變成斜角了。當 strokeLineJoin 爲 “round” 或 “bevel” 的時候,這個屬性無效。

group標籤可以把多個 path 放到一起,group 主要是用來設置路徑做動畫的關鍵屬性的。 group 支持的屬性如下:
android:name 定義 group 的名字
android:rotation 定義該 group 的路徑旋轉多少度
android:pivotX 定義縮放和旋轉該 group 時候的 X 參考點。該值相對於 vector 的 viewport 值來指定的。
android:pivotY 定義縮放和旋轉該 group 時候的 Y 參考點。該值相對於 vector 的 viewport 值來指定的。
android:scaleX 定義 X 軸的縮放倍數
android:scaleY 定義 Y 軸的縮放倍數
android:translateX 定義移動 X 軸的位移。相對於 vector 的 viewport 值來指定的。
android:translateY 定義移動 Y 軸的位移。相對於 vector 的 viewport 值來指定的。

clip-path標籤定義當前繪製的剪切路徑。注意,clip-path 只對當前的 group 和子 group 有效,屬性如下:
android:name 定義 clip path 的名字
android:pathData

相關兼容問題
兼容5.0以下的版本:使用Android Studio 2.2以上的版本,gradle版本在2.0以上

1.添加
	defaultConfig {
		vectorDrawables.useSupportLibrary = true
	}
2.添加
	compile 'com.android.support:appcompat-v7:25.3.1' //需要是23.2 版本以上的
3.Activity需要繼承與AppCompatActivity
4.使用在Actvity前面添加一個flag設置
	static {
		AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
	}
5.Button 不支持app:srcCompat
	Xml 使用在Button的selector
	RadioButton 直接使用
	textview的drawable  直接使用
	使用的動態Vector Drawable
	主要是不能直接修改 pathData
	不能使用自定義interpolator

實例(一)使用svg實現點擊打勾效果
在這裏插入圖片描述在這裏插入圖片描述
實現這個動畫效果主要是通過修改VectorDrawable中Path的android:trimPathEnd屬性來實現
這個屬性android:trimPathEnd 從路徑結束位置截斷路徑的比率,取值範圍從 0 到1,表示是從最開始到當前值結束的路徑
1.首先獲取勾的svg圖片xml文件,給Path命名爲gou,方便做動畫使用

<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="gou"
        android:fillColor="#FF000000"
        android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

2.添加AnimatedVectorDrawable對應的xml文件
animated-vector標籤中指定執行動畫的svg資源文件
target標籤表示動畫執行對象name爲gou的Path,執行動畫爲check_anim

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_check">
    <target
        android:animation="@animator/check_anim"
        android:name="gou"/>
</animated-vector>

3.添加自定義動畫文件
這裏使用了屬性動畫objectAnimator,對Path的trimPathEnd這個屬性值進行修改,修改範圍爲0.0f-1.0f

<?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>

4.到這裏所有動態Vector Drawable相關配置完成,接下來在控件中使用

<ImageView
        android:layout_marginTop="30dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="startAnim"
        android:src="@drawable/check_anim"/>

5.要實現動畫效果,這樣還不行還需要手動播放動畫

public void startAnim(View view){
        ImageView imageView= (ImageView) view;
        Drawable drawable=imageView.getDrawable();
        Animatable animatable= (Animatable) drawable;
        animatable.start();
    }

(二)實現搜索框動畫效果
在這裏插入圖片描述在這裏插入圖片描述
實現此動畫的關鍵在於修改Path中的trimPathStart屬性來實現

1.生成VectorDrawable文件searchbar.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="150dp"
        android:height="24dp"
        android:viewportHeight="24"
        android:viewportWidth="150">

    <path
        android:name="search"
        android:pathData="M141,17 A9,9 0 1,1 142,16 L149,23"
        android:strokeAlpha="0.8"
        android:strokeColor="#000000"
        android:strokeLineCap="round"
        android:strokeWidth="2"/>
    <path
        android:name="bar"
        android:pathData="M0,23 L149,23"
        android:strokeAlpha="0.8"
        android:strokeColor="#000000"
        android:strokeLineCap="square"
        android:strokeWidth="2"/>
</vector>

2.添加AnimatedVectorDrawable文件

<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/searchbar">

    <target
        android:name="search"
        android:animation="@animator/anim_searchbar_in"/>

    <target
        android:name="bar"
        android:animation="@animator/anim_searchbar_out"/>

</animated-vector>

3.根據實現的效果來寫動畫文件
anim_searchbar_in.xml

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:propertyName="trimPathStart"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="0"
    android:valueTo="1"
    android:valueType="floatType"/>

anim_searchbar_out.xml

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:propertyName="trimPathStart"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="1"
    android:valueTo="0"
    android:valueType="floatType"/>

4.代碼中開啓動畫效果

public void startAnim(View view){
        ImageView imageView= (ImageView) view;
        Drawable drawable=imageView.getDrawable();
        Animatable animatable= (Animatable) drawable;
        animatable.start();
    }

(三)實現繪製圖形的效果
效果圖
實現此動畫的關鍵在於修改Path中的trimPathEnd和strokeColor屬性來實現,group用於控制當前path的縮放
1.添加VectorDrawable文件star.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="200dp"
        android:height="200dp"
        android:viewportHeight="500"
        android:viewportWidth="500">
    <group
        android:scaleX="5.0"
        android:scaleY="5.0">
        <path
            android:name="star"
            android:pathData="M 50.0,90.0 L 82.9193546357,27.2774101308 L 12.5993502926,35.8158045183 L 59.5726265715,88.837672697 L 76.5249063296,20.0595700732 L 10.2916450361,45.1785327898 L 68.5889268818,85.4182410261 L 68.5889268818,14.5817589739 L 10.2916450361,54.8214672102 L 76.5249063296,79.9404299268 L 59.5726265715,11.162327303 L 12.5993502926,64.1841954817 L 82.9193546357,72.7225898692 L 50.0,10.0 L 17.0806453643,72.7225898692 L 87.4006497074,64.1841954817 L 40.4273734285,11.162327303 L 23.4750936704,79.9404299268 L 89.7083549639,54.8214672102 L 31.4110731182,14.5817589739 L 31.4110731182,85.4182410261 L 89.7083549639,45.1785327898 L 23.4750936704,20.0595700732 L 40.4273734285,88.837672697 L 87.4006497074,35.8158045183 L 17.0806453643,27.2774101308 L 50.0,90.0Z"
            android:strokeColor="#000000"
            android:strokeWidth="2"/>
    </group>
</vector>

2.添加AnimatedVectorDrawable文件

<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/star">

    <target
        android:name="star"
        android:animation="@animator/anim_star" />

</animated-vector>

3.根據實現的效果來編寫動畫文件anim_star.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:duration="5000"
        android:interpolator="@android:interpolator/linear"
        android:propertyName="trimPathEnd"
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:valueFrom="0"
        android:valueTo="1"
        android:valueType="floatType"/>

    <objectAnimator
        android:duration="5000"
        android:propertyName="strokeColor"
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:valueFrom="@color/colorAccent"
        android:valueTo="@color/colorPrimaryDark"/>

</set>

4.開始執行播放動畫

public void startAnim(View view){
        ImageView imageView= (ImageView) view;
        Drawable drawable=imageView.getDrawable();
        Animatable animatable= (Animatable) drawable;
        animatable.start();
    }

(四)實現兩個箭頭左右平移的動畫
效果圖
實現該效果主要通過修改Path中的translateX屬性來實現

1.添加VectorDrawable文件ic_arrow.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="120dp"
        android:height="120dp"
        android:viewportHeight="24.0"
        android:viewportWidth="24.0">

    <group android:name="left">
        <path
            android:fillColor="#FF000000"
            android:pathData="M9.01,14L2,14v2h7.01v3L13,15l-3.99,-4v3"/>
    </group>

    <group android:name="right">
        <path
            android:fillColor="#FF000000"
            android:pathData="M14.99,13v-3L22,10L22,8h-7.01L14.99,5L11,9l3.99,4"/>
    </group>

</vector>

2.添加AnimatedVectorDrawable文件

<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_arrow">

    <target
        android:name="left"
        android:animation="@animator/anim_left"/>

    <target
        android:name="right"
        android:animation="@animator/anim_right"/>

</animated-vector>

3.根據實現的效果來編寫動畫文件anim_left.xml,anim_right.xml

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:interpolator="@android:interpolator/anticipate_overshoot"
    android:propertyName="translateX"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="0"
    android:valueTo="-10"
    android:valueType="floatType"/>

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:interpolator="@android:interpolator/anticipate_overshoot"
    android:propertyName="translateX"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="0"
    android:valueTo="10"
    android:valueType="floatType"/>

4.開始執行播放動畫

public void startAnim(View view){
        ImageView imageView= (ImageView) view;
        Drawable drawable=imageView.getDrawable();
        Animatable animatable= (Animatable) drawable;
        animatable.start();
    }

(五)實現一個顏色不斷變化的矩形
效果圖
實現該效果主要通過修改Path中的fillColor屬性來實現

1.添加VectorDrawable文件square.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="200dp"
        android:height="200dp"
        android:viewportHeight="500"
        android:viewportWidth="500">

    <path
        android:name="square"
        android:fillColor="#000000"
        android:pathData="M100,100 L400,100 L400,400 L100,400 z"/>

</vector>

2.添加AnimatedVectorDrawable文件

<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/square">

    <target
        android:name="square"
        android:animation="@animator/anim_square_color" />

</animated-vector>

3.根據實現的效果來編寫動畫文件anim_square_color.xml

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:interpolator="@android:interpolator/decelerate_cubic"
    android:propertyName="fillColor"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="@android:color/holo_green_dark"
    android:valueTo="@android:color/darker_gray"
    android:valueType="intType"/>

4.開始執行播放動畫

public void startAnim(View view){
        ImageView imageView= (ImageView) view;
        Drawable drawable=imageView.getDrawable();
        Animatable animatable= (Animatable) drawable;
        animatable.start();
    }

(六) 也可以通過VectorDawable來實現一個選擇器
可以將VectorDrawable當成一個Dawable資源來使用 VectorDrawable extends Drawable

1.添加VectorDrawable文件selector1.xml,selector2.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportHeight="24.0"
        android:viewportWidth="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M14.59,8L12,10.59 9.41,8 8,9.41 10.59,12 8,14.59 9.41,16 12,13.41 14.59,16 16,14.59 13.41,12 16,9.41 14.59,8zM12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportHeight="24.0"
        android:viewportWidth="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

2.添加選擇器selector

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/selector1" android:state_pressed="true"/>
    <item android:drawable="@drawable/selector2"/>
</selector>

3.直接在佈局中引用資源即可

(七)實現一個將五星變成一個梯形的效果
效果圖
實現此動畫主要是通過修改Path中的pathData屬性來實現,
注意:valueFrom與valueTo中的值需要一 一對應起來,不然會出錯

1.添加VectorDrawable文件fivestar.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="120dp"
        android:height="120dp"
        android:viewportHeight="64"
        android:viewportWidth="64">
    <group>
        <path
            android:name="star"
            android:fillColor="#ff0000"
            android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z"
            android:strokeColor="#000000"
            android:strokeWidth="1"/>
    </group>
</vector>

2.添加AnimatedVectorDrawable文件

<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/fivestar">
    <target
        android:name="star"
        android:animation="@animator/anim_fivestar_morph" />
</animated-vector>

3.根據實現的效果來編寫動畫文件anim_fivestar_morph.xml

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:propertyName="pathData"
    android:valueFrom="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z"
    android:valueTo="M 48,54 L 31,54 15,54 10,35 6,23 25,10 32,4 40,10 58,23 54,35 z"
    android:valueType="pathType"/>

4.開始執行播放動畫

(八)實現百度圖標加載動畫效果
效果圖
實現此動畫主要是通過修改Path中的pathData屬性來實現,
注意:valueFrom與valueTo中的值需要一 一對應起來,不然會出錯

1.添加VectorDrawable文件vd_path_paw.xml

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="120dp"
    android:height="120dp"
    android:viewportHeight="24"
    android:viewportWidth="24">
    <path
        android:name="toe1"
        android:fillColor="#ffffff"
        android:pathData="M 4.5 7 C 5.88071187458 7 7 8.11928812542 7 9.5 C 7 10.8807118746 5.88071187458 12 4.5 12 C 3.11928812542 12 2 10.8807118746 2 9.5 C 2 8.11928812542 3.11928812542 7 4.5 7 Z" />
    <path
        android:name="toe2"
        android:fillColor="#ffffff"
        android:pathData="M 9 3 C 10.3807118746 3 11.5 4.11928812542 11.5 5.5 C 11.5 6.88071187458 10.3807118746 8 9 8 C 7.61928812542 8 6.5 6.88071187458 6.5 5.5 C 6.5 4.11928812542 7.61928812542 3 9 3 Z" />
    <path
        android:name="toe3"
        android:fillColor="#ffffff"
        android:pathData="M 15 3 C 16.3807118746 3 17.5 4.11928812542 17.5 5.5 C 17.5 6.88071187458 16.3807118746 8 15 8 C 13.6192881254 8 12.5 6.88071187458 12.5 5.5 C 12.5 4.11928812542 13.6192881254 3 15 3 Z" />
    <path
        android:name="toe4"
        android:fillColor="#ffffff"
        android:pathData="M 19.5 7 C 20.8807118746 7 22 8.11928812542 22 9.5 C 22 10.8807118746 20.8807118746 12 19.5 12 C 18.1192881254 12 17 10.8807118746 17 9.5 C 17 8.11928812542 18.1192881254 7 19.5 7 Z" />
    <path
        android:fillColor="#ffffff"
        android:pathData="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79 .05 c-.11 .02 -.22 .05 -.33 .09 -.7 .24 -1.28 .78 -1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79 .29 1.02 1.02 2.03 2.33 2.32 .73 .15 3.06-.44 5.54-.44h.18c2.48 0 4.81 .58 5.54 .44 1.31-.29 2.04-1.31 2.33-2.32 .31 -2.04-1.3-3.49-2.61-4.8z" />
</vector>

2.添加AnimatedVectorDrawable文件

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/vd_path_paw">
    <target
        android:name="toe1"
        android:animation="@animator/anim_path_morph_toe1"/>
    <target
        android:name="toe2"
        android:animation="@animator/anim_path_morph_toe2"/>
    <target
        android:name="toe3"
        android:animation="@animator/anim_path_morph_toe3"/>
    <target
        android:name="toe4"
        android:animation="@animator/anim_path_morph_toe4"/>
</animated-vector>

3.根據實現的效果來編寫動畫文件anim_path_morph_toe1,anim_path_morph_toe2,anim_path_morph_toe3,anim_path_morph_toe4

anim_path_morph_toe1.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially">
    <objectAnimator
        android:duration="420"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 4.5 7 C 5.88071187458 7 7 8.11928812542 7 9.5 C 7 10.8807118746 5.88071187458 12 4.5 12 C 3.11928812542 12 2 10.8807118746 2 9.5 C 2 8.11928812542 3.11928812542 7 4.5 7 Z"
        android:valueTo="M 4.5 9 C 5.88071187458 9 7 10.1192881254 7 11.5 C 7 12.8807118746 5.88071187458 14 4.5 14 C 3.11928812542 14 2 12.8807118746 2 11.5 C 2 10.1192881254 3.11928812542 9 4.5 9 Z"
        android:valueType="pathType" />
    <objectAnimator
        android:duration="840"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 4.5 9 C 5.88071187458 9 7 10.1192881254 7 11.5 C 7 12.8807118746 5.88071187458 14 4.5 14 C 3.11928812542 14 2 12.8807118746 2 11.5 C 2 10.1192881254 3.11928812542 9 4.5 9 Z"
        android:valueTo="M 4.5 4 C 5.88071187458 4 7 5.11928812542 7 6.5 C 7 7.88071187458 5.88071187458 9 4.5 9 C 3.11928812542 9 2 7.88071187458 2 6.5 C 2 5.11928812542 3.11928812542 4 4.5 4 Z"
        android:valueType="pathType" />
    <objectAnimator
        android:duration="420"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 4.5 4 C 5.88071187458 4 7 5.11928812542 7 6.5 C 7 7.88071187458 5.88071187458 9 4.5 9 C 3.11928812542 9 2 7.88071187458 2 6.5 C 2 5.11928812542 3.11928812542 4 4.5 4 Z"
        android:valueTo="M 4.5 7 C 5.88071187458 7 7 8.11928812542 7 9.5 C 7 10.8807118746 5.88071187458 12 4.5 12 C 3.11928812542 12 2 10.8807118746 2 9.5 C 2 8.11928812542 3.11928812542 7 4.5 7 Z"
        android:valueType="pathType" />
</set>

anim_path_morph_toe2.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially"
    android:shareInterpolator="true">
    <objectAnimator
        android:duration="480"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:startOffset="100"
        android:valueFrom="M 9 3 C 10.3807118746 3 11.5 4.11928812542 11.5 5.5 C 11.5 6.88071187458 10.3807118746 8 9 8 C 7.61928812542 8 6.5 6.88071187458 6.5 5.5 C 6.5 4.11928812542 7.61928812542 3 9 3 Z"
        android:valueTo="M 9 5 C 10.3807118746 5 11.5 6.11928812542 11.5 7.5 C 11.5 8.88071187458 10.3807118746 10 9 10 C 7.61928812542 10 6.5 8.88071187458 6.5 7.5 C 6.5 6.11928812542 7.61928812542 5 9 5 Z"
        android:valueType="pathType" />
    <objectAnimator
        android:duration="960"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 9 5 C 10.3807118746 5 11.5 6.11928812542 11.5 7.5 C 11.5 8.88071187458 10.3807118746 10 9 10 C 7.61928812542 10 6.5 8.88071187458 6.5 7.5 C 6.5 6.11928812542 7.61928812542 5 9 5 Z"
        android:valueTo="M 9 0 C 10.3807118746 0 11.5 1.11928812542 11.5 2.5 C 11.5 3.88071187458 10.3807118746 5 9 5 C 7.61928812542 5 6.5 3.88071187458 6.5 2.5 C 6.5 1.11928812542 7.61928812542 0 9 0 Z"
        android:valueType="pathType" />
    <objectAnimator
        android:duration="480"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 9 0 C 10.3807118746 0 11.5 1.11928812542 11.5 2.5 C 11.5 3.88071187458 10.3807118746 5 9 5 C 7.61928812542 5 6.5 3.88071187458 6.5 2.5 C 6.5 1.11928812542 7.61928812542 0 9 0 Z"
        android:valueTo="M 9 3 C 10.3807118746 3 11.5 4.11928812542 11.5 5.5 C 11.5 6.88071187458 10.3807118746 8 9 8 C 7.61928812542 8 6.5 6.88071187458 6.5 5.5 C 6.5 4.11928812542 7.61928812542 3 9 3 Z"
        android:valueType="pathType" />
</set>

anim_path_morph_toe3.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially"
    android:shareInterpolator="true">
    <objectAnimator
        android:duration="420"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:startOffset="200"
        android:valueFrom="M 15 3 C 16.3807118746 3 17.5 4.11928812542 17.5 5.5 C 17.5 6.88071187458 16.3807118746 8 15 8 C 13.6192881254 8 12.5 6.88071187458 12.5 5.5 C 12.5 4.11928812542 13.6192881254 3 15 3 Z"
        android:valueTo="M 15 5 C 16.3807118746 5 17.5 6.11928812542 17.5 7.5 C 17.5 8.88071187458 16.3807118746 10 15 10 C 13.6192881254 10 12.5 8.88071187458 12.5 7.5 C 12.5 6.11928812542 13.6192881254 5 15 5 Z"
        android:valueType="pathType" />
    <objectAnimator
        android:duration="840"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 15 5 C 16.3807118746 5 17.5 6.11928812542 17.5 7.5 C 17.5 8.88071187458 16.3807118746 10 15 10 C 13.6192881254 10 12.5 8.88071187458 12.5 7.5 C 12.5 6.11928812542 13.6192881254 5 15 5 Z"
        android:valueTo="M 15 0 C 16.3807118746 0 17.5 1.11928812542 17.5 2.5 C 17.5 3.88071187458 16.3807118746 5 15 5 C 13.6192881254 5 12.5 3.88071187458 12.5 2.5 C 12.5 1.11928812542 13.6192881254 0 15 0 Z"
        android:valueType="pathType" />
    <objectAnimator
        android:duration="420"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 15 0 C 16.3807118746 0 17.5 1.11928812542 17.5 2.5 C 17.5 3.88071187458 16.3807118746 5 15 5 C 13.6192881254 5 12.5 3.88071187458 12.5 2.5 C 12.5 1.11928812542 13.6192881254 0 15 0 Z"
        android:valueTo="M 15 3 C 16.3807118746 3 17.5 4.11928812542 17.5 5.5 C 17.5 6.88071187458 16.3807118746 8 15 8 C 13.6192881254 8 12.5 6.88071187458 12.5 5.5 C 12.5 4.11928812542 13.6192881254 3 15 3 Z"
        android:valueType="pathType" />
</set>

anim_path_morph_toe4.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially"
    android:shareInterpolator="true">
    <objectAnimator
        android:duration="450"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:startOffset="300"
        android:valueFrom="M 19.5 7 C 20.8807118746 7 22 8.11928812542 22 9.5 C 22 10.8807118746 20.8807118746 12 19.5 12 C 18.1192881254 12 17 10.8807118746 17 9.5 C 17 8.11928812542 18.1192881254 7 19.5 7 Z"
        android:valueTo="M 19.5 9 C 20.8807118746 9 22 10.1192881254 22 11.5 C 22 12.8807118746 20.8807118746 14 19.5 14 C 18.1192881254 14 17 12.8807118746 17 11.5 C 17 10.1192881254 18.1192881254 9 19.5 9 Z"
        android:valueType="pathType" />
    <objectAnimator
        android:duration="900"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 19.5 7 C 20.8807118746 7 22 8.11928812542 22 9.5 C 22 10.8807118746 20.8807118746 12 19.5 12 C 18.1192881254 12 17 10.8807118746 17 9.5 C 17 8.11928812542 18.1192881254 7 19.5 7 Z"
        android:valueTo="M 19.5 4 C 20.8807118746 4 22 5.11928812542 22 6.5 C 22 7.88071187458 20.8807118746 9 19.5 9 C 18.1192881254 9 17 7.88071187458 17 6.5 C 17 5.11928812542 18.1192881254 4 19.5 4 Z"
        android:valueType="pathType" />
    <objectAnimator
        android:duration="450"
        android:propertyName="pathData"
        android:repeatCount="-1"
        android:repeatMode="reverse"
        android:valueFrom="M 19.5 4 C 20.8807118746 4 22 5.11928812542 22 6.5 C 22 7.88071187458 20.8807118746 9 19.5 9 C 18.1192881254 9 17 7.88071187458 17 6.5 C 17 5.11928812542 18.1192881254 4 19.5 4 Z"
        android:valueTo="M 19.5 7 C 20.8807118746 7 22 8.11928812542 22 9.5 C 22 10.8807118746 20.8807118746 12 19.5 12 C 18.1192881254 12 17 10.8807118746 17 9.5 C 17 8.11928812542 18.1192881254 7 19.5 7 Z"
        android:valueType="pathType" />
</set>

(九)實現一個具有交互效果的中國地圖
效果圖
實現步驟:
1.首先從網站上去下載中國地圖的SVG,然後再通過工具將該svg文件轉換爲Android中的Vector Drawable資源
2.將轉換後的chinahigh.xml文件放入項目的drawable目錄下
3.接下來解析chinahigh.xml文件將從xml中解析的Path相關數據保存到ProvinceItem對象中
4.然後自定義View-ChinaMap在onDraw方法中繪製解析出來的所有Path
5.通過監聽View的onTouchEvent方法來實現點擊地圖填充顯示效果

使用到的關鍵技術
1.XmlPullParser解析xml文件,獲取Path中的數據
2.將android:pathData中的字符串轉換成Path類,使用工具類PathParser
3.判斷當前用戶點擊位置在哪個省的區域中,這裏先將Path的外圍矩形區域計算出來,然後通過給Region設置一個Path和矩形區域來進行取交集,得到省份所在區域,再通過Region的contains(x,y)方法來判斷當前點擊的省份

自定義地圖View ChinaMap代碼如下:

public class ChinaMap extends View {

    //畫筆顏色值數組
    private static final int color[]={Color.RED,Color.BLACK,Color.GREEN,Color.CYAN};

    private List<ProvinceItem> provinceList;//存儲所有省的Path數據
    private Context mContext;
    private Paint mPaint;
    private float scale=1.3f;//爲了地圖能完整顯示到手機上,這裏對其進行一定縮放

    private ExecutorService mExecutorService;

    public ChinaMap(Context context) {
        this(context, null);
    }

    public ChinaMap(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ChinaMap(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext=context;
        initData();
    }

    private void initData(){
        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.BLUE);
        mPaint.setStrokeWidth(5);

        provinceList=new ArrayList<>();
        //使用線程池開啓一個線程來對Vector Drawable 文件中的數據進行解析
        mExecutorService= Executors.newSingleThreadExecutor();
        mExecutorService.execute(new Runnable() {
            @Override
            public void run() {
                parseXml(mContext);
            }
        });
    }
    /*
        解析地圖文件VectorDrawable數據 將解析的數據封裝到provinceList集合中
     */
    private void parseXml(Context context) {

        InputStream inputStream = null;
        try {
            //獲取Vector文件資源的流
            inputStream = context.getResources().openRawResource(R.raw.chinahigh);
            //創建XmlPullParser用於解析xml文件
            XmlPullParser xmlParser = Xml.newPullParser();
            //設置要解析的文件流和流讀取格式
            xmlParser.setInput(inputStream, "utf-8");
            //獲取當前標籤的類型
            int eventType=xmlParser.getEventType();
            //當獲取的標籤是文檔結束標籤時,不再讀取文件
            while (eventType != XmlPullParser.END_DOCUMENT){
                //判斷當前標籤爲開始標籤 例如:開始標籤<path> 結束標籤</path>
                if(eventType == XmlPullParser.START_TAG){
                    //如果當前標籤爲path則會獲取它的相關數據
                    if("path".equals(xmlParser.getName())){
                        //創建一個實體類用於保存Path的信息
                        ProvinceItem provinceEntity=new ProvinceItem();
                        //這裏是獲取path標籤的android:pathData這個屬性的值 屬性索引爲3
                        String text=xmlParser.getAttributeValue(3);
                        //將字符串數據解析成Path對象
                        Path path=PathParser.createPathFromPathData(text);
                        provinceEntity.setPath(path);
                        //這裏爲每一個省 隨機一個顏色值顯示
                        provinceEntity.setFillColor(color[(int) (Math.random()*(color.length-1))]);
                        provinceList.add(provinceEntity);
                    }
                }
                eventType=xmlParser.next();//獲取下一個標籤
            }
            //xml文件解析完成後,通知界面刷新UI
            postInvalidate();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //繪製解析出來的所有省份
        if(provinceList != null && provinceList.size()>0){
            //將畫布縮放 以便地圖正常顯示到手機上
            canvas.scale(scale, scale);
            for (ProvinceItem province:provinceList){
                //通知每個省進行界面繪製
                province.onDraw(canvas,mPaint);
            }
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            //監聽按下事件 當用戶按下地圖時選中按下所在區域地圖
            case MotionEvent.ACTION_DOWN:
                if(provinceList != null && provinceList.size()>0){
                    //判斷當前手指點擊是否在某個省的範圍內
                    for (ProvinceItem province:provinceList){
                    	//因爲上面對canvas進行了縮放 這裏需要對點擊的座標進行相應縮放,才能取到正確區域位置
                        province.checkTouchScope(event.getX()/scale,event.getY()/scale);
                    }
                    //刷新地圖顯示
                    postInvalidate();
                }
                break;
        }
        return super.onTouchEvent(event);
    }
}

每個省份代表的區域用ProvinceItem類封裝實現,代碼如下:

public class ProvinceItem {

    private Path mPath;//該省在地圖上的路徑 需要將path繪製到View上
    private int mFillColor;//繪製該省的顏色
    private boolean isSelected;//表示是否點擊了此區域

    public Path getPath() {
        return mPath;
    }

    public void setPath(Path mPath) {
        this.mPath = mPath;
    }

    public int getFillColor() {
        return mFillColor;
    }

    public void setFillColor(int mFillColor) {
        this.mFillColor = mFillColor;
    }

    /**
     * 每個省自己繪製自己的Path
     * @param canvas
     * @param paint
     */
    public void onDraw(Canvas canvas, Paint paint){
        //被選中的區域則使用填充方式繪製地圖
        if(isSelected){
            paint.setStyle(Paint.Style.FILL_AND_STROKE);
        }else {
            paint.setStyle(Paint.Style.STROKE);
        }
        paint.setColor(mFillColor);
        canvas.drawPath(mPath,paint);
    }

    /**
     * 檢查當前手指點擊位置是否爲本省所在區域
     * @param x x位置
     * @param y y位置
     */
    public void checkTouchScope(float x,float y){
        //創建一個區域 用於表示當前省份所在區域
        Region region=new Region();
        //創建一個矩形 此爲path路徑所在區域的最小矩形
        RectF pathBound=new RectF();
        //計算Path外圍的最小矩形 將計算的值保存到pathBound中
        mPath.computeBounds(pathBound,false);
        //該方法用於計算Path所在區域與它的外圍最小矩形區域之間的交集區域 將交集區域保存到region中
        region.setPath(mPath,new Region((int) pathBound.left,(int)pathBound.top,
                (int)pathBound.right,(int)pathBound.bottom));
        //判斷當前手值按下的位置是否在當前省份的區域當中
        isSelected=region.contains((int)x,(int) y);
    }

}

使用到的工具類,將path標籤中的pathData字符串數據轉換成Path類

/**
 * path 路徑解析兼容類(兼容標準svg)
 */
public class PathParser {

    private static final String LOG_TAG = "PathParser";

// Copy from Arrays.copyOfRange() which is only available from API level 9.

    /**
     * Copies elements from {@code original} into a new array, from indexes start (inclusive) to
     * end (exclusive). The original order of elements is preserved.
     * If {@code end} is greater than {@code original.length}, the result is padded
     * with the value {@code 0.0f}.
     *
     * @param original the original array
     * @param start    the start index, inclusive
     * @param end      the end index, exclusive
     * @return the new array
     * @throws IllegalArgumentException       if {@code start > end}
     * @throws NullPointerException           if {@code original == null}
     */
    private static float[] copyOfRange(float[] original, int start, int end) {
        if (start > end) {
            throw new IllegalArgumentException();
        }
        int originalLength = original.length;
        if (start < 0 || start > originalLength) {
            throw new ArrayIndexOutOfBoundsException();
        }
        int resultLength = end - start;
        int copyLength = Math.min(resultLength, originalLength - start);
        float[] result = new float[resultLength];
        System.arraycopy(original, start, result, 0, copyLength);
        return result;
    }

    /**
     * @param pathData The string representing a path, the same as "d" string in svg file.
     * @return the generated Path object.
     */
    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;
    }

    /**
     * @param pathData The string representing a path, the same as "d" string in svg file.
     * @return an array of the PathDataNode.
     */
    public static PathDataNode[] createNodesFromPathData(String pathData) {
        if (pathData == null) {
            return null;
        }
        int start = 0;
        int end = 1;

        ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
        while (end < pathData.length()) {
            end = nextStart(pathData, end);
            String s = pathData.substring(start, end).trim();
            if (s.length() > 0) {
                float[] val = getFloats(s);
                addNode(list, s.charAt(0), val);
            }

            start = end;
            end++;
        }
        if ((end - start) == 1 && start < pathData.length()) {
            addNode(list, pathData.charAt(start), new float[0]);
        }
        return list.toArray(new PathDataNode[list.size()]);
    }

    /**
     * @param source The array of PathDataNode to be duplicated.
     * @return a deep copy of the <code>source</code>.
     */
    public static PathDataNode[] deepCopyNodes(PathDataNode[] source) {
        if (source == null) {
            return null;
        }
        PathDataNode[] copy = new PathDataNode[source.length];
        for (int i = 0; i < source.length; i++) {
            copy[i] = new PathDataNode(source[i]);
        }
        return copy;
    }

    /**
     * @param nodesFrom The source path represented in an array of PathDataNode
     * @param nodesTo   The target path represented in an array of PathDataNode
     * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
     */
    public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) {
        if (nodesFrom == null || nodesTo == null) {
            return false;
        }

        if (nodesFrom.length != nodesTo.length) {
            return false;
        }

        for (int i = 0; i < nodesFrom.length; i++) {
            if (nodesFrom[i].type != nodesTo[i].type
                    || nodesFrom[i].params.length != nodesTo[i].params.length) {
                return false;
            }
        }
        return true;
    }

    /**
     * Update the target's data to match the source.
     * Before calling this, make sure canMorph(target, source) is true.
     *
     * @param target The target path represented in an array of PathDataNode
     * @param source The source path represented in an array of PathDataNode
     */
    public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
        for (int i = 0; i < source.length; i++) {
            target[i].type = source[i].type;
            for (int j = 0; j < source[i].params.length; j++) {
                target[i].params[j] = source[i].params[j];
            }
        }
    }

    private static int nextStart(String s, int end) {
        char c;

        while (end < s.length()) {
            c = s.charAt(end);
            // Note that 'e' or 'E' are not valid path commands, but could be
            // used for floating point numbers' scientific notation.
            // Therefore, when searching for next command, we should ignore 'e'
            // and 'E'.
            if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
                    && c != 'e' && c != 'E') {
                return end;
            }
            end++;
        }
        return end;
    }

    private static void addNode(ArrayList<PathDataNode> list, char cmd, float[] val) {
        list.add(new PathDataNode(cmd, val));
    }

    private static class ExtractFloatResult {
        // We need to return the position of the next separator and whether the
        // next float starts with a '-' or a '.'.
        int mEndPosition;
        boolean mEndWithNegOrDot;
    }

    /**
     * Parse the floats in the string.
     * This is an optimized version of parseFloat(s.split(",|\\s"));
     *
     * @param s the string containing a command and list of floats
     * @return array of floats
     */
    private static float[] getFloats(String s) {
        if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') {
            return new float[0];
        }
        try {
            float[] results = new float[s.length()];
            int count = 0;
            int startPosition = 1;
            int endPosition = 0;

            ExtractFloatResult result = new ExtractFloatResult();
            int totalLength = s.length();

            // The startPosition should always be the first character of the
            // current number, and endPosition is the character after the current
            // number.
            while (startPosition < totalLength) {
                extract(s, startPosition, result);
                endPosition = result.mEndPosition;

                if (startPosition < endPosition) {
                    results[count++] = Float.parseFloat(
                            s.substring(startPosition, endPosition));
                }

                if (result.mEndWithNegOrDot) {
                    // Keep the '-' or '.' sign with next number.
                    startPosition = endPosition;
                } else {
                    startPosition = endPosition + 1;
                }
            }
            return copyOfRange(results, 0, count);
        } catch (NumberFormatException e) {
            throw new RuntimeException("error in parsing \"" + s + "\"", e);
        }
    }

    /**
     * Calculate the position of the next comma or space or negative sign
     *
     * @param s      the string to search
     * @param start  the position to start searching
     * @param result the result of the extraction, including the position of the
     *               the starting position of next number, whether it is ending with a '-'.
     */
    private static void extract(String s, int start, ExtractFloatResult result) {
        // Now looking for ' ', ',', '.' or '-' from the start.
        int currentIndex = start;
        boolean foundSeparator = false;
        result.mEndWithNegOrDot = false;
        boolean secondDot = false;
        boolean isExponential = false;
        for (; currentIndex < s.length(); currentIndex++) {
            boolean isPrevExponential = isExponential;
            isExponential = false;
            char currentChar = s.charAt(currentIndex);
            switch (currentChar) {
                case ' ':
                case ',':
                    foundSeparator = true;
                    break;
                case '-':
                    // The negative sign following a 'e' or 'E' is not a separator.
                    if (currentIndex != start && !isPrevExponential) {
                        foundSeparator = true;
                        result.mEndWithNegOrDot = true;
                    }
                    break;
                case '.':
                    if (!secondDot) {
                        secondDot = true;
                    } else {
                        // This is the second dot, and it is considered as a separator.
                        foundSeparator = true;
                        result.mEndWithNegOrDot = true;
                    }
                    break;
                case 'e':
                case 'E':
                    isExponential = true;
                    break;
            }
            if (foundSeparator) {
                break;
            }
        }
        // When there is nothing found, then we put the end position to the end
        // of the string.
        result.mEndPosition = currentIndex;
    }

    /**
     * Each PathDataNode represents one command in the "d" attribute of the svg
     * file.
     * An array of PathDataNode can represent the whole "d" attribute.
     */
    public static class PathDataNode {
        /*package*/
        char type;
        float[] params;

        private PathDataNode(char type, float[] params) {
            this.type = type;
            this.params = params;
        }

        private PathDataNode(PathDataNode n) {
            type = n.type;
            params = copyOfRange(n.params, 0, n.params.length);
        }

        /**
         * Convert an array of PathDataNode to Path.
         *
         * @param node The source array of PathDataNode.
         * @param path The target Path object.
         */
        public static void nodesToPath(PathDataNode[] node, Path path) {
            float[] current = new float[6];
            char previousCommand = 'm';
            for (int i = 0; i < node.length; i++) {
                addCommand(path, current, previousCommand, node[i].type, node[i].params);
                previousCommand = node[i].type;
            }
        }

        /**
         * The current PathDataNode will be interpolated between the
         * <code>nodeFrom</code> and <code>nodeTo</code> according to the
         * <code>fraction</code>.
         *
         * @param nodeFrom The start value as a PathDataNode.
         * @param nodeTo   The end value as a PathDataNode
         * @param fraction The fraction to interpolate.
         */
        public void interpolatePathDataNode(PathDataNode nodeFrom,
                                            PathDataNode nodeTo, float fraction) {
            for (int i = 0; i < nodeFrom.params.length; i++) {
                params[i] = nodeFrom.params[i] * (1 - fraction)
                        + nodeTo.params[i] * fraction;
            }
        }

        private static void addCommand(Path path, float[] current,
                                       char previousCmd, char cmd, float[] val) {

            int incr = 2;
            float currentX = current[0];
            float currentY = current[1];
            float ctrlPointX = current[2];
            float ctrlPointY = current[3];
            float currentSegmentStartX = current[4];
            float currentSegmentStartY = current[5];
            float reflectiveCtrlPointX;
            float reflectiveCtrlPointY;

            switch (cmd) {
                case 'z':
                case 'Z':
                    path.close();
                    // Path is closed here, but we need to move the pen to the
                    // closed position. So we cache the segment's starting position,
                    // and restore it here.
                    currentX = currentSegmentStartX;
                    currentY = currentSegmentStartY;
                    ctrlPointX = currentSegmentStartX;
                    ctrlPointY = currentSegmentStartY;
                    path.moveTo(currentX, currentY);
                    break;
                case 'm':
                case 'M':
                case 'l':
                case 'L':
                case 't':
                case 'T':
                    incr = 2;
                    break;
                case 'h':
                case 'H':
                case 'v':
                case 'V':
                    incr = 1;
                    break;
                case 'c':
                case 'C':
                    incr = 6;
                    break;
                case 's':
                case 'S':
                case 'q':
                case 'Q':
                    incr = 4;
                    break;
                case 'a':
                case 'A':
                    incr = 7;
                    break;
            }

            for (int k = 0; k < val.length; k += incr) {
                switch (cmd) {
                    case 'm': // moveto - Start a new sub-path (relative)
                        currentX += val[k + 0];
                        currentY += val[k + 1];
                        if (k > 0) {
                            // According to the spec, if a moveto is followed by multiple
                            // pairs of coordinates, the subsequent pairs are treated as
                            // implicit lineto commands.
                            path.rLineTo(val[k + 0], val[k + 1]);
                        } else {
                            path.rMoveTo(val[k + 0], val[k + 1]);
                            currentSegmentStartX = currentX;
                            currentSegmentStartY = currentY;
                        }
                        break;
                    case 'M': // moveto - Start a new sub-path
                        currentX = val[k + 0];
                        currentY = val[k + 1];
                        if (k > 0) {
                            // According to the spec, if a moveto is followed by multiple
                            // pairs of coordinates, the subsequent pairs are treated as
                            // implicit lineto commands.
                            path.lineTo(val[k + 0], val[k + 1]);
                        } else {
                            path.moveTo(val[k + 0], val[k + 1]);
                            currentSegmentStartX = currentX;
                            currentSegmentStartY = currentY;
                        }
                        break;
                    case 'l': // lineto - Draw a line from the current point (relative)
                        path.rLineTo(val[k + 0], val[k + 1]);
                        currentX += val[k + 0];
                        currentY += val[k + 1];
                        break;
                    case 'L': // lineto - Draw a line from the current point
                        path.lineTo(val[k + 0], val[k + 1]);
                        currentX = val[k + 0];
                        currentY = val[k + 1];
                        break;
                    case 'h': // horizontal lineto - Draws a horizontal line (relative)
                        path.rLineTo(val[k + 0], 0);
                        currentX += val[k + 0];
                        break;
                    case 'H': // horizontal lineto - Draws a horizontal line
                        path.lineTo(val[k + 0], currentY);
                        currentX = val[k + 0];
                        break;
                    case 'v': // vertical lineto - Draws a vertical line from the current point (r)
                        path.rLineTo(0, val[k + 0]);
                        currentY += val[k + 0];
                        break;
                    case 'V': // vertical lineto - Draws a vertical line from the current point
                        path.lineTo(currentX, val[k + 0]);
                        currentY = val[k + 0];
                        break;
                    case 'c': // curveto - Draws a cubic Bézier curve (relative)
                        path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
                                val[k + 4], val[k + 5]);

                        ctrlPointX = currentX + val[k + 2];
                        ctrlPointY = currentY + val[k + 3];
                        currentX += val[k + 4];
                        currentY += val[k + 5];

                        break;
                    case 'C': // curveto - Draws a cubic Bézier curve
                        path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
                                val[k + 4], val[k + 5]);
                        currentX = val[k + 4];
                        currentY = val[k + 5];
                        ctrlPointX = val[k + 2];
                        ctrlPointY = val[k + 3];
                        break;
                    case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
                        reflectiveCtrlPointX = 0;
                        reflectiveCtrlPointY = 0;
                        if (previousCmd == 'c' || previousCmd == 's'
                                || previousCmd == 'C' || previousCmd == 'S') {
                            reflectiveCtrlPointX = currentX - ctrlPointX;
                            reflectiveCtrlPointY = currentY - ctrlPointY;
                        }
                        path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
                                val[k + 0], val[k + 1],
                                val[k + 2], val[k + 3]);

                        ctrlPointX = currentX + val[k + 0];
                        ctrlPointY = currentY + val[k + 1];
                        currentX += val[k + 2];
                        currentY += val[k + 3];
                        break;
                    case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
                        reflectiveCtrlPointX = currentX;
                        reflectiveCtrlPointY = currentY;
                        if (previousCmd == 'c' || previousCmd == 's'
                                || previousCmd == 'C' || previousCmd == 'S') {
                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
                        }
                        path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
                                val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
                        ctrlPointX = val[k + 0];
                        ctrlPointY = val[k + 1];
                        currentX = val[k + 2];
                        currentY = val[k + 3];
                        break;
                    case 'q': // Draws a quadratic Bézier (relative)
                        path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
                        ctrlPointX = currentX + val[k + 0];
                        ctrlPointY = currentY + val[k + 1];
                        currentX += val[k + 2];
                        currentY += val[k + 3];
                        break;
                    case 'Q': // Draws a quadratic Bézier
                        path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
                        ctrlPointX = val[k + 0];
                        ctrlPointY = val[k + 1];
                        currentX = val[k + 2];
                        currentY = val[k + 3];
                        break;
                    case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
                        reflectiveCtrlPointX = 0;
                        reflectiveCtrlPointY = 0;
                        if (previousCmd == 'q' || previousCmd == 't'
                                || previousCmd == 'Q' || previousCmd == 'T') {
                            reflectiveCtrlPointX = currentX - ctrlPointX;
                            reflectiveCtrlPointY = currentY - ctrlPointY;
                        }
                        path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
                                val[k + 0], val[k + 1]);
                        ctrlPointX = currentX + reflectiveCtrlPointX;
                        ctrlPointY = currentY + reflectiveCtrlPointY;
                        currentX += val[k + 0];
                        currentY += val[k + 1];
                        break;
                    case 'T': // Draws a quadratic Bézier curve (reflective control point)
                        reflectiveCtrlPointX = currentX;
                        reflectiveCtrlPointY = currentY;
                        if (previousCmd == 'q' || previousCmd == 't'
                                || previousCmd == 'Q' || previousCmd == 'T') {
                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
                        }
                        path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
                                val[k + 0], val[k + 1]);
                        ctrlPointX = reflectiveCtrlPointX;
                        ctrlPointY = reflectiveCtrlPointY;
                        currentX = val[k + 0];
                        currentY = val[k + 1];
                        break;
                    case 'a': // Draws an elliptical arc
                        // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
                        drawArc(path,
                                currentX,
                                currentY,
                                val[k + 5] + currentX,
                                val[k + 6] + currentY,
                                val[k + 0],
                                val[k + 1],
                                val[k + 2],
                                val[k + 3] != 0,
                                val[k + 4] != 0);
                        currentX += val[k + 5];
                        currentY += val[k + 6];
                        ctrlPointX = currentX;
                        ctrlPointY = currentY;
                        break;
                    case 'A': // Draws an elliptical arc
                        drawArc(path,
                                currentX,
                                currentY,
                                val[k + 5],
                                val[k + 6],
                                val[k + 0],
                                val[k + 1],
                                val[k + 2],
                                val[k + 3] != 0,
                                val[k + 4] != 0);
                        currentX = val[k + 5];
                        currentY = val[k + 6];
                        ctrlPointX = currentX;
                        ctrlPointY = currentY;
                        break;
                }
                previousCmd = cmd;
            }
            current[0] = currentX;
            current[1] = currentY;
            current[2] = ctrlPointX;
            current[3] = ctrlPointY;
            current[4] = currentSegmentStartX;
            current[5] = currentSegmentStartY;
        }

        private static void drawArc(Path p,
                                    float x0,
                                    float y0,
                                    float x1,
                                    float y1,
                                    float a,
                                    float b,
                                    float theta,
                                    boolean isMoreThanHalf,
                                    boolean isPositiveArc) {

            /* Convert rotation angle from degrees to radians */
            double thetaD = Math.toRadians(theta);
            /* Pre-compute rotation matrix entries */
            double cosTheta = Math.cos(thetaD);
            double sinTheta = Math.sin(thetaD);
            /* Transform (x0, y0) and (x1, y1) into unit space */
            /* using (inverse) rotation, followed by (inverse) scale */
            double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
            double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
            double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
            double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;

            /* Compute differences and averages */
            double dx = x0p - x1p;
            double dy = y0p - y1p;
            double xm = (x0p + x1p) / 2;
            double ym = (y0p + y1p) / 2;
            /* Solve for intersecting unit circles */
            double dsq = dx * dx + dy * dy;
            if (dsq == 0.0) {
                Log.w(LOG_TAG, " Points are coincident");
                return; /* Points are coincident */
            }
            double disc = 1.0 / dsq - 1.0 / 4.0;
            if (disc < 0.0) {
                Log.w(LOG_TAG, "Points are too far apart " + dsq);
                float adjust = (float) (Math.sqrt(dsq) / 1.99999);
                drawArc(p, x0, y0, x1, y1, a * adjust,
                        b * adjust, theta, isMoreThanHalf, isPositiveArc);
                return; /* Points are too far apart */
            }
            double s = Math.sqrt(disc);
            double sdx = s * dx;
            double sdy = s * dy;
            double cx;
            double cy;
            if (isMoreThanHalf == isPositiveArc) {
                cx = xm - sdy;
                cy = ym + sdx;
            } else {
                cx = xm + sdy;
                cy = ym - sdx;
            }

            double eta0 = Math.atan2((y0p - cy), (x0p - cx));

            double eta1 = Math.atan2((y1p - cy), (x1p - cx));

            double sweep = (eta1 - eta0);
            if (isPositiveArc != (sweep >= 0)) {
                if (sweep > 0) {
                    sweep -= 2 * Math.PI;
                } else {
                    sweep += 2 * Math.PI;
                }
            }

            cx *= a;
            cy *= b;
            double tcx = cx;
            cx = cx * cosTheta - cy * sinTheta;
            cy = tcx * sinTheta + cy * cosTheta;

            arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
        }

        /**
         * Converts an arc to cubic Bezier segments and records them in p.
         *
         * @param p     The target for the cubic Bezier segments
         * @param cx    The x coordinate center of the ellipse
         * @param cy    The y coordinate center of the ellipse
         * @param a     The radius of the ellipse in the horizontal direction
         * @param b     The radius of the ellipse in the vertical direction
         * @param e1x   E(eta1) x coordinate of the starting point of the arc
         * @param e1y   E(eta2) y coordinate of the starting point of the arc
         * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
         * @param start The start angle of the arc on the ellipse
         * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
         */
        private static void arcToBezier(Path p,
                                        double cx,
                                        double cy,
                                        double a,
                                        double b,
                                        double e1x,
                                        double e1y,
                                        double theta,
                                        double start,
                                        double sweep) {
            // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
            // and http://www.spaceroots.org/documents/ellipse/node22.html

            // Maximum of 45 degrees per cubic Bezier segment
            int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));

            double eta1 = start;
            double cosTheta = Math.cos(theta);
            double sinTheta = Math.sin(theta);
            double cosEta1 = Math.cos(eta1);
            double sinEta1 = Math.sin(eta1);
            double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
            double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);

            double anglePerSegment = sweep / numSegments;
            for (int i = 0; i < numSegments; i++) {
                double eta2 = eta1 + anglePerSegment;
                double sinEta2 = Math.sin(eta2);
                double cosEta2 = Math.cos(eta2);
                double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
                double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
                double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
                double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
                double tanDiff2 = Math.tan((eta2 - eta1) / 2);
                double alpha =
                        Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
                double q1x = e1x + alpha * ep1x;
                double q1y = e1y + alpha * ep1y;
                double q2x = e2x - alpha * ep2x;
                double q2y = e2y - alpha * ep2y;

                p.cubicTo((float) q1x,
                        (float) q1y,
                        (float) q2x,
                        (float) q2y,
                        (float) e2x,
                        (float) e2y);
                eta1 = eta2;
                e1x = e2x;
                e1y = e2y;
                ep1x = ep2x;
                ep1y = ep2y;
            }
        }
    }
}

SVG下載地址1
SVG下載地址2
SVG下載地址3
圖片轉成SVG
SVG圖片編輯
SVG轉VectorDrawable
SVG相關教程
相關類型圖片對比
地圖相關SVG地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章