本文章对应的示例已上传到 GitHub
,点击这里查看java
咱们在开发一个列表的时候,有时候会须要实现列表整页滑动的效果。列表的实现你们应该都会使用 RecyclerView
,但 RecyclerView
原生是不支持整页滑动的。最近 RecyclerView
添加了 SnapHelper
的 API,它是用来帮助实现 ItemView
的对齐,SDK 默认实现了 LinearSnapHelper
和 PagerSnapHelper
,分别用来实现居中对齐和每次滑动一个 ItemView
的效果。git
咱们就借助 SnapHelper
的原理来实现一个能够整页滑动的 RecyclerView
。效果以下所示:github
SnapHelper
介绍SnapHelper
是用来帮助对齐 ItemView
的,继承 SnapHelper
咱们须要实现三个方法,分别是app
View findSnapView(RecyclerView.LayoutManager layoutManager) 复制代码
找到须要对齐的
ItemView
,咱们这里称为snapView
。ide
int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView)
复制代码
计算
snapView
到要对齐的位置之间的距离。ui
int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) 复制代码
找到要对齐的
ItemView
在Adapter
里面的position
。this
如上图所示,假设咱们每次须要滑动一页:snapView
就是须要对齐的 ItemView
,对应 findSnapView()
的返回值;SnapView
在 Adapter
中的 position
就是须要对齐的位置,对应 findTargetSnapPosition()
的返回值;snap distance
就是对齐须要滑动的距离,对应 calculateDistanceToFinalSnap()
的返回值。spa
PagerSnapHelper
实现咱们要实现一个能够整页滑动的 SnapHelper
。3d
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
}
}
}
复制代码
mScrolledX
、mScrolledY
就是RecyclerView
滑动的总距离,mCurrentScrolledX
、mCurrentScrolledY
就是RecyclerView
本次滑动的距离,用来判断RecyclerView
滑动的方向。
ItemView
在 Adapter
中的位置: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
,点击这里查看