在多年前,曾寫過這篇的我
Willy's Fish教學筆記』 Android Canvas 繪圖 介紹 教學 使用
今天就要利用裡面提到的相關方法,來實作一個 custom view
這次的 UI 需求是像漫畫一樣的對話框,如下圖
https://github.com/WillysFish/ArrowTooltip
我把他包成一個庫,讓大家可以使用
使用方法如下:
在 root build.gradle 加入
allprojects {
repositories {
。。。
maven { url 'https://jitpack.io' }
}
}
然後 implementation
implementation 'com.github.WillysFish:ArrowTooltip:1.0.0'
======================================================
在引入 library 之後,我們可以到 github 查看相關的使用方式
在這裡我們就先聚焦在裡面是如何實作的
首先,這是一個用 ConstraintLayout 做成的 view
class ArrowTooltipLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {}
我們會畫出背景樣式
然後包一些我們想要 show 的 view 在裡面
用法就和一般的 ConstraintLayout 一樣的方便
再來,我們會利用 attrs.xml 來定義我們需要的自訂參數,如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ArrowTooltipLayout">
<attr name="arrowOrientation" format="enum">
<enum name="top" value="0" />
<enum name="bottom" value="1" />
<enum name="start" value="2" />
<enum name="end" value="3" />
</attr>
。。。
<attr name="arrowHeight" format="dimension" />
<attr name="arrowWidth" format="dimension" />
</declare-styleable>
</resources>
在 Layout 的開頭,可以去取得我們剛剛自訂的參數
// 取得自訂參數
private val attributes by lazy {
context.obtainStyledAttributes(
attrs, R.styleable.ArrowTooltipLayout)
}
// 顏色
private val tipLayoutColor =
attributes.getColor(
R.styleable.ArrowTooltipLayout_tipLayoutColor, Color.GRAY)
。。。
在畫畫之前,還有最後一步要做
就是決定 view 的大小
override fun onMeasure(
widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val minW: Int =
paddingLeft + paddingRight + suggestedMinimumWidth
val w: Int =
View.resolveSizeAndState(minW, widthMeasureSpec, 0)
val minH: Int =
paddingBottom + paddingTop + suggestedMinimumHeight
val h: Int =
View.resolveSizeAndState(minH, heightMeasureSpec, 0)
setMeasuredDimension(w, h)
}
這裡面我們先定了最小 size
然後利用 resolveSizeAndState() 來算出最後的 size
那為什麼要用 resolveSizeAndState() 呢?
比如說我們常用的 wrap_content、match_parent
resolveSizeAndState() 會將我們可接受的最小值與 MeasureSpec of Parent 做計算
決定出最後的大小
在設定完 size 之後
終於,最後一步,我們可以來畫我們想要的對話框了
畫面主要是由 onDraw() 來完成
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.also { c ->
// 箭頭方向
when (arrowOrientation) {
ArrowOrientation.BOTTOM -> drawBottomArrowLayout(c)
ArrowOrientation.START -> drawLeftArrowLayout(c)
ArrowOrientation.END -> drawRightArrowLayout(c)
else -> drawTopArrowLayout(c)
}
}
}
由於我們有箭頭方向的參數
所以畫的方式會不同,這裡以箭頭在下的情況來解說
private fun drawBottomArrowLayout(canvas: Canvas) {
// 起點
val startX = if (specificX > 0) specificX
else measuredWidth * positionPercentage
val startY = measuredHeight.toFloat()
// 四個角
val topRightX = measuredWidth.toFloat()
val topRightY = 0F
val bottomRightX = measuredWidth.toFloat()
val bottomRightY = measuredHeight - arrowH.toFloat()
val bottomLeftX = 0F
val bottomLeftY = measuredHeight - arrowH.toFloat()
val topLeftX = 0F
val topLeftY = 0F
// 三角點 1 & 2 (依起點順時針)
val triangleX1 = startX - arrowW / 2F
val triangleY1 = startY - arrowH.toFloat()
val triangleX2 = startX + arrowW / 2F
val triangleY2 = startY - arrowH.toFloat()
// 設置 Path
path.moveTo(startX, startY)
path.lineTo(triangleX1, triangleY1)
path.lineTo(bottomLeftX + tipLayoutRadius, bottomLeftY)
path.cubicTo(
bottomLeftX + tipLayoutRadius, bottomLeftY,
bottomLeftX, bottomLeftY,
bottomLeftX, bottomLeftY - tipLayoutRadius
)
path.lineTo(topLeftX, topLeftY + tipLayoutRadius)
path.cubicTo(
topLeftX, topLeftY + tipLayoutRadius,
topLeftX, topLeftY,
topLeftX + tipLayoutRadius, topLeftY
)
path.lineTo(topRightX - tipLayoutRadius, topRightY)
path.cubicTo(
topRightX - tipLayoutRadius, topRightY,
topRightX, topRightY,
topRightX, topRightY + tipLayoutRadius
)
path.lineTo(bottomRightX, bottomRightY - tipLayoutRadius)
path.cubicTo(
bottomRightX, bottomRightY - tipLayoutRadius,
bottomRightX, bottomRightY,
bottomRightX - tipLayoutRadius, bottomRightY
)
path.lineTo(triangleX2, triangleY2)
path.close()
canvas.drawPath(path, paint)
}
這裡我們介紹幾個東西
1、measuredHeight、measuredWidth 是我們這個 view 的實際大小
2、cubicTo() 我們給予 3 個座標,讓其畫出貝茲曲線 (導角 radius)
利用上面這些東西
我們可以算出每個需要的座標點
然後描成一個 Path
最後再用 Canvas.drawPath 就可以呈現畫面了
完成 !!
等一下,run 過之後我們會發現剛剛畫的東西,都沒有出現
onDraw() 並沒有被調用
這時候要加上這句,來解鎖 Draw 功能
init {
setWillNotDraw(false)
}
因為 ViewGroup 在一般情況下
出於性能考量,會被設置成 WILL_NOT_DROW,這樣,ondraw 就不會被執行
所以我們設定為 false,onDraw 才會被調用
這麼一來就 OK 了,一個我們自己畫的 View 就完成了
參考資料:
https://developer.android.com/training/custom-views/custom-drawing
https://github.com/WillysFish/ArrowTooltip
留言列表