擼一款”靈動“的滑動按鈕

wizardev發表於2019-03-02

前言:為什麼起這個標題呢?因為,這個滑動按鈕看起來不是那麼的僵硬,哈哈。限於篇幅原因,不會把所有的知識點都講解一遍,只會挑選一些需要注意的點及不太好理解的地方進行講解。

效果圖

  先放張最後實現效果圖,大家可以看著這個效果,思考一下怎麼實現的。

擼一款”靈動“的滑動按鈕

主要講解的內容

  文章將會選擇以下內容進行講解

  • 怎樣讓按鈕隨手指移動
  • 處理越界問題的方法
  • 怎樣處理回彈(就是沒有滑動到指定位置,返回到原點)
  • 怎樣在滑動到指定位置後禁止滑動
  • 讓文字跟隨按鈕移動的方法
  • 自定義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”首發

歡迎關注我的公眾號
掃碼關注公眾號,回覆“獲取資料”有驚喜

相關文章