擼一款」靈動「的滑動按鈕

前言:爲何起這個標題呢?由於,這個滑動按鈕看起來不是那麼的僵硬,哈哈。限於篇幅緣由,不會把全部的知識點都講解一遍,只會挑選一些須要注意的點及不太好理解的地方進行講解。java

效果圖

  先放張最後實現效果圖,你們能夠看着這個效果,思考一下怎麼實現的。git

主要講解的內容

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

  • 怎樣讓按鈕隨手指移動
  • 處理越界問題的方法
  • 怎樣處理回彈(就是沒有滑動到指定位置,返回到原點)
  • 怎樣在滑動到指定位置後禁止滑動
  • 讓文字跟隨按鈕移動的方法
  • 自定義View添加陰影的方法

這裏會選擇按鈕初始位置在中間的這種狀況來說解,由於,按鈕初始位置在左邊的時候就是按鈕位置在中間的時候一種狀態。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之定點寫文字

自定義View添加陰影的方法

  添加陰影的方法就是爲畫筆設置setShadowLayer,咱們來看下這個方法

setShadowLayer(float radius, float dx, float dy, int shadowColor)
複製代碼

方法中有四個參數,四個參數的做用以下

  • radius:設置陰影的模糊半徑,其實就是設置陰影的大小,當爲0時沒有陰影
  • dx:陰影在X軸上的偏移量
  • dy:陰影在Y軸上的偏移量
  • shadowColor:陰影的顏色

注:只爲畫筆設置這個是不起做用的,還須要關閉硬件加速。

結束語

  文章到這裏,已經把須要解決的問題都解決了。其實除了文中說的這些,還有其餘的一些須要注意的細節,如只有點擊在按鈕範圍內才能滑動、狀態改變時的回調等。這些細節都在代碼中進行了處理,能夠點擊這裏獲取源碼,以爲不錯的話,就順手點個star吧!

本文已由公衆號「AndroidShared」首發

歡迎關注個人公衆號
掃碼關注公衆號,回覆「獲取資料」有驚喜
相關文章
相關標籤/搜索