整頁滑動的 RecyclerView

本文章對應的示例已上傳到 GitHub點擊這裏查看java

需求描述

咱們在開發一個列表的時候,有時候會須要實現列表整頁滑動的效果。列表的實現你們應該都會使用 RecyclerView ,但 RecyclerView 原生是不支持整頁滑動的。最近 RecyclerView 添加了 SnapHelper 的 API,它是用來幫助實現 ItemView 的對齊,SDK 默認實現了 LinearSnapHelperPagerSnapHelper ,分別用來實現居中對齊和每次滑動一個 ItemView 的效果。git

咱們就藉助 SnapHelper 的原理來實現一個能夠整頁滑動的 RecyclerView 。效果以下所示:github

SnapHelper 介紹

SnapHelper 是用來幫助對齊 ItemView 的,繼承 SnapHelper 咱們須要實現三個方法,分別是app

View findSnapView(RecyclerView.LayoutManager layoutManager) 複製代碼

找到須要對齊的 ItemView ,咱們這裏稱爲 snapViewide

int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView)
複製代碼

計算 snapView 到要對齊的位置之間的距離。ui

int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) 複製代碼

找到要對齊的 ItemViewAdapter 裏面的 positionthis

如上圖所示,假設咱們每次須要滑動一頁:snapView 就是須要對齊的 ItemView ,對應 findSnapView() 的返回值;SnapViewAdapter 中的 position 就是須要對齊的位置,對應 findTargetSnapPosition() 的返回值;snap distance 就是對齊須要滑動的距離,對應 calculateDistanceToFinalSnap() 的返回值。spa

PagerSnapHelper 實現

咱們要實現一個能夠整頁滑動的 SnapHelper3d

  • 首先咱們須要找到須要對齊的 ItemView
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
        if (mFlung) {
            resetCurrentScrolled()
            mFlung = false
            return null
        }
        if (layoutManager == null) return null
        // 首先找到須要對齊的 ItemView 在 Adapter 中的位置
        val targetPosition = getTargetPosition()
        println("$TAG findSnapView, pos: $targetPosition")
        if (targetPosition == RecyclerView.NO_POSITION) return null
        // 正常狀況下,咱們在這裏經過 layoutManager.findViewByPosition(int pos) 返回 view 便可,但會存在兩個問題:
        // 1. 若是這個位置的 view 尚未 layout 的話,會返回 null,達不到對齊的效果;
        // 2. 即便 view 不爲空,但滑動速度會不一致,後面會講到;
        // 因此在這裏,咱們把 position 傳遞給 LinearSmoothScroller,讓它幫助咱們滑動到指定位置。
        layoutManager.startSmoothScroll(createScroller(layoutManager).apply {
            this?.targetPosition = targetPosition
        })
        return null
    }
複製代碼

LinearSmoothScroller 能夠設置一個 targetPosition ,而後調用 layoutManager.startSmoothScroll(LinearSmoothScroller scroller) ,它會幫助咱們自動把 targetPosition 對應的 ItemView 對齊到邊界,默認是左對齊,和咱們需求一致。code

private fun getTargetPosition(): Int {
        println("$TAG getTargetPosition, mScrolledX: $mScrolledX, mCurrentScrolledX: $mCurrentScrolledX")
        val page = when {
            mCurrentScrolledX > 0 -> mScrolledX / mRecyclerViewWidth + 1
            mCurrentScrolledX < 0 -> mScrolledX / mRecyclerViewWidth
            else -> RecyclerView.NO_POSITION
        }
        resetCurrentScrolled()
        return (if (page == RecyclerView.NO_POSITION) RecyclerView.NO_POSITION else page * itemCount)
    }
複製代碼

getTargetPosition() 就是根據 RecyclerView 滑動的距離和方向,找出滑動一頁後,須要對齊的 ItemView 的位置。

private val mScrollListener = object : RecyclerView.OnScrollListener() {
        private var scrolledByUser = false

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            if (newState == RecyclerView.SCROLL_STATE_DRAGGING) scrolledByUser = true
            if (newState == RecyclerView.SCROLL_STATE_IDLE && scrolledByUser) {
                scrolledByUser = false
            }
        }

        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            mScrolledX += dx
            mScrolledY += dy
            if (scrolledByUser) {
                mCurrentScrolledX += dx
                mCurrentScrolledY += dy
            }
        }
    }
複製代碼

mScrolledXmScrolledY 就是 RecyclerView 滑動的總距離,mCurrentScrolledXmCurrentScrolledY 就是 RecyclerView 本次滑動的距離,用來判斷 RecyclerView 滑動的方向。

  • 找出須要對齊的 ItemViewAdapter 中的位置:
override fun findTargetSnapPosition( layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int ): Int {
        val targetPosition = getTargetPosition()
        mFlung = targetPosition != RecyclerView.NO_POSITION
        println("$TAG findTargetSnapPosition, pos: $targetPosition")
        return targetPosition
    }
複製代碼

很簡單,就是 getTargetPosition() 返回的值。解釋一點,findTargetSnapPosition() 方法只有在 RecyclerView 觸發 fling 的時候纔會調用。SnapHelper 內部也是使用的 LinearSmoothScroller 實現的滑動,設置的 targetPosition 就是 findTargetSnapPosition() 的返回值。這也解釋了咱們爲何不在 findSnapView() 方法中直接返回 snapView ,就是爲了保持滑動速度的一致。

  • 計算須要滑動的距離
override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? {
        val out = IntArray(2)
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager))
            out[1] = 0
        } else if (layoutManager.canScrollVertically()) {
            out[0] = 0
            out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager))
        }
        return out
    }
複製代碼
private fun distanceToStart(targetView: View, orientationHelper: OrientationHelper): Int {
        return orientationHelper.getDecoratedStart(targetView) - orientationHelper.startAfterPadding
    }
複製代碼

解釋一點,OrientationHelper 能夠很方便地幫助咱們計算 ItemView 的位置。

本文章對應的示例已上傳到 GitHub點擊這裏查看

相關文章
相關標籤/搜索