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地址