整页滑动的 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点击这里查看

相关文章
相关标签/搜索