背景
可以使用以下方法将RecyclerView捕捉到其中心:
LinearSnapHelper().attachToRecyclerView(recyclerView)
例:
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
recyclerView.adapter = object : RecyclerView.Adapter() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView = holder.itemView as TextView
textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
textView.text = position.toString()
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
val cellSize = recyclerView.width / 3
view.layoutParams.height = cellSize
view.layoutParams.width = cellSize
view.gravity = Gravity.CENTER
return object : RecyclerView.ViewHolder(view) {}
}
}
LinearSnapHelper().attachToRecyclerView(recyclerView)
}
}
activity_main.xml中
android:id="@+id/recyclerView" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
它也可以将其捕捉到其他方面,就像在某些库中所做的那样,例如here.
还有一些库允许使用可以像ViewPager一样工作的RecyclerView,例如here.
问题
假设我有一个包含许多项目的RecyclerView(在我的情况下为水平),我希望它将每个X项目(X是常量)视为一个单元,并对齐每个单元.
例如,如果我滚动一下,它可以捕捉到0项或X项,但不能捕捉它们之间的某些内容.
在某种程度上,它的行为与普通ViewPager的情况类似,只是每个页面中都有X个项目.
例如,如果我们从上面编写的示例代码继续,假设X == 3,则捕捉将来自此空闲状态:
到这个空闲状态(如果我们滚动得足够多,否则将保持在以前的状态):
更多的投掷或滚动应该像在ViewPager上一样处理,就像我上面提到的库一样.
向下一个捕捉点滚动更多(朝同一方向)将是到达项目“6”,“9”,依此类推……
我尝试了什么
我试图搜索替代库,我也尝试阅读有关此问题的文档,但我没有找到任何可能有用的文档.
也许可以通过使用ViewPager,但我认为这不是最好的方式,因为ViewPager不能很好地回收它的项目,我认为它在如何捕捉方面不如RecyclerView灵活.
问题
>是否可以将RecyclerView设置为捕捉每个X项目,将每个X项目视为单个页面以捕捉到?
当然,这些物品将为整个RecyclerView提供足够的空间,均匀分布.
>假设有可能,当RecyclerView即将捕捉到某个项目(包括拥有此项目)之前,如何捕获它之前我将如何获得回调?我问这个是因为它与我问here的问题有关.
Kotlin解决方案
基于“Cheticamp”答案(here)的工作Kotlin解决方案,无需验证您是否具有RecyclerView大小,并且在示例中选择了网格而不是列表:
MainActivity.kt
class MainActivity : AppCompatActivity() {
val USE_GRID = false
// val USE_GRID = true
val ITEMS_PER_PAGE = 4
var selectedItemPos = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val inflater = LayoutInflater.from(this)
recyclerView.adapter = object : RecyclerView.Adapter() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView = holder.itemView as TextView
textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
textView.text = if (selectedItemPos == position) "selected: $position" else position.toString()
}
override fun getItemCount(): Int {
return 100
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
view.layoutParams.width = if (USE_GRID)
recyclerView.width / (ITEMS_PER_PAGE / 2)
else
recyclerView.width / 4
view.layoutParams.height = recyclerView.height / (ITEMS_PER_PAGE / 2)
view.gravity = Gravity.CENTER
return object : RecyclerView.ViewHolder(view) {
}
}
}
recyclerView.layoutManager = if (USE_GRID)
GridLayoutManager(this, ITEMS_PER_PAGE / 2, GridLayoutManager.HORIZONTAL, false)
else
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
val snapToBlock = SnapToBlock(recyclerView, ITEMS_PER_PAGE)
snapToBlock.attachToRecyclerView(recyclerView)
snapToBlock.setSnapBlockCallback(object : SnapToBlock.SnapBlockCallback {
override fun onBlockSnap(snapPosition: Int) {
if (selectedItemPos == snapPosition)
return
selectedItemPos = snapPosition
recyclerView.adapter.notifyDataSetChanged()
}
override fun onBlockSnapped(snapPosition: Int) {
if (selectedItemPos == snapPosition)
return
selectedItemPos = snapPosition
recyclerView.adapter.notifyDataSetChanged()
}
})
}
}
SnapToBlock.kt
/**@param maxFlingBlocks Maxim blocks to move during most vigorous fling*/
class SnapToBlock constructor(private val maxFlingBlocks: Int) : SnapHelper() {
private var recyclerView: RecyclerView? = null
// Total number of items in a block of view in the RecyclerView
private var blocksize: Int = 0
// Maximum number of positions to move on a fling.
private var maxPositionsToMove: Int = 0
// Width of a RecyclerView item if orientation is horizonal; height of the item if vertical
private var itemDimension: Int = 0
// Callback interface when blocks are snapped.
private var snapBlockCallback: SnapBlockCallback? = null
// When snapping, used to determine direction of snap.
private var priorFirstPosition = RecyclerView.NO_POSITION
// Our private scroller
private var scroller: Scroller? = null
// Horizontal/vertical layout helper
private var orientationHelper: OrientationHelper? = null
// LTR/RTL helper
private var layoutDirectionHelper: LayoutDirectionHelper? = null
@Throws(IllegalStateException::class)
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
if (recyclerView != null) {
this.recyclerView = recyclerView
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
orientationHelper = when {
layoutManager.canScrollHorizontally() -> OrientationHelper.createHorizontalHelper(layoutManager)
layoutManager.canScrollVertically() -> OrientationHelper.createVerticalHelper(layoutManager)
else -> throw IllegalStateException("RecyclerView must be scrollable")
}
scroller = Scroller(this.recyclerView!!.context, sInterpolator)
initItemDimensionIfNeeded(layoutManager)
}
super.attachToRecyclerView(recyclerView)
}
// Called when the target view is available and we need to know how much more
// to scroll to get it lined up with the side of the RecyclerView.
override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray {
val out = IntArray(2)
initLayoutDirectionHelperIfNeeded(layoutManager)
if (layoutManager.canScrollHorizontally())
out[0] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
if (layoutManager.canScrollVertically())
out[1] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
if (snapBlockCallback != null)
if (out[0] == 0 && out[1] == 0)
snapBlockCallback!!.onBlockSnapped(layoutManager.getPosition(targetView))
else
snapBlockCallback!!.onBlockSnap(layoutManager.getPosition(targetView))
return out
}
private fun initLayoutDirectionHelperIfNeeded(layoutManager: RecyclerView.LayoutManager) {
if (layoutDirectionHelper == null)
if (layoutManager.canScrollHorizontally())
layoutDirectionHelper = LayoutDirectionHelper()
else if (layoutManager.canScrollVertically())
// RTL doesn't matter for vertical scrolling for this class.
layoutDirectionHelper = LayoutDirectionHelper(false)
}
// We are flinging and need to know where we are heading.
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
initLayoutDirectionHelperIfNeeded(layoutManager)
val lm = layoutManager as LinearLayoutManager
initItemDimensionIfNeeded(layoutManager)
scroller!!.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE)
return when {
velocityX != 0 -> layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalX, itemDimension)
else -> if (velocityY != 0)
layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalY, itemDimension)
else RecyclerView.NO_POSITION
}
}
// We have scrolled to the neighborhood where we will snap. Determine the snap position.
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
// Snap to a view that is either 1) toward the bottom of the data and therefore on screen,
// or, 2) toward the top of the data and may be off-screen.
val snapPos = calcTargetPosition(layoutManager as LinearLayoutManager)
val snapView = if (snapPos == RecyclerView.NO_POSITION)
null
else
layoutManager.findViewByPosition(snapPos)
if (snapView == null)
Log.d(TAG, "<<<
Log.d(TAG, "<<<
}
}
更新
即使我已将an answer标记为已接受,因为它工作正常,我注意到它存在严重问题:
>平滑滚动似乎不能正常工作(不滚动到正确的位置).只滚动该工作是这样的(但具有“拖尾”效果):
(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)
>当切换到RTL(从右到左)语言环境,例如希伯来语(“?????”)时,它根本不让我滚动.
>我注意到onCreateViewHolder被大量调用.事实上,每次滚动时都会调用它,即使有时它应该回收ViewHolders.这意味着有过多的视图创建,也可能意味着存在内存泄漏.
我试图自己解决这些问题,但到目前为止失败了.
如果有人知道如何修复它,我将授予额外的新奖励
更新:当我们修复了RTL / LTR时,我在这篇文章中更新了Kotlin解决方案.
更新:关于第3点,这似乎是因为recyclelerView有一个视图池,它会很快被填充.为了解决这个问题,我们可以通过对我们在其中使用的每种视图类型使用recyclelerView.getRecycledViewPool().setMaxRecycledViews(viewType,Integer.MAX_VALUE)来简单地扩大池大小.真的需要这个奇怪的事情.我已经将它发布到谷歌(here和here),但被拒绝,默认情况下该池应该是无限制的.最后,我决定至少要求为所有视图类型提供更方便的功能(here).