系列文章
Android高級UI進(jìn)階之路(一) —— View的基礎(chǔ)知識
Android高級UI進(jìn)階之路(二) —— 深入理解Android8.0 View的觸摸事件分發(fā)機(jī)制
Android高級UI進(jìn)階之路(三) —— 理解View的工作原理及自定義View入門
Android高級UI進(jìn)階之路(四) —— Paint渲染濾鏡xfermode使用
Android高級UI進(jìn)階之路(五) —— Canvas詳解
Android高級UI進(jìn)階之路(六) —— PathMeasure-制作路徑動(dòng)畫
Android高級UI進(jìn)階之路(七) —— SVG基礎(chǔ)使用(繪制中國地圖)
前言
前面陸陸續(xù)續(xù)寫了幾篇 高級 UI 系列文章 ,感覺還不錯(cuò)。因?yàn)楣ぷ鲀?nèi)容原因作者對 UI 開發(fā)涉及的很少,所以打算寫一點(diǎn)關(guān)于 UI 的文章,也算是給自己一個(gè)全面的復(fù)習(xí)。本篇文章還是 基本概念 + 實(shí)戰(zhàn)來講解。
概念
SVG 的全稱是 (Scalable Vector Graphics) 它是一個(gè)可縮放的矢量圖形,是專門用于網(wǎng)絡(luò)的矢量圖標(biāo)準(zhǔn),與矢量圖相對應(yīng)的是位圖,Bitmap 就是位圖,它由一個(gè)個(gè)像素點(diǎn)組成,當(dāng)圖片放大到一定大小時(shí), 就會(huì)出現(xiàn)馬賽克現(xiàn)象,Photoshop 就是常用的位圖處理軟件,而矢量圖則由一個(gè)個(gè)點(diǎn)組成,經(jīng)過數(shù)學(xué)計(jì)算利用直線和曲線繪制而成,無論如何放大,都不會(huì)出現(xiàn)馬賽克問題,illustrator 就是常用的矢量圖繪圖軟件。
SVG VS Bitmap
好處:
SVG 使用 XML 格式定義圖形,,可被非常用的多的工具讀取和修改;
SVG 由點(diǎn)來存儲(chǔ),由計(jì)算機(jī)根據(jù)點(diǎn)信息繪圖,不會(huì)失真,無須根據(jù)分辨率適配多套圖標(biāo);
SVG 的占用空間比 Bitmap 小,比如一張 500px * 500px 的圖像,轉(zhuǎn)成 SVG 后占用的空間大小是 20KB, 而 PNG 圖片則需要 732KB 的空間。
SVG 可以轉(zhuǎn)換 Path 路徑,與 Path 動(dòng)畫相結(jié)合,可以形成更豐富的動(dòng)畫。
vector 標(biāo)簽
在 Android 中, SVG 矢量圖是使用標(biāo)簽定義的,并存放在 res/drawable/ 目錄下。一段簡單的 SVG 圖像代碼定義如下:
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
android:height="24dp"
android:viewportHeight="1024"
android:viewportWidth="1024"
android:width="24dp"
tools:ignore="MissingDefaultResource">
<path android:fillColor="#040000"
android:pathData="M513.29,738h-2.3V0h2.3z"/>
<path android:fillColor="#040000"
android:pathData="M512.08,727.97S482.38,896.04 480.09,939.08c-0.76,14.31 -9.58,84.92 32.88,84.92"/>
<path android:fillColor="#040000"
android:pathData="M511.02,1024c42.47,0 33.66,-70.6 32.89,-84.92 -2.3,-43.04 -31.99,-211.11 -31.99,-211.11"/>
</vector>
它定義的圖像如下所示:

上面水滴形狀就是呈現(xiàn)出來的對應(yīng)的圖像,在這段代碼中,首先使用 vector 標(biāo)簽來指定這是一幅 SVG 圖像,而它有下面幾個(gè)屬性。
width/height : 表示該 SVG 寬高
viewportHeight/viewportWidth: 表示 SVG 圖形劃分的比例
path 標(biāo)簽
常用屬性
標(biāo)簽名稱 |
說明 |
android:name |
聲明一個(gè)標(biāo)記,類似于 ID ,便于對其做動(dòng)畫的時(shí)候順利地找到該節(jié)點(diǎn) |
android:pathData |
對 SVG 矢量圖的描述 |
android:strokeWidth |
畫筆的寬度 |
android:fillColor |
填充顏色 |
android:fillAlpha |
填充顏色的透明度 |
android:strokeColor |
描邊顏色 |
android:strokeWidth |
描邊寬度 |
android:strokeAlpha |
描邊透明度 |
android:strokeLineJoin |
用于指定折線拐角形狀,取值有 miter (結(jié)合處為銳角)、round(結(jié)合處為圓弧)、bevel(結(jié)合處為直線) |
android:strokeLineCap |
畫出線條的終點(diǎn)的形狀(線帽),取值有 butt(無限帽) 、round (圓形線帽)、square(方形線帽) |
android:strokeMiterLimit |
設(shè)置斜角的上限 |
android:trimPathStart 屬性
該屬性用于指定路徑從哪里開始,取值 0 ~ 1,表示路徑開始位置的百分比。當(dāng)取值為 0 時(shí),表示從頭部開始;當(dāng)取值為 1 時(shí),整條路徑不可見。
android:trimPathEnd 屬性
該屬性用于指定路徑的結(jié)束位置,取值為 0 ~ 1 ,表示路徑結(jié)束位置的百分比。當(dāng)取值為 1 時(shí),路徑正常結(jié)束;當(dāng)取值為 0 時(shí),表示從頭開始位置就已經(jīng)結(jié)束了,整條路徑不可見。
android:trimPathOffset 屬性
該屬性用于指定結(jié)果路徑的位移距離,取值為 0 ~ 1 。當(dāng)取值為 0 時(shí),不進(jìn)行位移;當(dāng)取值為 1 時(shí),位移整條路徑的長度。
android:pathData 屬性
在 path 標(biāo)簽中,主要通過 pathData 屬性來指定 SVG 圖像的顯示內(nèi)容。而 pathData 屬性初 M 和 L 指令以外,還有更多的指定。
指令 |
對應(yīng) |
說明 |
M |
moveto(M x,y) |
將畫筆移動(dòng)到指定的地方 |
L |
lineto(L X,Y) |
畫直線到指定的坐標(biāo)位置 |
H |
Horizontal lineto(H X) |
畫水平線到指定的 X 坐標(biāo)位置 |
V |
Vertical lineto(V Y) |
畫垂直線到指定的 Y 坐標(biāo)位置 |
C |
curveto(C X1,Y1,X2,Y2,ENDX,ENDY) |
三階貝濟(jì)埃曲線 |
S |
Smooth curveto(S X2,Y2,ENDX,ENDY) |
三階貝濟(jì)埃曲線 |
Q |
Quadratic Belzier curve(Q X,Y,ENDX,ENDY) |
二階貝濟(jì)埃曲線 |
T |
smooth quadratic Belaizer curveto(T ENDX,ENDY) |
映射前面路徑后的終點(diǎn) |
A |
elliptic Arc(A RX,RY,XROTATION,FLAYG1,FLAY2,X,Y) |
弧線 |
Z |
Closepath |
關(guān)閉路徑 |
制作 SVG 圖像
方法一: 設(shè)計(jì)軟件
如有你有繪圖基礎(chǔ),則可以使用 Illustrator 或在線 SVG 工具制作 SVG 圖像,比如:editor.method.ac/ ,或通過 SVG 源文件下載網(wǎng)站下載后進(jìn)行編輯。
方法二: Iconfont
阿里巴巴的矢量圖庫

Android 中引入 SVG 圖像
準(zhǔn)備工作
我們知道在 Android 中是不支持直接使用 SVG 圖像解析的,我們必須將 SVG圖像轉(zhuǎn)換為 vector 標(biāo)簽描述,這里有 2 種方法;
方法一: 在線轉(zhuǎn)換
點(diǎn)擊跳轉(zhuǎn)在線轉(zhuǎn)換網(wǎng)站

方法二: AS 轉(zhuǎn)

按照我上面的步驟,就可以生成 Vector 圖像了
基礎(chǔ)使用
下面對 ImageView 怎么直接使用 vector 進(jìn)行說明(ps:這里用的 androidx 版本,如果是低版本需要自己去做兼容);
在 ImageView 中使用
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" tools:ignore="MissingDefaultResource">
<ImageView
android:id="@+id/iv"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:src="@drawable/ic_line"
android:layout_height="500dp"/>
</RelativeLayout>

進(jìn)階使用
前面講解了 vector 標(biāo)簽,靜態(tài)顯示 vector 和制作 SVG 圖像的方法,那么該小節(jié)就講解動(dòng)態(tài)的 vector, 動(dòng)態(tài)的 vector 所實(shí)現(xiàn)的效果才是 SVG 圖像在 Android 應(yīng)用中的精髓。
要實(shí)現(xiàn) Vector 動(dòng)畫,首先需要 Vector 圖像和它所對應(yīng)的動(dòng)畫,這里依然使用上一小節(jié)水滴狀態(tài)的圖像,
先來看一下效果:

給 path 定義 name,如下所示

定義一個(gè) Animator 文件,以表示對這幅 Vector 圖像做動(dòng)畫
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="trimPathStart"
android:valueFrom="1"
android:valueTo="0"
android:duration="3000"
>
</objectAnimator>
需要注意的是,這里的文件是對應(yīng) Vector 中 path 標(biāo)簽的,這里動(dòng)畫效果是動(dòng)態(tài)改變 path 標(biāo)簽的 trimPathStart 屬性值,從 0 ~ 1 。
定義 animated-vector 進(jìn)行關(guān)聯(lián)
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:drawable="@drawable/ic_line"
tools:targetApi="lollipop">
<target android:animation="@anim/anim_start"
android:name="num_1"></target>
<target android:animation="@anim/anim_start"
android:name="num_2"></target>
<target android:animation="@anim/anim_start"
android:name="num_3"></target>
</animated-vector>
在上述代碼中,drawable 代表關(guān)聯(lián)的 vector 圖像,target 代表將 path name 和動(dòng)畫進(jìn)行關(guān)聯(lián)
代碼中進(jìn)行設(shè)置
class SVGDemo1Activity : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_svg)
startAnimatabe()
}
private fun startAnimatabe() {
val animatedVectorDrawable = AnimatedVectorDrawableCompat.create(this, R.drawable.line_animated_vector)
iv.setImageDrawable(animatedVectorDrawable)
val animatable = iv.drawable as Animatable
animatable.start()
}
}
實(shí)戰(zhàn)
輸入搜索動(dòng)畫
利用在線繪制 SVG 圖標(biāo)網(wǎng)站 制作搜索圖標(biāo)
可以自己隨意搗鼓繪制,繪制好了之后點(diǎn)擊視圖->源代碼,將 SVG 代碼復(fù)制出來保存成 search_svg.xml
在線轉(zhuǎn)換 svg2vector
點(diǎn)擊空白或者直接將 SVG 拖拽指定區(qū)域進(jìn)行轉(zhuǎn)換
將轉(zhuǎn)換好的 Android 格式的 vector 導(dǎo)入 AS

開始制作動(dòng)畫關(guān)聯(lián)
//1.在 /res/aniamator 文件夾下 定義動(dòng)畫
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="trimPathStart"
android:valueFrom="1"
android:valueTo="0"
android:duration="2000"
>
</objectAnimator>
//2\. 在/res/drawable/ 定義 vector
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="580dp"
android:height="400dp"
android:viewportWidth="580"
android:viewportHeight="400">
<path
android:name="svg_1"
android:strokeColor="#000"
android:strokeWidth="1.5"
android:pathData="M 164.54545 211.91761 L 380 212.8267" />
<path
android:name="svg_2"
android:strokeColor="#000"
android:strokeWidth="1.5"
android:pathData="M 360 180.09943 C 366.024924042 180.09943 370.90909 184.780091469 370.90909 190.55398 C 370.90909 196.327868531 366.024924042 201.00853 360 201.00853 C 353.975075958 201.00853 349.09091 196.327868531 349.09091 190.55398 C 349.09091 184.780091469 353.975075958 180.09943 360 180.09943 Z" />
<path
android:name="svg_3"
android:strokeColor="#000"
android:strokeWidth="1.5"
android:pathData="M 369.09091 197.37216 L 380.90909 208.28125" />
</vector>
//3\. 在/res/drawable/ 關(guān)聯(lián)動(dòng)畫和 vector
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:drawable="@drawable/search_svg"
tools:targetApi="lollipop">
<target android:animation="@animator/anim_start"
android:name="svg_1"></target>
<target android:animation="@animator/anim_start"
android:name="svg_2"></target>
<target android:animation="@animator/anim_start"
android:name="svg_3"></target>
</animated-vector>
效果

警車燈閃爍

今日頭條下拉刷新動(dòng)畫
來一個(gè)復(fù)雜組合動(dòng)畫,請看下面效果圖:

準(zhǔn)備 vector 數(shù)據(jù)
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportHeight="200"
android:viewportWidth="200">
<path
android:name="tt_1"
android:fillColor="#C2BFBF"
android:pathData="
M20,30
L100,30
M100,30
L100,90
M100,90
L20,90
M20,90
L20,30"
android:strokeColor="#C2BFBF"
android:strokeLineCap="round"
android:strokeWidth="6"/>
<path
android:name="tt_2"
android:pathData="
M120,30
L180,30
M120,60
L180,60
M120,90
L180,90"
android:strokeColor="#C2BFBF"
android:strokeLineCap="round"
android:strokeWidth="6"/>
<path
android:name="tt_3"
android:pathData="
M20,120
L180,120
M20,150
L180,150
M20,180
L180,180"
android:strokeColor="#C2BFBF"
android:strokeLineCap="round"
android:strokeWidth="6"/>
<path
android:pathData="
M0,0
L200,0
M200,0
L200,200
M200,200
L0,200
M0,200
L0,0"
android:strokeColor="#C2BFBF"
android:strokeLineCap="round"
android:strokeWidth="6"/>
</vector>
定義順時(shí)針執(zhí)行動(dòng)畫并做 pathData 變換
這里拿其中一個(gè)位置變化來舉例說明:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially">//按順序執(zhí)行
//依次執(zhí)行 pathData 位置變換
<objectAnimator
android:duration="600"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="pathData"
android:valueFrom="
M20,30
L100,30
M100,30
L100,90
M100,90
L20,90
M20,90
L20,30"
android:valueTo="
M100,30
L180,30
M180,30
L180,90
M180,90
L100,90
M100,90
L100,30"
android:valueType="pathType" />
<objectAnimator
android:duration="600"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="pathData"
android:valueFrom="
M100,30
L180,30
M180,30
L180,90
M180,90
L100,90
M100,90
L100,30"
android:valueTo="
M100,120
L180,120
M180,120
L180,180
M180,180
L100,180
M100,180
L100,120"
android:valueType="pathType" />
<objectAnimator
android:duration="600"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="pathData"
android:valueFrom="
M100,120
L180,120
M180,120
L180,180
M180,180
L100,180
M100,180
L100,120"
android:valueTo="
M20,120
L100,120
M100,120
L100,180
M100,180
L20,180
M20,180
L20,120"
android:valueType="pathType" />
<objectAnimator
android:duration="600"
android:interpolator="@android:interpolator/decelerate_cubic"
android:propertyName="pathData"
android:valueFrom="
M20,120
L100,120
M100,120
L100,180
M100,180
L20,180
M20,180
L20,120"
android:valueTo="
M20,30
L100,30
M100,30
L100,90
M100,90
L20,90
M20,90
L20,30"
android:valueType="pathType" />
</set>
如果對標(biāo)簽中的定義還不了解的先去看下文章中 path 標(biāo)簽 中的說明。如果不理解標(biāo)簽意思,根本就看不懂。
進(jìn)行關(guān)聯(lián)
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:drawable="@drawable/ic_toutiao"
tools:targetApi="lollipop">
<target
android:animation="@animator/tt_path_one"
android:name="tt_1"/>
<target
android:animation="@animator/tt_path_two"
android:name="tt_2"/>
<target
android:animation="@animator/tt_path_three"
android:name="tt_3"/>
</animated-vector>
代碼控制重復(fù)執(zhí)行
class SVGDemo1Activity : AppCompatActivity() {
var reStartTT = @SuppressLint("HandlerLeak")
object : Handler() {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
startAnimatabe(R.drawable.line_animated_toutiao, true)
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_svg)
//水滴動(dòng)畫
startWaterDropAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_vector, false)
}
//搜索動(dòng)畫
startSearchAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_search, false)
}
//執(zhí)行警車動(dòng)畫
startPoliceCarAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_car, false)
}
//執(zhí)行頭條動(dòng)畫
startTTAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_toutiao, true)
}
}
private fun startAnimatabe(lineAnimatedVector: Int, isRegister: Boolean): Animatable {
val animatedVectorDrawable = AnimatedVectorDrawableCompat.create(this, lineAnimatedVector)
iv.setImageDrawable(animatedVectorDrawable)
val animatable = iv.drawable as Animatable
animatable.start()
animatedVectorDrawable!!.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
super.onAnimationEnd(drawable)
if (!isRegister) return
animatedVectorDrawable.unregisterAnimationCallback(this)
//重新開始在 xml 設(shè)置 restart 無效暫時(shí)用 Handler 實(shí)現(xiàn)了。
reStartTT.sendEmptyMessage(0)
}
})
return animatable
}
}
繪制中國地圖
該篇之前實(shí)現(xiàn) SVG pathData 都是利用 ImageView 來實(shí)現(xiàn),并不是所有的場合都適合上面的方式,比如我想要實(shí)現(xiàn) pathData 區(qū)域點(diǎn)擊,那么上面所講的方式應(yīng)該是不能實(shí)現(xiàn),下面我們以一個(gè)實(shí)例來看怎么自定義 View 實(shí)現(xiàn) PathData 和 pathData 區(qū)域點(diǎn)擊事件。
下面我們利用 path 來繪制一個(gè)中國地圖,先來看一個(gè)最終效果圖,如下:
看起來是不是很炫,還不錯(cuò),嘿嘿,下面我們就來看一下如何實(shí)現(xiàn)。
準(zhǔn)備地圖 SVG
首先去下載地圖數(shù)據(jù)
選擇下載免費(fèi)的地圖數(shù)據(jù)

* 找到對應(yīng)的國家點(diǎn)擊下載 svg 數(shù)據(jù)

* 選擇對應(yīng)的地圖數(shù)據(jù),我這里下載的是高質(zhì)量的 SVG
SVG to Vector xml
將下載好的 china.svg 格式的文件轉(zhuǎn)為 vector 節(jié)點(diǎn)的 xml 數(shù)據(jù) 或者用 AS 自帶轉(zhuǎn)也行,看個(gè)人愛好。

轉(zhuǎn)好之后放入 AS 中,如下所示

現(xiàn)在有了這些數(shù)據(jù),我們就可以解析 xml path 節(jié)點(diǎn),拿到 pathData 數(shù)據(jù)我們不就可以繪制 path 了嘛。下面就開始解析 xml ,解析的方法很多種,我們這里用 dom 解析。
開始解析 xml
解析 xml 有很多種方式,這里就直接使用 DOM 解析,pathData2Path 我這里直接用 Android SDK 提供的 android.support.v4.graphics#PathParser 由于源碼中它被標(biāo)注了 hide 屬性 ,我們需要直接將它 copy 到我們自己項(xiàng)目中, 具體轉(zhuǎn)化請看如下代碼:
/**
* 開始解析 xml
*/
public fun dom2xml(stream: InputStream?): MutableList<MapData> {
mapDataLists.clear()
//dom
val newInstance = DocumentBuilderFactory.newInstance()
val newDocumentBuilder = newInstance.newDocumentBuilder()
//拿到 Docment 對象
val document = newDocumentBuilder.parse(stream)
//獲取 xml 中屬于 path 節(jié)點(diǎn)的所有信息
val elementsByTagName = document.getElementsByTagName(PATH_TAG)
//定義四個(gè)點(diǎn),確定整個(gè) map 的范圍
var left = -1f
var right = -1f
var top = -1f
var bottom = -1f
//開始遍歷標(biāo)簽,拿到 path 數(shù)據(jù)組
for (pathData in 0 until elementsByTagName.length) {
val item = elementsByTagName.item(pathData) as Element
val name = item.getAttribute("android:name")
val fillColor = item.getAttribute("android:fillColor")
val strokeColor = item.getAttribute("android:strokeColor")
val strokeWidth = item.getAttribute("android:strokeWidth")
val pathData = item.getAttribute("android:pathData")
val path = PathParser.createPathFromPathData(pathData)
mapDataLists.add(MapData(name, fillColor, strokeColor, strokeWidth, path))
//獲取控件的寬高
val rect = RectF()
//獲取到每個(gè)省份的邊界
path.computeBounds(rect, true)
//遍歷取出每個(gè)path中的left取所有的最小值
left = if (left == -1f) rect.left else Math.min(left, rect.left)
//遍歷取出每個(gè)path中的right取所有的最大值
right = if (right == -1f) rect.right else Math.max(right, rect.right)
//遍歷取出每個(gè)path中的top取所有的最小值
top = if (top == -1f) rect.top else Math.min(top, rect.top)
//遍歷取出每個(gè)path中的bottom取所有的最大值
bottom = if (bottom == -1f) rect.bottom else Math.max(bottom, rect.bottom)
}
//MAP 的矩形區(qū)域
MAP_RECTF = RectF(left, top, right, bottom)
return mapDataLists;
}
進(jìn)行控件測量適配橫豎屏切換和寬高定義 wrap_content 模式
/**
* 開始測量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//測量模式
var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(heightMeasureSpec)
//測量大小
widthSize = MeasureSpec.getSize(widthMeasureSpec)
heightSize = MeasureSpec.getSize(heightMeasureSpec)
if (!MAP_RECTF.isEmpty && mMapRectHeight != 0f && mMapRectWidth != 0f) {
//顯示比例
scaleHeightValues = heightSize / mMapRectHeight
scaleWidthValues = widthSize / mMapRectWidth
}
//xml 文件中寬高 wrap_content
if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST) {
//如果是橫屏寬保留最大,高需要適配
if (widthSize < heightSize && mMapRectHeight != 0f) {
setMeasuredDimension(widthSize, (mMapRectHeight * scaleWidthValues).toInt())
} else {
setMeasuredDimension(widthSize, heightSize)
}
} else {
setMeasuredDimension(widthSize, heightSize)
}
}
開始繪制 path
/**
* 繪制 Map 數(shù)據(jù)
*/
@SuppressLint("Range")
private fun drawMap(canvas: Canvas) {
canvas.save()
if (widthSize > heightSize) {
canvas.scale(scaleWidthValues, scaleHeightValues)
} else {
canvas.scale(scaleWidthValues, scaleWidthValues)
}
mapDataList.forEach { data ->
run {
if (data.isSelect) {
drawPath(data, canvas, Color.RED)
} else {
drawPath(data, canvas, Color.parseColor(data.fillColor))
}
}
}
canvas.restore()
canvas.drawText("中國????地圖", widthSize / 2 - mPaintTextTitle.measureText("中國????地圖") / 2f, 100f, mPaintTextTitle)
}
/**
* 開始繪制 Path
*/
private fun drawPath(
data: MapData,
canvas: Canvas,
magenta: Int
) {
mPaintPath.setColor(magenta)
mPaintPath.setStyle(Paint.Style.FILL)
mPaintPath.setTextSize(30f)
mPaintPath.setStrokeWidth(data.strokeWidth.toFloat())
canvas.drawPath(data.pathData, mPaintPath)
val rectF = RectF()
data.pathData.computeBounds(rectF, true)
canvas.drawText(
if (data.name.isEmpty()) "" else data.name,
rectF.centerX() - mPaintText.measureText(data.name) / 2,
rectF.centerY(), mPaintText
)
}
給地圖添加各自的點(diǎn)擊事件
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> return true
MotionEvent.ACTION_UP -> {
handlerTouch(event.getX(), event.getY())
}
}
return super.onTouchEvent(event)
}
/**
* 處理點(diǎn)擊事件
*/
private fun handlerTouch(x: Float, y: Float) {
if (mapDataList.size == 0) return
var xScale = 0f
var yScale = 0f
if (widthSize > heightSize) {
xScale = scaleWidthValues
yScale = scaleHeightValues
} else {
xScale = scaleWidthValues
yScale = scaleWidthValues
}
mapDataList.forEach { data ->
run {
data.isSelect = false
if (isTouchRegion(x / xScale, y / yScale, data.pathData)) {
data.isSelect = true
postInvalidate()
}
}
}
}
}
/**
* 判斷是否在點(diǎn)擊區(qū)域內(nèi)
*/
fun isTouchRegion(x: Float, y: Float, path: Path): Boolean {
//創(chuàng)建一個(gè)矩形
val rectF = RectF()
//獲取到當(dāng)前省份的矩形邊界
path.computeBounds(rectF, true)
//創(chuàng)建一個(gè)區(qū)域?qū)ο? val region = Region()
//將path對象放入到Region區(qū)域?qū)ο笾? region.setPath(path, Region(rectF.left.toInt(), rectF.top.toInt(), rectF.right.toInt(), rectF.bottom.toInt()))
//返回是否這個(gè)區(qū)域包含傳進(jìn)來的坐標(biāo)
return region.contains(x.toInt(), y.toInt())
}
到這里 SVG 知識已經(jīng)講解完了,覺得還不過癮的可以自己嘗試一下其他國家的地圖繪制。
總結(jié)
這里一定要注意在低版本上使用 SVG 存在兼容問題,需要各自查閱資料解決。文章來源:http://www.zghlxwxcb.cn/news/detail-753729.html
不知道還有沒有記得上一篇 [高級 UI 成長之路 (六) PathMeasure 制作路徑動(dòng)畫] 中我提到了只要給我一個(gè) Path 數(shù)據(jù),我就能繪制出圖形,看完該篇是不是認(rèn)為說的沒毛病吧。建議大家在項(xiàng)目上多使用 SVG ,好處文章開頭也提到了,這里就不在啰嗦了。到這里 SVG 制作圖像和動(dòng)畫效果就全部講完了。文章來源地址http://www.zghlxwxcb.cn/news/detail-753729.html
到了這里,關(guān)于Android高級UI進(jìn)階之路(七)——SVG基礎(chǔ)使用(繪制中國地圖)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!