如何捕捉RecyclerView项目,以便每个X项目将被视为一个单元来捕捉?
背景
可以使用以下方法将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中
也可以把它抓到其他的一边,就像在这里的一些图书馆所做的那样。
还有一些库允许有一个像ViewPager一样工作的RecyclerView,比如这里 。
问题
假设我有一个RecyclerView(水平在我的情况下)与许多项目,我希望它会把每个X项目(X是常数)作为一个单位,并捕捉到每个单位。
例如,如果我滚动一下,它可以捕捉到0项或X项,但不会捕捉到它们之间的东西。
在某种程度上,它的行为与一般的ViewPager类似,只是每个页面都有X个项目。
例如,如果我们继续上面写的示例代码,假设X == 3,捕捉将来自这个空闲状态:
到这个空闲状态(如果我们滚动足够的话,否则会保持在以前的状态):
更多的投掷或滚动应该像ViewPager一样处理,就像我上面提到的库一样。
滚动更多(在同一方向)到下一个捕捉点将达到项目“6”,“9”,等等…
我试过了
我试图寻找替代的图书馆,我也尝试阅读关于这个文档,但我没有find任何可能有用的东西。
也许可以通过使用ViewPager,但我认为这不是最好的方式,因为ViewPager不会很好地回收它的项目,我认为它不像RecyclerView那么灵活。
问题
-
是否有可能设置RecyclerView捕捉每个X项目,将每个X项目作为单个页面来捕捉?
当然,这些物品会为整个RecyclerView平均占用足够的空间。
-
假设有可能,当RecyclerView即将捕捉某个项目(包括该项目)之前,如何获得回调? 我问这个,因为这跟我在这里问的同一个问题有关。
Kotlin解决方案
在示例中,基于“Cheticamp”的工作Kotlin解决方案的答案( 此处 )无需validation您具有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, "<<<<findSnapView is returning null!") Log.d(TAG, "<<<= priorFirstPosition) { // Scrolling toward bottom of data val firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition() snapPos = if (firstCompletePosition != RecyclerView.NO_POSITION && firstCompletePosition % blocksize == 0) firstCompletePosition else roundDownToBlockSize(firstVisiblePos + blocksize) } else { // Scrolling toward top of data snapPos = roundDownToBlockSize(firstVisiblePos) // Check to see if target view exists. If it doesn't, force a smooth scroll. // SnapHelper only snaps to existing views and will not scroll to a non-existant one. // If limiting fling to single block, then the following is not needed since the // views are likely to be in the RecyclerView pool. if (layoutManager.findViewByPosition(snapPos) == null) { val toScroll = layoutDirectionHelper!!.calculateDistanceToScroll(layoutManager, snapPos) recyclerView!!.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator) } } priorFirstPosition = firstVisiblePos return snapPos } private fun initItemDimensionIfNeeded(layoutManager: RecyclerView.LayoutManager) { if (itemDimension != 0) return val child = layoutManager.getChildAt(0) ?: return if (layoutManager.canScrollHorizontally()) { itemDimension = child.width blocksize = getSpanCount(layoutManager) * (recyclerView!!.width / itemDimension) } else if (layoutManager.canScrollVertically()) { itemDimension = child.height blocksize = getSpanCount(layoutManager) * (recyclerView!!.height / itemDimension) } maxPositionsToMove = blocksize * maxFlingBlocks } private fun getSpanCount(layoutManager: RecyclerView.LayoutManager): Int = (layoutManager as? GridLayoutManager)?.spanCount ?: 1 private fun roundDownToBlockSize(trialPosition: Int): Int = trialPosition - trialPosition % blocksize private fun roundUpToBlockSize(trialPosition: Int): Int = roundDownToBlockSize(trialPosition + blocksize - 1) override fun createScroller(layoutManager: RecyclerView.LayoutManager): LinearSmoothScroller? { return if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) null else object : LinearSmoothScroller(recyclerView!!.context) { override fun onTargetFound(targetView: View, state: RecyclerView.State?, action: RecyclerView.SmoothScroller.Action) { val snapDistances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager, targetView) val dx = snapDistances[0] val dy = snapDistances[1] val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))) if (time > 0) action.update(dx, dy, time, sInterpolator) } override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float = MILLISECONDS_PER_INCH / displayMetrics.densityDpi } } fun setSnapBlockCallback(callback: SnapBlockCallback?) { snapBlockCallback = callback } /* Helper class that handles calculations for LTR and RTL layouts. */ private inner class LayoutDirectionHelper { // Is the layout an RTL one? private val mIsRTL: Boolean constructor() { mIsRTL = ViewCompat.getLayoutDirection(recyclerView) == ViewCompat.LAYOUT_DIRECTION_RTL } constructor(isRTL: Boolean) { mIsRTL = isRTL } /* Calculate the amount of scroll needed to align the target view with the layout edge. */ fun getScrollToAlignView(targetView: View): Int = if (mIsRTL) orientationHelper!!.getDecoratedEnd(targetView) - recyclerView!!.width else orientationHelper!!.getDecoratedStart(targetView) /** * Calculate the distance to final snap position when the view corresponding to the snap * position is not currently available. * * @param layoutManager LinearLayoutManager or descendent class * @param targetPos - Adapter position to snap to * @return int[2] {x-distance in pixels, y-distance in pixels} */ fun calculateDistanceToScroll(layoutManager: LinearLayoutManager, targetPos: Int): IntArray { val out = IntArray(2) val firstVisiblePos = layoutManager.findFirstVisibleItemPosition() if (layoutManager.canScrollHorizontally()) { if (targetPos <= firstVisiblePos) // scrolling toward top of data if (mIsRTL) { val lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition()) out[0] = orientationHelper!!.getDecoratedEnd(lastView) + (firstVisiblePos - targetPos) * itemDimension } else { val firstView = layoutManager.findViewByPosition(firstVisiblePos) out[0] = orientationHelper!!.getDecoratedStart(firstView) - (firstVisiblePos - targetPos) * itemDimension } } if (layoutManager.canScrollVertically() && targetPos <= firstVisiblePos) { // scrolling toward top of data val firstView = layoutManager.findViewByPosition(firstVisiblePos) out[1] = firstView.top - (firstVisiblePos - targetPos) * itemDimension } return out } /* Calculate the number of positions to move in the RecyclerView given a scroll amount and the size of the items to be scrolled. Return integral multiple of mBlockSize not equal to zero. */ fun getPositionsToMove(llm: LinearLayoutManager, scroll: Int, itemSize: Int): Int { var positionsToMove: Int positionsToMove = roundUpToBlockSize(Math.abs(scroll) / itemSize) if (positionsToMove maxPositionsToMove) // Clamp number of positions to move so we don't get wild flinging. positionsToMove = maxPositionsToMove if (scroll < 0) positionsToMove *= -1 if (mIsRTL) positionsToMove *= -1 return if (layoutDirectionHelper!!.isDirectionToBottom(scroll var t = input // _o(t) = t * t * ((tension + 1) * t + tension) // o(t) = _o(t - 1) + 1 t -= 1.0f t * t * t + 1.0f } private val MILLISECONDS_PER_INCH = 100f private val TAG = "SnapToBlock" } }
更新
尽管我已经标记了一个被接受的答案 ,因为它工作正常,我注意到它有严重的问题:
-
平滑滚动似乎不能正常工作(不滚动到正确的地方)。 只有滚动这项工作是这样的(但“拖影”效果):
(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)
-
当切换到RTL(从右到左)的语言环境,如希伯来语(“עברית”),它不让我滚动。
-
我注意到
onCreateViewHolder
被称为很多。 实际上,每次我滚动时都会调用它,即使是它应该回收ViewHolders的时间。 这意味着有过多的视图创建,也可能意味着有内存泄漏。
我试图解决这些问题,但迄今为止失败了。
如果这里有人知道如何解决这个问题,我会给予额外的新奖金
更新:因为我们修复了RTL / LTR,所以我在这篇文章中更新了Kotlin解决方案。
SnapHelper
为你正在尝试的东西提供必要的框架,但是它需要扩展来处理视图块。 下面的类SnapToBlock
扩展了SnapHelper
以捕捉视图块。 在这个例子中,我已经使用了四个视图,但可以更多或更少。
更新:代码已经改变,以适应GridLayoutManager
以及LinearLayoutManager
。 现在禁止投掷,所以对齐工作更多的列表一个ViewPager
。 现在支持水平和垂直滚动以及LTR和RTL布局。
更新:更改平滑滚动插值器更像ViewPager
。
更新:添加回调前/后贴紧。
更新:添加对RTL布局的支持。
以下是示例应用程序的快速video:
设置布局管理器如下:
// For LinearLayoutManager horizontal orientation recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)); // For GridLayoutManager vertical orientation recyclerView.setLayoutManager(new GridLayoutManager(this, SPAN_COUNT, RecyclerView.VERTICAL, false));
添加以下内容将SnapToBlock
附加到RecyclerView
。
SnapToBlock snapToBlock = new SnapToBlock(mMaxFlingPages); snapToBlock.attachToRecyclerView(recyclerView);
mMaxFlingPages
是允许同时抛出的最大块数(rowsCols *跨度)。
对于即将拍摄并完成拍摄时的回叫,请添加以下内容:
snapToBlock.setSnapBlockCallback(new SnapToBlock.SnapBlockCallback() { @Override public void onBlockSnap(int snapPosition) { ... } @Override public void onBlockSnapped(int snapPosition) { ... } });
SnapToBlock.java
/* The number of items in the RecyclerView should be a multiple of block size; otherwise, the extra item views will not be positioned on a block boundary when the end of the data is reached. Pad out with empty item views if needed. Updated to accommodate RTL layouts. */ public class SnapToBlock extends SnapHelper { private RecyclerView mRecyclerView; // Total number of items in a block of view in the RecyclerView private int mBlocksize; // Maximum number of positions to move on a fling. private int mMaxPositionsToMove; // Width of a RecyclerView item if orientation is horizonal; height of the item if vertical private int mItemDimension; // Maxim blocks to move during most vigorous fling. private final int mMaxFlingBlocks; // Callback interface when blocks are snapped. private SnapBlockCallback mSnapBlockCallback; // When snapping, used to determine direction of snap. private int mPriorFirstPosition = RecyclerView.NO_POSITION; // Our private scroller private Scroller mScroller; // Horizontal/vertical layout helper private OrientationHelper mOrientationHelper; // LTR/RTL helper private LayoutDirectionHelper mLayoutDirectionHelper; // Borrowed from ViewPager.java private static final Interpolator sInterpolator = new Interpolator() { public float getInterpolation(float t) { // _o(t) = t * t * ((tension + 1) * t + tension) // o(t) = _o(t - 1) + 1 t -= 1.0f; return t * t * t + 1.0f; } }; SnapToBlock(int maxFlingBlocks) { super(); mMaxFlingBlocks = maxFlingBlocks; } @Override public void attachToRecyclerView(@Nullable final RecyclerView recyclerView) throws IllegalStateException { if (recyclerView != null) { mRecyclerView = recyclerView; final LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); if (layoutManager.canScrollHorizontally()) { mOrientationHelper = OrientationHelper.createHorizontalHelper(layoutManager); mLayoutDirectionHelper = new LayoutDirectionHelper(ViewCompat.getLayoutDirection(mRecyclerView)); } else if (layoutManager.canScrollVertically()) { mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager); // RTL doesn't matter for vertical scrolling for this class. mLayoutDirectionHelper = new LayoutDirectionHelper(RecyclerView.LAYOUT_DIRECTION_LTR); } else { throw new IllegalStateException("RecyclerView must be scrollable"); } mScroller = new Scroller(mRecyclerView.getContext(), 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. @NonNull @Override public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) { int[] out = new int[2]; if (layoutManager.canScrollHorizontally()) { out[0] = mLayoutDirectionHelper.getScrollToAlignView(targetView); } if (layoutManager.canScrollVertically()) { out[1] = mLayoutDirectionHelper.getScrollToAlignView(targetView); } if (mSnapBlockCallback != null) { if (out[0] == 0 && out[1] == 0) { mSnapBlockCallback.onBlockSnapped(layoutManager.getPosition(targetView)); } else { mSnapBlockCallback.onBlockSnap(layoutManager.getPosition(targetView)); } } return out; } // We are flinging and need to know where we are heading. @Override public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) { LinearLayoutManager lm = (LinearLayoutManager) layoutManager; initItemDimensionIfNeeded(layoutManager); mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); if (velocityX != 0) { return mLayoutDirectionHelper .getPositionsToMove(lm, mScroller.getFinalX(), mItemDimension); } if (velocityY != 0) { return mLayoutDirectionHelper .getPositionsToMove(lm, mScroller.getFinalY(), mItemDimension); } return RecyclerView.NO_POSITION; } // We have scrolled to the neighborhood where we will snap. Determine the snap position. @Override public View findSnapView(RecyclerView.LayoutManager layoutManager) { // 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. int snapPos = calcTargetPosition((LinearLayoutManager) layoutManager); View snapView = (snapPos == RecyclerView.NO_POSITION) ? null : layoutManager.findViewByPosition(snapPos); if (snapView == null) { Log.d(TAG, "<<<
上面定义的SnapBlockCallback
接口可用于报告要捕捉的块的开始处的视图的适配器位置。 如果视图不在屏幕上,则在与该位置关联的视图可能不会被实例化。
I would do something like that
-
Block scrolling inside RecyclerView (eg How to disable RecyclerView scrolling? )
-
Create Gesture Fling Detecor and attach it to RecyclerView
- Inside Gesture Detector detect fling events events
- On Fling event, detect side (left right)
- Scroll RecyclerView to position (first Visible item + your const * (left?-1:1))
should work 🙂
- 如何在Android studio中随机生成一个字符串和颜色的字符串
- 如何添加RecyclerView依赖 – gradle 3
- 如何将快门效果应用于TextureView Camera2预览
- 毕加索不容忍空的字符串网址?
- 如何在回收视图中为自定义ViewHolder实现OnClickListener?
- Android Studio – 获取活动视图实例的最佳方式
- kotlin.TypeCastException:null不能转换为非null类型android.support.v7.widget.Toolbar
- Gradle安装错误:Javadoc
- 如何设置支持库零食栏文本颜色的东西比android:textColor?