前言:爲何起這個標題呢?由於,這個滑動按鈕看起來不是那麼的僵硬,哈哈。限於篇幅緣由,不會把全部的知識點都講解一遍,只會挑選一些須要注意的點及不太好理解的地方進行講解。java
先放張最後實現效果圖,你們能夠看着這個效果,思考一下怎麼實現的。git
文章將會選擇如下內容進行講解github
這裏會選擇按鈕初始位置在中間的這種狀況來說解,由於,按鈕初始位置在左邊的時候就是按鈕位置在中間的時候一種狀態。canvas
要想讓按鈕隨着手指的移動而移動,就須要重寫View的onTouchEvent
方法,在這個方法中能夠監聽手指的幾個動做,如手指的「按下」、「滑動」、「擡起」,ide
捕獲到這些動做後,就能夠針對每一個動做作相應的處理,最終達到讓按鈕隨手指移動的效果。具體的代碼以下post
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
//手指按下
MotionEvent.ACTION_DOWN -> {
val clickX = event.x
}
//手指移動
MotionEvent.ACTION_MOVE -> {
slideX = event.x
//刷新view
postInvalidate()
}
//手指擡起
MotionEvent.ACTION_UP -> {
}
}
return super.onTouchEvent(event)
}
複製代碼
簡單解釋一下上面的代碼,在手指按下的時候,獲取手機按下位置的座標,用clickX
變量保存按下的X軸的位置(後面會用到)
,在手指滑動的時候用全局變量slideX
來保存滑動到的X的座標,而後刷新view。手指擡起的動做這裏不用處理。上面的代碼只是獲取到了手指移動的X軸的座標,下面就要經過View的onDraw
方法來繪製中間按鈕,代碼以下動畫
private fun drawSnake(canvas: Canvas) {
//計算圓的半徑
val circleRadius = mSnakeRadius.toFloat() - mShadowRadius / 2
//計算中間按鈕的X座標,在初始的時候,slideX的值爲0
val circleCenter = if (slideX == 0f) {
if (mSlideState == SlideState.INIT_LEFT) {
//按鈕位於左邊的時候圓心的X座標
mSnakeRadius + mShadowRadius / 2
} else {
//按鈕在中間的時候圓心的X座標,mResultWidth爲View的寬度
mResultWidth / 2.toFloat()
}
} else {
//手指滑動後
slideX
}
//這裏根據手指移動到的位置來肯定圓形的X座標
canvas.drawCircle(circleCenter, mResultHeight / 2.toFloat(), circleRadius, mSnakePaint)
}
複製代碼
上面的每行代碼都進行了註釋,這裏就再也不講解每句代碼的了。ui
若是你是按上面的代碼來讓按鈕跟隨手指進行移動的話,當移動到邊緣的時候你會發現移動的按鈕超出背景了!那這是什麼問題呢?由於,這裏是把手指移動的位置的X座標做爲圓心的X座標了,當手指移動到邊緣的時候,圓心的X座標也在邊緣了,因此,就會出現按鈕超出背景的問題了。那麼該怎麼解決這個問題呢?其實很簡單,就是當手指移動的位置的X座標大於View的總長度減去按鈕半徑長度的時候,將slideX
變量從新賦值。代碼以下spa
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
//手指按下
MotionEvent.ACTION_DOWN -> {
val clickX = event.x
}
//手指移動
MotionEvent.ACTION_MOVE -> {
slideX = event.x
if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
//防止超出右邊界
slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
} else if (slideX < mSnakeRadius + mShadowRadius / 2) {
//防止超出左邊界
slideX = mSnakeRadius+mShadowRadius / 2
}
//刷新view
postInvalidate()
}
//手指擡起
MotionEvent.ACTION_UP -> {
}
}
return super.onTouchEvent(event)
}
複製代碼
從上面的代碼中能夠可看到,處理按鈕超出邊界的方法是,當圓心的X的座標將要大於或小於要求的座標的時候,不直接讓手指的X的座標做爲圓心的座標了,而是從新計算圓心X軸的座標。3d
這部分要處理的就是,當按鈕沒有移動到目標位置時,擡起手指,是讓按鈕返回到初始的位置,仍是讓按鈕到達目標位置。處理這個問題其實也很簡單,就是須要事先設定一個位置,當擡起手指的時候判斷圓心的X的座標大於這個位置仍是小於這個位置,若是大於就直接將按鈕移到目標位置,若是小於就將按鈕移動到初始位置。以下圖
這裏規定的位置如上圖,這個位置距離黑色背景邊界的長度恰好等於按鈕的半徑。當往左邊滑動時,手指擡起的時候,若是圓心的X座標大於2倍的按鈕的半徑即直徑的話,就回到初始位置,不然滑動到左邊,往右邊滑動時的判斷同理。具體的代碼以下
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
//手指按下
MotionEvent.ACTION_DOWN -> {
val clickX = event.x
}
//手指移動
MotionEvent.ACTION_MOVE -> {
slideX = event.x
if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
//防止超出右邊界
slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
} else if (slideX < mSnakeRadius + mShadowRadius / 2) {
//防止超出左邊界
slideX = mSnakeRadius+mShadowRadius / 2
}
//刷新view
postInvalidate()
}
//手指擡起
MotionEvent.ACTION_UP -> {
//判斷左滑仍是右滑
if (slideX > center) {
//按鈕的邊界超出右邊規定的位置
if (slideX > mResultWidth - 2 * mSnakeRadius - mShadowRadius) {
//直接滑到目標點
slideAnimate(slideX.toInt(), (mResultWidth - mSnakeRadius - mShadowRadius / 2).toInt())
mSlideState = SlideState.RIGHT_FINISH
if (slideListener != null) {
slideListener!!.onSlideRightFinish()
}
} else {
setInitText()
slideAnimate(slideX.toInt(), center)
}
} else if (slideX < center) {
if (slideX < 2 * mSnakeRadius + mShadowRadius) {
//直接滑到目標點
slideAnimate(slideX.toInt(), (mSnakeRadius + mShadowRadius / 2).toInt())
mSlideState = SlideState.LEFT_FINISH
if (slideListener != null) {
slideListener!!.onSlideLiftFinish()
}
} else {
setInitText()
slideAnimate(slideX.toInt(), center)
}
} else {
//鬆手後回到原點
slideAnimate(slideX.toInt(), center)
}
}
}
return super.onTouchEvent(event)
}
複製代碼
上面的代碼中slideAnimate
這個方法是添加位移動畫,代碼以下
private fun slideAnimate(start: Int, end: Int) {
val valueAnimator = ValueAnimator.ofInt(start, end)
valueAnimator.addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Int
slideX = animatedValue.toFloat()
postInvalidate()
}
valueAnimator.start()
}
複製代碼
什麼叫滑動到指定位置呢?這裏拿按鈕在中間時候的View舉例,好比當按鈕滑動到兩頭的時候,就是到指定位置了,這時擡起手指,在下次再滑動按鈕的時候就不能滑動了。這個問題也很好解決,解決的方法也有不少,本文采用的方法就是初始化一個成員變量,每次進入onTouchEvent
方法時,首先判斷這個變量,條件成立則直接返回false
。不成立就繼續響應手指的動做,當按鈕滑動到指定的位置的就改變這個變量的值,使下次再進入這個方法時條件成立。代碼以下
override fun onTouchEvent(event: MotionEvent): Boolean {
//滑動完成後禁止滑動
if (mSlideState == SlideState.LEFT_FINISH || mSlideState == SlideState.RIGHT_FINISH) {
return false
}
when (event.action) {
//手指按下
MotionEvent.ACTION_DOWN -> {
val clickX = event.x
}
//手指移動
MotionEvent.ACTION_MOVE -> {
slideX = event.x
if (slideX > mResultWidth - mSnakeRadius - mShadowRadius / 2) {
//防止超出右邊界
slideX = mResultWidth - mSnakeRadius-mShadowRadius / 2
} else if (slideX < mSnakeRadius + mShadowRadius / 2) {
//防止超出左邊界
slideX = mSnakeRadius+mShadowRadius / 2
}
//刷新view
postInvalidate()
}
//手指擡起
MotionEvent.ACTION_UP -> {
//判斷左滑仍是右滑
if (slideX > center) {
//按鈕的邊界超出右邊規定的位置
if (slideX > mResultWidth - 2 * mSnakeRadius - mShadowRadius) {
//直接滑到目標點
slideAnimate(slideX.toInt(), (mResultWidth - mSnakeRadius - mShadowRadius / 2).toInt())
//改變變量的狀態
mSlideState = SlideState.RIGHT_FINISH
if (slideListener != null) {
slideListener!!.onSlideRightFinish()
}
} else {
setInitText()
slideAnimate(slideX.toInt(), center)
}
} else if (slideX < center) {
if (slideX < 2 * mSnakeRadius + mShadowRadius) {
//直接滑到目標點
slideAnimate(slideX.toInt(), (mSnakeRadius + mShadowRadius / 2).toInt())
//改變變量的狀態
mSlideState = SlideState.LEFT_FINISH
if (slideListener != null) {
slideListener!!.onSlideLiftFinish()
}
} else {
setInitText()
slideAnimate(slideX.toInt(), center)
}
} else {
//鬆手後回到原點
slideAnimate(slideX.toInt(), center)
}
}
}
return super.onTouchEvent(event)
}
複製代碼
這部分可能算是自定義這個View最複雜的部分了,其實只要想明白了,也不復雜。這裏就拿按鈕從中間往右滑來說解,按鈕從中間往左邊滑計算方法和往右滑的差很少,先看圖
好了,如今根據上面的圖,就能夠算出初始和最終的文字的X軸的位置了,
初始狀態X = (mResultWidth / 2- mSnakeRadius)/2
最終狀態X=mResultWidth / 2
mResultWidth爲View的寬度,mSnakeRadius爲按鈕的半徑
知道了初始和最終的X軸的位置,下面就是根據按鈕移動的距離來改變文字的X做座標了
val distance = end - start
val resultLeftX = start + distance * proportion
end和start就是最終狀態X和初始狀態X,resultLeftX就是要畫的文字的X座標,proportion就是百分比,根據公式可知這個proportion值的變化範圍值從0到1的。
如今,最重要的一點就是怎麼根據按鈕的移動來改變上面公示中的proportion
,應該怎麼辦呢?繼續看圖
這個proportion
就是上圖中的x比y的值,上圖中中間的一條線就是slideX
,它是根據按鈕的滑動不斷變化的,這樣就能知足proportion
的值從0到1的條件了。計算的公式以下
proportion = (slideX - halfResultWidth) / (mResultWidth - mSnakeRadius - halfResultWidth - mShadowRadius / 2)
這部分具體的代碼以下
private fun drawInnerText(canvas: Canvas) {
//這部分是裁剪畫布,由於當viwe加陰影的時候,文字會越界,想看效果的話,能夠把裁剪畫布的代碼註釋掉
canvas.save()
//裁剪的大小是黑色背景的大小,形狀是矩形
canvas.clipRect(
mShadowRadius,
mShadowRadius,
mResultWidth.toFloat() - mShadowRadius,
mResultHeight.toFloat() - mShadowRadius
)
val fontMetrics = mInnerTextPaint.fontMetrics
//畫圓環內的文字
val baseline = mResultHeight / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom
//文字的起始位置
val halfResultWidth = mResultWidth / 2
val start = (halfResultWidth - mSnakeRadius) / 2
//文字的最終位置
// val end = (mResultWidth - 2 * mSnakeRadius) / 2
val end = mResultWidth / 2
//距離
val distance = end - start
//根據狀態不一樣設置按鈕的初始位置
var proportion = if (mSlideState == SlideState.INIT_LEFT) {
-1f
} else {
0f
}
if (slideX != 0f) {
//計算比例來移動文字的距離
proportion = (slideX - halfResultWidth) / (mResultWidth - mSnakeRadius - halfResultWidth - mShadowRadius / 2)
}
val resultLeftX = start + distance * proportion
//畫按鈕左邊的文字
canvas.drawText(
leftContent,
resultLeftX,
baseline,
mInnerTextPaint
)
val resultRightX =
halfResultWidth + (mSnakeRadius / 2) + (halfResultWidth / 2) + (distance * proportion)
//畫按鈕右邊的文字
canvas.drawText(
rightContent,
resultRightX,
baseline,
mInnerTextPaint
)
canvas.restore()
}
複製代碼
這裏重點解釋一下下面的一段代碼
private fun drawInnerText(canvas: Canvas) {
canvas.save()
canvas.clipRect(
mShadowRadius,
mShadowRadius,
mResultWidth.toFloat() - mShadowRadius,
mResultHeight.toFloat() - mShadowRadius
)
//...省略部分代碼
canvas.restore()
}
複製代碼
**這段代碼是爲了解決給View畫陰影的時候,文字移動超出背景仍然顯示的問題。**當view添加陰影時,能夠把這段代碼註釋掉,看下有什麼不一樣。還有一點就是在指定位置寫文字的問題,你們能夠參考個人這篇文章Android自定義View之定點寫文字。
添加陰影的方法就是爲畫筆設置setShadowLayer
,咱們來看下這個方法
setShadowLayer(float radius, float dx, float dy, int shadowColor)
複製代碼
方法中有四個參數,四個參數的做用以下
注:只爲畫筆設置這個是不起做用的,還須要關閉硬件加速。
文章到這裏,已經把須要解決的問題都解決了。其實除了文中說的這些,還有其餘的一些須要注意的細節,如只有點擊在按鈕範圍內才能滑動、狀態改變時的回調等。這些細節都在代碼中進行了處理,能夠點擊這裏獲取源碼,以爲不錯的話,就順手點個star吧!
本文已由公衆號「AndroidShared」首發