Kotlin--›Android RecyclerView滾動處理(滾動到底部/頂部/居中/偏移/動畫等特性)

之前寫過一篇Android–>RecyclerView 顯示底部,滾動底部(無動畫)文章.

當時是爲了滿足需求, 沒想太多順手寫的. 雖然功能上能滿足, 但是代碼上還是有點low.

這幾天, 我的徒弟傻豆 在寫一個IM項目, 需要滾動到底部. 於是我重寫了一個ScrollHelper滾動操作類.


特性

  • 1.支持滾動時的動畫控制
  • 2.支持滾動到任意position
  • 3.支持滾動offset控制
  • 4.支持滾動到頂部or底部or居中
  • 5.支持鎖定滾動, 短時間之內強制滾動到目標position
  • 6.支持智能鎖定滾動(達到某個條件, 自動滾動到設置的目標position)

需求分析

1.滾動, 滾動偏移, 滾動動畫

需要動畫使用:

//帶偏移, 帶動畫
androidx.recyclerview.widget.RecyclerView#smoothScrollBy
//滾動, 帶動畫
androidx.recyclerview.widget.RecyclerView#smoothScrollToPosition

不需要動畫使用:

//帶偏移, 不帶動畫
androidx.recyclerview.widget.RecyclerView#scrollBy
//滾動, 不帶動畫
androidx.recyclerview.widget.RecyclerView#scrollToPosition

注意:
如果觸發了androidx.recyclerview.widget.RecyclerView.ItemAnimator動畫,
那麼androidx.recyclerview.widget.RecyclerView#scrollToPositionandroidx.recyclerview.widget.RecyclerView#smoothScrollToPosition
都會在一定程度上產生滾動動畫.

提示
爲什麼需要使用scrollToPositionscrollBy呢?
這裏給大家推薦一套滾動方案:
如果需要滾動的目標已經出現在屏幕內, 那麼直接使用scrollByorsmoothScrollBy.
如果需要滾動的目標沒有出現在屏幕內, 那麼先使用scrollToPositionorsmoothScrollToPosition,再使用scrollByorsmoothScrollBy.

如果調用了androidx.recyclerview.widget.RecyclerView.Adapter#notifyItemInserted, 那麼scrollToPositionorsmoothScrollToPosition方法可能會無效果.通常此時都需要使用post, 文章後面會給出我的方法.

2. 滾動到頂部, 底部, 居中

需要細粒度的控制滾動, 必須要保證目標已經出現的屏幕內, 纔看完美控制.

控制方法就是scrollByorsmoothScrollBy.

/**當需要滾動的目標位置已經在屏幕上可見*/
internal fun scrollWithVisible(scrollParams: ScrollParams) {
    when (scrollType) {
        SCROLL_TYPE_NORMAL -> {//不處理
            //nothing
        }
        SCROLL_TYPE_TOP -> {//滾動到頂部
            viewByPosition(scrollParams.scrollPosition)?.also { child ->
                recyclerView?.apply {
                    val dx = layoutManager!!.getDecoratedLeft(child) -
                            paddingLeft - scrollParams.scrollOffset

                    val dy = layoutManager!!.getDecoratedTop(child) -
                            paddingTop - scrollParams.scrollOffset

                    if (scrollParams.scrollAnim) {
                        smoothScrollBy(dx, dy)
                    } else {
                        scrollBy(dx, dy)
                    }
                }
            }
        }
        SCROLL_TYPE_BOTTOM -> {//滾動到底部
            viewByPosition(scrollParams.scrollPosition)?.also { child ->
                recyclerView?.apply {
                    val dx =
                        layoutManager!!.getDecoratedRight(child) -
                                measuredWidth + paddingRight + scrollParams.scrollOffset
                    val dy =
                        layoutManager!!.getDecoratedBottom(child) -
                                measuredHeight + paddingBottom + scrollParams.scrollOffset

                    if (scrollParams.scrollAnim) {
                        smoothScrollBy(dx, dy)
                    } else {
                        scrollBy(dx, dy)
                    }
                }
            }
        }
        SCROLL_TYPE_CENTER -> {//滾動到居中
            viewByPosition(scrollParams.scrollPosition)?.also { child ->

                recyclerView?.apply {
                    val recyclerCenterX =
                        (measuredWidth - paddingLeft - paddingRight) / 2 + paddingLeft

                    val recyclerCenterY =
                        (measuredHeight - paddingTop - paddingBottom) / 2 + paddingTop

                    val dx = layoutManager!!.getDecoratedLeft(child) - recyclerCenterX +
                            layoutManager!!.getDecoratedMeasuredWidth(child) / 2 + scrollParams.scrollOffset

                    val dy = layoutManager!!.getDecoratedTop(child) - recyclerCenterY +
                            layoutManager!!.getDecoratedMeasuredHeight(child) / 2 + scrollParams.scrollOffset

                    if (scrollParams.scrollAnim) {
                        smoothScrollBy(dx, dy)
                    } else {
                        scrollBy(dx, dy)
                    }
                }
            }
        }
    }
}

private fun viewByPosition(position: Int): View? {
    return recyclerView?.layoutManager?.findViewByPosition(position)
}

3.鎖定滾動

鎖定滾動我這裏使用了ViewTreeObserver.OnGlobalLayoutListenerorViewTreeObserver.OnDrawListener當做觸發時機, 這樣就不用自己寫handle post了, 而且觸發更及時.

inner abstract class LockScrollListener : ViewTreeObserver.OnGlobalLayoutListener,
   ViewTreeObserver.OnDrawListener,
   IAttachListener, Runnable {

   /**激活滾動動畫*/
   var scrollAnim: Boolean = true
   /**激活第一個滾動的動畫*/
   var firstScrollAnim: Boolean = false

   /**不檢查界面 情況, 強制滾動到最後的位置. 關閉後. 會智能判斷*/
   var force: Boolean = false

   /**第一次時, 是否強制滾動*/
   var firstForce: Boolean = true

   /**滾動閾值, 倒數第幾個可見時, 就允許滾動*/
   var scrollThreshold = 2

   /**鎖定需要滾動的position, -1就是最後一個*/
   var lockPosition = RecyclerView.NO_POSITION

   /**是否激活功能*/
   var enableLock = true

   /**鎖定時長, 毫秒*/
   var lockDuration: Long = -1

   //記錄開始的統計時間
   var _lockStartTime = 0L

   override fun run() {
       if (!enableLock || recyclerView?.layoutManager?.itemCount ?: 0 <= 0) {
           return
       }

       isScrollAnim = if (firstForce) firstScrollAnim else scrollAnim
       scrollType = SCROLL_TYPE_BOTTOM

       val position =
           if (lockPosition == RecyclerView.NO_POSITION) lastItemPosition() else lockPosition

       if (force || firstForce) {
           scroll(position)
           onScrollTrigger()
           L.i("鎖定滾動至->$position $force $firstForce")
       } else {
           val lastItemPosition = lastItemPosition()
           if (lastItemPosition != RecyclerView.NO_POSITION) {
               //智能判斷是否可以鎖定
               if (position == 0) {
                   //滾動到頂部
                   val findFirstVisibleItemPosition =
                       recyclerView?.layoutManager.findFirstVisibleItemPosition()

                   if (findFirstVisibleItemPosition <= scrollThreshold) {
                       scroll(position)
                       onScrollTrigger()
                       L.i("鎖定滾動至->$position")
                   }
               } else {
                   val findLastVisibleItemPosition =
                       recyclerView?.layoutManager.findLastVisibleItemPosition()

                   if (lastItemPosition - findLastVisibleItemPosition <= scrollThreshold) {
                       //最後第一個或者最後第2個可見, 智能判斷爲可以滾動到尾部
                       scroll(position)
                       onScrollTrigger()
                       L.i("鎖定滾動至->$position")
                   }
               }
           }
       }

       firstForce = false
   }

   var attachView: View? = null

   override fun attach(view: View) {
       detach()
       attachView = view
   }

   override fun detach() {
       attachView?.removeCallbacks(this)
   }

   /**[ViewTreeObserver.OnDrawListener]*/
   override fun onDraw() {
       initLockStartTime()
       onLockScroll()
   }

   /**[ViewTreeObserver.OnGlobalLayoutListener]*/
   override fun onGlobalLayout() {
       initLockStartTime()
       onLockScroll()
   }

   open fun initLockStartTime() {
       if (_lockStartTime <= 0) {
           _lockStartTime = nowTime()
       }
   }

   open fun isLockTimeout(): Boolean {
       return if (lockDuration > 0) {
           val nowTime = nowTime()
           nowTime - _lockStartTime > lockDuration
       } else {
           false
       }
   }

   open fun onLockScroll() {
       attachView?.removeCallbacks(this)
       if (enableLock) {
           if (isLockTimeout()) {
               //鎖定超時, 放棄操作
           } else {
               attachView?.post(this)
           }
       }
   }

   open fun onScrollTrigger() {

   }
}

/**鎖定滾動到最後一個位置*/
inner class LockScrollLayoutListener : LockScrollListener() {

   override fun attach(view: View) {
       super.attach(view)
       view.viewTreeObserver.addOnGlobalLayoutListener(this)
   }

   override fun detach() {
       super.detach()
       attachView?.viewTreeObserver?.removeOnGlobalLayoutListener(this)
   }
}

/**滾動到0*/
inner class FirstPositionListener : LockScrollListener() {

   init {
       lockPosition = 0
       firstScrollAnim = true
       scrollAnim = true
       force = true
       firstForce = true
   }

   override fun attach(view: View) {
       super.attach(view)
       view.viewTreeObserver.addOnDrawListener(this)
   }

   override fun detach() {
       super.detach()
       attachView?.viewTreeObserver?.removeOnDrawListener(this)
   }

   override fun onScrollTrigger() {
       super.onScrollTrigger()
       if (isLockTimeout() || lockDuration == -1L) {
           detach()
       }
   }
}

private interface IAttachListener {
   fun attach(view: View)

   fun detach()
}

//滾動參數
internal data class ScrollParams(
   var scrollPosition: Int = RecyclerView.NO_POSITION,
   var scrollType: Int = SCROLL_TYPE_NORMAL,
   var scrollAnim: Boolean = true,
   var scrollOffset: Int = 0
)

使用方法

複製源碼到工程即可, 就一個類文件.

1.初始化

val scrollHelper = ScrollHelper()
scrollHelper.attach(recyclerView)

2.操作方法

每次觸發滾動時, 可配置的參數:

/**觸發滾動是否伴隨了adapter的addItem*/
var isFromAddItem = false
/**滾動是否需要動畫*/
var isScrollAnim = false
/**滾動類別*/
var scrollType = SCROLL_TYPE_NORMAL
/**額外的偏移距離*/
var scrollOffset: Int = 0


/**滾動類別: 默認不特殊處理. 滾動到item顯示了就完事*/
const val SCROLL_TYPE_NORMAL = 0
/**滾動類別: 將item滾動到第一個位置*/
const val SCROLL_TYPE_TOP = 1
/**滾動類別: 將item滾動到最後一個位置*/
const val SCROLL_TYPE_BOTTOM = 2
/**滾動類別: 將item滾動到居中位置*/
const val SCROLL_TYPE_CENTER = 3
//滾動到指定位置
ScrollHelper#scroll(position)

//滾動到底部
ScrollHelper#scrollToLast()

//滾動到頂部
ScrollHelper#scrollToFirst()

3.鎖定滾動

鎖定滾動配置參數:

/**激活滾動動畫*/
var scrollAnim: Boolean = true
/**激活第一個滾動的動畫*/
var firstScrollAnim: Boolean = false

/**不檢查界面 情況, 強制滾動到最後的位置. 關閉後. 會智能判斷*/
var force: Boolean = false

/**第一次時, 是否強制滾動*/
var firstForce: Boolean = true

/**滾動閾值, 倒數第幾個可見時, 就允許滾動*/
var scrollThreshold = 2

/**鎖定需要滾動的position, -1就是最後一個*/
var lockPosition = RecyclerView.NO_POSITION

/**是否激活功能*/
var enableLock = true

/**鎖定時長, 毫秒*/
var lockDuration: Long = -1
//鎖定滾動
ScrollHelper#lockPosition()

磚廠地址


羣內有各(pian)種(ni)各(jin)樣(qun)的大佬,等你來撩.

聯繫作者

點此快速加羣

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

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

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