前面陸陸續續寫了幾篇 高級 UI 系列文章
,感受還不錯。由於工做內容緣由做者對 UI 開發涉及的不多,因此打算寫一點關於 UI 的文章,也算是給本身一個全面的複習。本篇文章仍是 基本概念 + 實戰來說解。php
SVG 的全稱是 (Scalable Vector Graphics) 它是一個可縮放的矢量圖形,是專門用於網絡的矢量圖標準,與矢量圖相對應的是位圖,Bitmap 就是位圖,它由一個個像素點組成,當圖片放大到必定大小時, 就會出現馬賽克現象,Photoshop 就是經常使用的位圖處理軟件,而矢量圖則由一個個點組成,通過數學計算利用直線和曲線繪製而成,不管如何放大,都不會出現馬賽克問題,illustrator
就是經常使用的矢量圖繪圖軟件。java
好處:android
在 Android 中, SVG 矢量圖是使用標籤訂義的,並存放在 res/drawable/ 目錄下。一段簡單的 SVG 圖像代碼定義以下:git
<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>
複製代碼
它定義的圖像以下所示:github
上面水滴形狀就是呈現出來的對應的圖像,在這段代碼中,首先使用 vector 標籤來指定這是一幅 SVG 圖像,而它有下面幾個屬性。canvas
經常使用屬性網絡
標籤名稱 | 說明 |
---|---|
android:name | 聲明一個標記,相似於 ID ,便於對其作動畫的時候順利地找到該節點 |
android:pathData | 對 SVG 矢量圖的描述 |
android:strokeWidth | 畫筆的寬度 |
android:fillColor | 填充顏色 |
android:fillAlpha | 填充顏色的透明度 |
android:strokeColor | 描邊顏色 |
android:strokeWidth | 描邊寬度 |
android:strokeAlpha | 描邊透明度 |
android:strokeLineJoin | 用於指定折線拐角形狀,取值有 miter (結合處爲銳角)、round(結合處爲圓弧)、bevel(結合處爲直線) |
android:strokeLineCap | 畫出線條的終點的形狀(線帽),取值有 butt(無限帽) 、round (圓形線帽)、square(方形線帽) |
android:strokeMiterLimit | 設置斜角的上限 |
android:trimPathStart 屬性app
該屬性用於指定路徑從哪裏開始,取值 0 ~ 1,表示路徑開始位置的百分比。當取值爲 0 時,表示從頭部開始;當取值爲 1 時,整條路徑不可見。dom
android:trimPathEnd 屬性ide
該屬性用於指定路徑的結束位置,取值爲 0 ~ 1 ,表示路徑結束位置的百分比。當取值爲 1 時,路徑正常結束;當取值爲 0 時,表示從頭開始位置就已經結束了,整條路徑不可見。
android:trimPathOffset 屬性
該屬性用於指定結果路徑的位移距離,取值爲 0 ~ 1 。當取值爲 0 時,不進行位移;當取值爲 1 時,位移整條路徑的長度。
android:pathData 屬性
在 path 標籤中,主要經過 pathData 屬性來指定 SVG 圖像的顯示內容。而 pathData 屬性初 M 和 L 指令之外,還有更多的指定。
指令 | 對應 | 說明 |
---|---|---|
M | moveto(M x,y) | 將畫筆移動到指定的地方 |
L | lineto(L X,Y) | 畫直線到指定的座標位置 |
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 Belaizer curveto(T ENDX,ENDY) | 映射前面路徑後的終點 |
A | elliptic Arc(A RX,RY,XROTATION,FLAYG1,FLAY2,X,Y) | 弧線 |
Z | Closepath | 關閉路徑 |
方法一: 設計軟件
若有你有繪圖基礎,則可使用 Illustrator 或在線 SVG 工具製做 SVG 圖像,好比:editor.method.ac/ ,或經過 SVG 源文件下載網站下載後進行編輯。
方法二: Iconfont
咱們知道在 Android 中是不支持直接使用 SVG 圖像解析的,咱們必須將 SVG圖像轉換爲 vector 標籤描述,這裏有 2 種方法;
方法一: 在線轉換
方法二: AS 轉
按照我上面的步驟,就能夠生成 Vector 圖像了
下面對 ImageView 怎麼直接使用 vector 進行說明(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>
複製代碼
前面講解了 vector 標籤,靜態顯示 vector 和製做 SVG 圖像的方法,那麼該小節就講解動態的 vector, 動態的 vector 所實現的效果纔是 SVG 圖像在 Android 應用中的精髓。
要實現 Vector 動畫,首先須要 Vector 圖像和它所對應的動畫,這裏依然使用上一小節水滴狀態的圖像,
先來看一下效果:
給 path 定義 name,以下所示
定義一個 Animator 文件,以表示對這幅 Vector 圖像作動畫
<?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>
複製代碼
須要注意的是,這裏的文件是對應 Vector 中 path 標籤的,這裏動畫效果是動態改變 path 標籤的 trimPathStart 屬性值,從 0 ~ 1 。
定義 animated-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/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 表明關聯的 vector 圖像,target 表明將 path name 和動畫進行關聯
代碼中進行設置
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()
}
}
複製代碼
利用在線繪製 SVG 圖標網站 製做搜索圖標
能夠本身隨意搗鼓繪製,繪製好了以後點擊視圖->源代碼,將 SVG 代碼複製出來保存成 search_svg.xml
點擊空白或者直接將 SVG 拖拽指定區域進行轉換
將轉換好的 Android 格式的 vector 導入 AS
開始製做動畫關聯
//1.在 /res/aniamator 文件夾下 定義動畫
<?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/ 關聯動畫和 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>
複製代碼
效果
仍是很炫吧,😁!代碼在GitHub
詳細代碼請移步GitHub
來一個複雜組合動畫,請看下面效果圖:
準備 vector 數據
<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>
複製代碼
定義順時針執行動畫並作 pathData 變換
這裏拿其中一個位置變化來舉例說明:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:ordering="sequentially">//按順序執行
//依次執行 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>
複製代碼
若是對標籤中的定義還不瞭解的先去看下文章中 path 標籤
中的說明。若是不理解標籤意思,根本就看不懂。
進行關聯
<?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>
複製代碼
代碼控制重複執行
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)
//水滴動畫
startWaterDropAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_vector, false)
}
//搜索動畫
startSearchAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_search, false)
}
//執行警車動畫
startPoliceCarAnimator.setOnClickListener {
startAnimatabe(R.drawable.line_animated_car, false)
}
//執行頭條動畫
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 設置 restart 無效暫時用 Handler 實現了。
reStartTT.sendEmptyMessage(0)
}
})
return animatable
}
}
複製代碼
詳細代碼請移步GitHub
該篇以前實現 SVG pathData 都是利用 ImageView 來實現,並非全部的場合都適合上面的方式,好比我想要實現 pathData 區域點擊,那麼上面所講的方式應該是不能實現,下面咱們以一個實例來看怎麼自定義 View 實現 PathData 和 pathData 區域點擊事件。
下面咱們利用 path 來繪製一箇中國地圖,先來看一個最終效果圖,以下:
看起來是否是很炫,還不錯,嘿嘿,下面咱們就來看一下如何實現。
準備地圖 SVG
首先去下載地圖數據
選擇下載免費的地圖數據
找到對應的國家點擊下載 svg 數據
選擇對應的地圖數據,我這裏下載的是高質量的 SVG
SVG to Vector xml
將下載好的 china.svg 格式的文件轉爲 vector 節點的 xml 數據 或者用 AS 自帶轉也行,看我的愛好。
轉好以後放入 AS 中,以下所示
如今有了這些數據,咱們就能夠解析 xml path
節點,拿到 pathData
數據咱們不就能夠繪製 path 了嘛。下面就開始解析 xml ,解析的方法不少種,咱們這裏用 dom 解析。
開始解析 xml
解析 xml 有不少種方式,這裏就直接使用 DOM
解析,pathData2Path 我這裏直接用 Android SDK 提供的 android.support.v4.graphics#PathParser
因爲源碼中它被標註了 hide
屬性 ,咱們須要直接將它 copy 到咱們本身項目中, 具體轉化請看以下代碼:
/** * 開始解析 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 節點的全部信息
val elementsByTagName = document.getElementsByTagName(PATH_TAG)
//定義四個點,肯定整個 map 的範圍
var left = -1f
var right = -1f
var top = -1f
var bottom = -1f
//開始遍歷標籤,拿到 path 數據組
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()
//獲取到每一個省份的邊界
path.computeBounds(rect, true)
//遍歷取出每一個path中的left取全部的最小值
left = if (left == -1f) rect.left else Math.min(left, rect.left)
//遍歷取出每一個path中的right取全部的最大值
right = if (right == -1f) rect.right else Math.max(right, rect.right)
//遍歷取出每一個path中的top取全部的最小值
top = if (top == -1f) rect.top else Math.min(top, rect.top)
//遍歷取出每一個path中的bottom取全部的最大值
bottom = if (bottom == -1f) rect.bottom else Math.max(bottom, rect.bottom)
}
//MAP 的矩形區域
MAP_RECTF = RectF(left, top, right, bottom)
return mapDataLists;
}
複製代碼
進行控件測量適配橫豎屏切換和寬高定義 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 數據 */
@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
)
}
複製代碼
給地圖添加各自的點擊事件
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)
}
/** * 處理點擊事件 */
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()
}
}
}
}
}
/** * 判斷是否在點擊區域內 */
fun isTouchRegion(x: Float, y: Float, path: Path): Boolean {
//建立一個矩形
val rectF = RectF()
//獲取到當前省份的矩形邊界
path.computeBounds(rectF, true)
//建立一個區域對象
val region = Region()
//將path對象放入到Region區域對象中
region.setPath(path, Region(rectF.left.toInt(), rectF.top.toInt(), rectF.right.toInt(), rectF.bottom.toInt()))
//返回是否這個區域包含傳進來的座標
return region.contains(x.toInt(), y.toInt())
}
複製代碼
詳細代碼請看MapView.kt
到這裏 SVG 知識已經講解完了,以爲還不過癮的能夠本身嘗試一下其餘國家的地圖繪製。
這裏必定要注意在低版本上使用 SVG 存在兼容問題,須要各自查閱資料解決。
不知道還有沒有記得上一篇 高級 UI 成長之路 (六) PathMeasure 製做路徑動畫 中我提到了只要給我一個 Path
數據,我就能繪製出圖形,看完該篇是否是認爲說的沒毛病吧。建議你們在項目上多使用 SVG ,好處文章開頭也提到了,這裏就不在囉嗦了。到這裏 SVG 製做圖像和動畫效果就所有講完了。