前言:為什麼起這個標題呢?因為,這個滑動按鈕看起來不是那麼的僵硬,哈哈。限於篇幅原因,不會把所有的知識點都講解一遍,只會挑選一些需要注意的點及不太好理解的地方進行講解。
效果圖
先放張最後實現效果圖,大家可以看著這個效果,思考一下怎麼實現的。
主要講解的內容
文章將會選擇以下內容進行講解
- 怎樣讓按鈕隨手指移動
- 處理越界問題的方法
- 怎樣處理回彈(就是沒有滑動到指定位置,返回到原點)
- 怎樣在滑動到指定位置後禁止滑動
- 讓文字跟隨按鈕移動的方法
- 自定義View新增陰影的方法
這裡會選擇按鈕初始位置在中間的這種情況來講解,因為,按鈕初始位置在左邊的時候就是按鈕位置在中間的時候一種狀態。
讓按鈕隨手指移動
要想讓按鈕隨著手指的移動而移動,就需要重寫View的onTouchEvent
方法,在這個方法中可以監聽手指的幾個動作,如手指的“按下”、“滑動”、“抬起”,
捕獲到這些動作後,就可以針對每個動作做相應的處理,最終達到讓按鈕隨手指移動的效果。具體的程式碼如下
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)
}
複製程式碼
上面的每行程式碼都進行了註釋,這裡就不再講解每句程式碼的了。
處理越界問題的方法
如果你是按上面的程式碼來讓按鈕跟隨手指進行移動的話,當移動到邊緣的時候你會發現移動的按鈕超出背景了!那這是什麼問題呢?因為,這裡是把手指移動的位置的X座標作為圓心的X座標了,當手指移動到邊緣的時候,圓心的X座標也在邊緣了,所以,就會出現按鈕超出背景的問題了。那麼該怎麼解決這個問題呢?其實很簡單,就是當手指移動的位置的X座標大於View的總長度減去按鈕半徑長度的時候,將slideX
變數重新賦值。程式碼如下
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軸的座標。
處理回彈
這部分要處理的就是,當按鈕沒有移動到目標位置時,抬起手指,是讓按鈕返回到初始的位置,還是讓按鈕到達目標位置。處理這個問題其實也很簡單,就是需要事先設定一個位置,當抬起手指的時候判斷圓心的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之定點寫文字。
自定義View新增陰影的方法
新增陰影的方法就是為畫筆設定setShadowLayer
,我們來看下這個方法
setShadowLayer(float radius, float dx, float dy, int shadowColor)
複製程式碼
方法中有四個引數,四個引數的作用如下
- radius:設定陰影的模糊半徑,其實就是設定陰影的大小,當為0時沒有陰影
- dx:陰影在X軸上的偏移量
- dy:陰影在Y軸上的偏移量
- shadowColor:陰影的顏色
注:只為畫筆設定這個是不起作用的,還需要關閉硬體加速。
結束語
文章到這裡,已經把需要解決的問題都解決了。其實除了文中說的這些,還有其他的一些需要注意的細節,如只有點選在按鈕範圍內才能滑動、狀態改變時的回撥等。這些細節都在程式碼中進行了處理,可以點選這裡獲取原始碼,覺得不錯的話,就順手點個star吧!
本文已由公眾號“AndroidShared”首發