本文章對應的示例已上傳到 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
,點擊這裏查看