Kotlin--›Android 超高模仿QQ7.5 側滑菜單

效果圖:
這裏寫圖片描述

特性模仿

  • 全屏可視區域滑動檢測 (菜單關閉和打開狀態, 都支持)
  • 內容區域滑動過程中自帶陰影遮罩
  • 菜單打開狀態, 點擊陰影區域自動關閉
  • 滑動過程中, 視差效果
  • 可以嵌套在其他具有滾動特性的View中

實現方法如果使用 ViewDragHelper 那麼侷限性會很多, 所以這裏我採用了最原始的TouchEvent控制.

以下代碼, 只貼部分片段, 詳細請下載源碼

首先監聽Touch事件
任何需要處理Touch事件的控件, 都必備回調2個方法onScrollonFling, 缺一個都是不完整的.
onScroll:用在簡單的手指上下,左右移動. 這個方法會回調出, 手指與上一個事件的移動間隔距離
onFling:用在手指快速的上下,左右移動. 這個方法回調出的是手指與上一個事件之間的移動速度

/*用來檢測手指滑動方向*/
protected val orientationGestureDetector = GestureDetectorCompat(context, object : GestureDetector.SimpleOnGestureListener() {
   override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
       //L.e("call: onFling -> \n$e1 \n$e2 \n$velocityX $velocityY")

       firstMotionEvent = e1
       secondMotionEvent = e2

       val absX = Math.abs(velocityX)
       val absY = Math.abs(velocityY)

       if (absX > flingVelocitySlop || absY > flingVelocitySlop) {
           if (absY > absX) {
               //豎直方向的Fling操作
               onFlingChange(if (velocityY > 0) ORIENTATION.BOTTOM else ORIENTATION.TOP, velocityY)
           } else if (absX > absY) {
               //水平方向的Fling操作
               onFlingChange(if (velocityX > 0) ORIENTATION.RIGHT else ORIENTATION.LEFT, velocityX)
           }
       }

       return true
   }

   override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
       //L.e("call: onScroll -> \n$e1 \n$e2 \n$distanceX $distanceY")
       firstMotionEvent = e1
       secondMotionEvent = e2

       val absX = Math.abs(distanceX)
       val absY = Math.abs(distanceY)

       if (absX > scrollDistanceSlop || absY > scrollDistanceSlop) {
           if (absY > absX) {
               //豎直方向的Scroll操作
               onScrollChange(if (distanceY > 0) ORIENTATION.TOP else ORIENTATION.BOTTOM, distanceY)
           } else if (absX > absY) {
               //水平方向的Scroll操作
               onScrollChange(if (distanceX > 0) ORIENTATION.LEFT else ORIENTATION.RIGHT, distanceX)
           }
       }

       return true
   }

})

如何讓Layout移動起來?
通常情況下在View中, 我們會想到用View的x,y, 座標來控制.或者translationX, translationY, 等;
在ViewGroup, 我們可以用ScrollTo, ScrollBy方法, 讓佈局移動起來.

但是, 我這裏用了View.Layout方法, 通過改變View在ViewGroup中的座標, 達到移動效果.

private fun refreshContentLayout(left: Int) {
   if (childCount == 2) {
       getChildAt(1).apply {
           layout(left, 0, left + this.measuredWidth, this.measuredHeight)  //關鍵點
       }
   }
}

我們只需要, 不斷的傳入不同的left座標值, 就可以讓View移動起來;

那麼如何讓left不斷變化呢?
很自然的就想到了用動畫, 動畫雖然也簡單, 但是我這裏用了一個和自定義View密切相關的另一個必備神器類OverScroller
也許你會覺得OverScroller是用來滾動View的, 那你就太小看它了. 它其實內部也是動畫機制.

如何使用OverScroller讓left動起來?
其實, 你只要會用它, 基本上不難, 關鍵在於…請往下看!

 /**開始滾動到某個位置*/
 open fun startScrollTo(startX: Int, toX: Int) {
     overScroller.startScroll(startX, scrollY, toX - startX, 0, 300)
     postInvalidate()
 }

OverScroller密切關聯的方法就是computeScroll了.其實你把它理解成OverScroller的執行回調, 可能更好一點.

 override fun computeScroll() {
     if (overScroller.computeScrollOffset()) {
         //scrollTo(overScroller.currX, overScroller.currY) //原本應該是這樣的.
         val currX = overScroller.currX
         if (contentLayoutLeft != currX) {
             refreshContentLayout(currX)   //但是...投機取巧, 我們用這個...完美的讓left動起來了.
         }
         postInvalidate()
     }
 }

到此:ViewDragHelper的拖動功能, 已經模仿出來了.但是, 我們的可擴展性, 可操作比他大100倍.

如何營造視差效果?
對於這個, 其實我們只需要在 更新內容left的時候, 同步更新菜單的left就行了. 只不過left要經過阻尼處理一下. 效果才逼真.

 //單獨更新菜單,營造視差滾動
 private fun refreshMenuLayout() {
     //計算出菜單展開的比例
     val fl = contentLayoutLeft.toFloat() / maxMenuWidth
     if (fl > 0f && childCount > 0) {
         getChildAt(0).apply {
             //視差開始時的偏移值
             val menuOffsetStart = -maxMenuWidth / 2
             val left = menuOffsetStart + (menuOffsetStart.abs() * fl).toInt()
             layout(left, 0, left + this.measuredWidth, this.measuredHeight)
         }
     }
 }

如何實現陰影遮罩?
玩過Canvas的應該都知道, 直接繪製一個透明圖層就行了. 關鍵在於, 透明度要跟隨菜單的打開程度動態計算

override fun draw(canvas: Canvas) {
     super.draw(canvas)
     //繪製內容區域的陰影遮蓋
     if (isMenuClose()) {

     } else {
         val layoutLeft = contentLayoutLeft
         debugPaint.color = Color.BLACK.tranColor((255 * (layoutLeft.toFloat() / maxMenuWidth) * 0.4f /*限制一下值*/).toInt())
         debugPaint.style = Paint.Style.FILL_AND_STROKE
         maskRect.set(layoutLeft, 0, measuredWidth, measuredHeight)
         canvas.drawRect(maskRect, debugPaint)
     }
 }

也許你還想學習更多, 來我的羣吧, 我寫代碼的能力, 遠大於寫文章的能力:

聯繫作者

點此快速加羣

請使用QQ掃碼加羣, 小夥伴們在等着你哦!

關注我的公衆號, 每天都能一起玩耍哦!


開源地址: https://github.com/YaYaStudio/SliderMenuLayout

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章