本文為 Nick Rout 發(fā)布于 Medium 的文章譯文
原文鏈接為 Detecting snap changes with Android’s RecyclerView SnapHelper
本文僅作為個(gè)人學(xué)習(xí)記錄所用。如有涉及侵權(quán),請(qǐng)相關(guān)人士盡快聯(lián)系譯文作者。
SnapHelper 是 AndroidX RecyclerView 軟件包的重要補(bǔ)充。 簡(jiǎn)而言之,它可用于更改 RecyclerView 的行為,用于輔助 RecyclerView 在滾動(dòng)結(jié)束時(shí)將 Item 對(duì)齊到某個(gè)位置。
目前,基本 SnapHelper 類有兩種標(biāo)準(zhǔn)實(shí)現(xiàn); LinearSnapHelper 和 PagerSnapHelper,它們各自提供的功能略有不同。 兩者都支持水平和垂直方向。
LinearSnapHelper 適用于較小的項(xiàng)目,并將目標(biāo)子視圖的中心對(duì)齊到 RecyclerView 的中心:

PagerSnapHelper適用于全屏項(xiàng)目,其行為類似于ViewPager:

使用這些類的API非常簡(jiǎn)單:
val snapHelper = LinearSnapHelper() // Or PagerSnapHelper
snapHelper.attachToRecyclerView(recyclerView)
缺少API的情況????♀?
如果我們想知道捕捉位置何時(shí)更改該怎么辦? 例如,也許我們正在使用 PagerSnapHelper,并且想要顯示一個(gè)頁(yè)面指示器。
不幸的是,在撰寫本文時(shí),尚不存在此類 API 。 對(duì)于這樣的回調(diào),甚至存在一個(gè)開放的問(wèn)題,已經(jīng)存在了一段時(shí)間。
我們將如何實(shí)施呢? SnapHelper 類很復(fù)雜且不是非常模塊化,因此擴(kuò)展它們(或編寫新的子類)將很痛苦。 幸運(yùn)的是,我們可以利用現(xiàn)有的 RecyclerView 類和一些 Kotlin 魔術(shù)來(lái)實(shí)現(xiàn)這一目標(biāo)。
查找當(dāng)前的捕捉位置??
我們需要的第一件事是確定當(dāng)前捕捉位置的方法。 同樣,目前尚不存在此類 SnapHelper 函數(shù),我們將必須自行實(shí)現(xiàn)此功能。
SnapHelper 提供的功能是一種查找當(dāng)前快照視圖的方法。 我們必須傳遞 SnapHelper 附加到的 RecyclerView 使用的 LayoutManager:
val layoutManager = recyclerView.layoutManager
val snapView = snapHelper.findSnapView(layoutManager)
然后,我們可以使用此 LayoutManager 來(lái)確定此 View 的位置:
val snapPosition = layoutManager.getPosition(snapView)
我們可以為 Kotlin 擴(kuò)展函數(shù)中的可重用性而巧妙地將其包裝起來(lái),同時(shí)還要考慮到一些可為空性方面:
package com.nickrout.snaphelperlistener
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SnapHelper
fun SnapHelper.getSnapPosition(recyclerView: RecyclerView): Int {
val layoutManager = recyclerView.layoutManager ?: return RecyclerView.NO_POSITION
val snapView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
return layoutManager.getPosition(snapView)
}
監(jiān)聽(tīng)捕捉位置變化??
在深入研究如何確定捕捉位置變化之前,我們先定義一個(gè)簡(jiǎn)單的回調(diào)接口:
package com.nickrout.snaphelperlistener
interface OnSnapPositionChangeListener {
fun onSnapPositionChange(position: Int)
}
確定捕捉位置變化
我們知道,捕捉位置只會(huì)在滾動(dòng)過(guò)程中改變。 因此,為了確定更改,我們將結(jié)合先前定義的getSnapPosition 函數(shù)和 OnScrollListener 的自定義子類。 重要的是要注意,我們僅想知道捕捉位置何時(shí)發(fā)生變化,因此我們的類需要保留對(duì)最后一個(gè)已知位置的引用,以便僅在此位置不同時(shí)才觸發(fā)回調(diào)。 關(guān)鍵功能:
private var snapPosition = RecyclerView.NO_POSITION
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
maybeNotifySnapPositionChange(recyclerView)
}
private fun maybeNotifySnapPositionChange(recyclerView: RecyclerView) {
val snapPosition = snapHelper.getSnapPosition(recyclerView)
val snapPositionChanged = this.snapPosition != snapPosition
if (snapPositionChanged) {
onSnapPositionChangeListener
.onSnapPositionChange(snapPosition)
this.snapPosition = snapPosition
}
}

添加選項(xiàng)以在滾動(dòng)完成時(shí)通知
上面的實(shí)現(xiàn)將在滾動(dòng)事件期間將所有捕捉位置更改通知我們,特別是在使用 LinearSnapHelper 時(shí)。 也許我們只想知道最終的捕捉位置是什么(即,當(dāng)滾動(dòng)狀態(tài)變?yōu)榭臻e狀態(tài)時(shí))?
首先,讓我們定義一個(gè)枚舉類來(lái)指定以下兩個(gè)選項(xiàng):
enum class Behavior {
NOTIFY_ON_SCROLL,
NOTIFY_ON_SCROLL_STATE_IDLE
}
然后,我們使用第二個(gè) OnScrollListener 回調(diào)來(lái)實(shí)現(xiàn)此目的:
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
maybeNotifySnapPositionChange(recyclerView)
}
}

最終課程
我們的最終課程融合了上述所有功能,同時(shí)還使用了可空性和默認(rèn)參數(shù):
package com.nickrout.snaphelperlistener
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SnapHelper
class SnapOnScrollListener(
private val snapHelper: SnapHelper,
var behavior: Behavior = Behavior.NOTIFY_ON_SCROLL,
var onSnapPositionChangeListener: OnSnapPositionChangeListener? = null
) : RecyclerView.OnScrollListener() {
enum class Behavior {
NOTIFY_ON_SCROLL,
NOTIFY_ON_SCROLL_STATE_IDLE
}
private var snapPosition = RecyclerView.NO_POSITION
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (behavior == Behavior.NOTIFY_ON_SCROLL) {
maybeNotifySnapPositionChange(recyclerView)
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
maybeNotifySnapPositionChange(recyclerView)
}
}
private fun maybeNotifySnapPositionChange(recyclerView: RecyclerView) {
val snapPosition = snapHelper.getSnapPosition(recyclerView)
val snapPositionChanged = this.snapPosition != snapPosition
if (snapPositionChanged) {
onSnapPositionChangeListener?.onSnapPositionChange(snapPosition)
this.snapPosition = snapPosition
}
}
}
我們將新類與現(xiàn)有的 RecyclerView 和 SnapHelper 連接起來(lái),如下所示:
val snapOnScrollListener = SnapOnScrollListener(snapHelper, behavior, onSnapPositionChangeListener)
recyclerView.addOnScrollListener(snapOnScrollListener)
添加便捷的擴(kuò)展功能??
我們當(dāng)前的實(shí)現(xiàn)效果很好,但是我們可以通過(guò)使用另一個(gè)擴(kuò)展功能來(lái)減少設(shè)置所需的樣板代碼。
我們要確保新的 OnSnapPositionChangeListener 和 SnapOnScrollListener 類的設(shè)置一致。 我們還希望保持“行為”選項(xiàng)可用:
package com.nickrout.snaphelperlistener
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SnapHelper
fun RecyclerView.attachSnapHelperWithListener(
snapHelper: SnapHelper,
behavior: SnapOnScrollListener.Behavior = SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL,
onSnapPositionChangeListener: OnSnapPositionChangeListener) {
snapHelper.attachToRecyclerView(this)
val snapOnScrollListener = SnapOnScrollListener(snapHelper, onSnapPositionChangeListener, behavior)
addOnScrollListener(snapOnScrollListener)
}
現(xiàn)在,我們有一種簡(jiǎn)單的方法可以將 RecyclerView 與 SnapHelper 聯(lián)系在一起,同時(shí)還可以監(jiān)聽(tīng)捕捉位置的變化:
recyclerView.attachSnapHelperWithListener(snapHelper, behavior, onSnapPositionChangeListener)
我希望這篇文章,可以幫助你對(duì) SnapHelper 有更深入的了解。