如何捕捉RecyclerView项,以便将每个X项视为要捕捉的单个单元? [英] How to snap RecyclerView items so that every X items would be considered like a single unit to snap to?

查看:63
本文介绍了如何捕捉RecyclerView项,以便将每个X项视为要捕捉的单个单元?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

可以使用以下命令将RecyclerView捕捉到其中心:

It's possible to snap a RecyclerView to its center using :

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<RecyclerView.ViewHolder>() {
            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.support.v7.widget.RecyclerView
    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"/>

也可以将其捕捉到其他方面,例如在某些库中所做的操作,例如 此处 .

It's also possible to snap it to other sides, as was done in some libraries, such as here.

也有一些库允许具有可以像ViewPager一样工作的RecyclerView,例如 此处 .

There are also libraries that allow to have a RecyclerView that can work like a ViewPager, such as here.

假设我有一个RecyclerView(在我的情况下为水平),其中包含许多物品,并且我希望它将每X个物品(X为常数)视为一个单元,并捕捉到每个这些单元.

Supposed I have a RecyclerView (horizontal in my case) with many items, and I want that it will treat every X items (X is constant) as a single unit, and snap to each of those units.

例如,如果我滚动一点,它可能会捕捉到0项或X项,但不会捕捉到它们之间的某个东西.

For example, if I scroll a bit, it could snap to either the 0-item, or the X-item, but not to something in between them.

在某种程度上,它的行为与普通ViewPager的行为类似,只是每个页面中将包含X个项目.

In a way, it's similar in its behavior to a case of a normal ViewPager, just that each page would have X items in it.

例如,如果我们继续我上面编写的示例代码(假设X == 3),则捕捉将从该空闲状态开始:

For example, if we continue from the sample code I wrote above,suppose X==3 , the snapping would be from this idle state:

到此空闲状态(以防我们滚动足够,否则将保持先前的状态):

to this idle state (in case we scrolled enough, otherwise would stay in previous state) :

像我上面提到的库一样,应该像在ViewPager上一样处理更多的滚动或滚动.

Flinging or scrolling more should be handled like on ViewPager, just like the library I've mentioned above.

更多地(沿相同方向)滚动到下一个捕捉点将是到达项目"6". ,"9"等...

Scrolling more (in the same direction) to the next snapping point would be to reach item "6" , "9", and so on...

我尝试搜索替代库,也尝试阅读有关此文档,但是我没有发现任何有用的东西.

I tried to search for alternative libraries, and I also tried to read the docs regarding this, but I didn't find anything that might be useful.

使用ViewPager也有可能,但是我认为这不是最好的方法,因为ViewPager不能很好地回收其项目,而且我认为它在捕获方面不如RecyclerView灵活.

It might also be possible by using a ViewPager, but I think that's not the best way, because ViewPager doesn't recycle its items well, and I think it's less flexible than RecyclerView in terms of how to snap.

  1. 是否可以将RecyclerView设置为捕捉每X个项目,并将每个X项视为要捕捉到的单个页面?

  1. Is it possible to set RecyclerView to snap every X items, to treat each X items as a single page to snap to?

当然,这些物品将平均为整个RecyclerView占用足够的空间.

Of course, the items will take enough space for the whole RecyclerView, evenly.

假设有可能,当RecyclerView将要捕捉到某个项目(包括拥有该项目)之前,我如何获得回调?我之所以这样问,是因为它与我问 此处 的同一问题有关.

Supposed it is possible, how would I get a callback when the RecyclerView is about to snap to a certain item, including having this item, before it got snapped? I ask this because it's related to the same question I asked here.


科特林溶液

·基于"Cheticamp"的有效的Kotlin解决方案.答案( 此处 ),而无需验证您是否拥有RecyclerView大小,并在示例中选择使用网格而不是列表:


Kotlin solution

A working Kotlin solution based on "Cheticamp" answer (here), without the need to verify that you have the RecyclerView size, and with the choice of having a grid instead of a list, in the sample:

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<RecyclerView.ViewHolder>() {
            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, "<<<<findSnapView snapos=" + snapPos)
        return snapView
    }

    // Does the heavy lifting for findSnapView.
    private fun calcTargetPosition(layoutManager: LinearLayoutManager): Int {
        val snapPos: Int
        initLayoutDirectionHelperIfNeeded(layoutManager)
        val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
        if (firstVisiblePos == RecyclerView.NO_POSITION)
            return RecyclerView.NO_POSITION
        initItemDimensionIfNeeded(layoutManager)
        if (firstVisiblePos >= 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 < blocksize)
            // Must move at least one block
                positionsToMove = blocksize
            else 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 < 0)) {
                // Scrolling toward the bottom of data.
                roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove
            } else roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove
            // Scrolling toward the top of the data.
        }

        fun isDirectionToBottom(velocityNegative: Boolean): Boolean = if (mIsRTL) velocityNegative else !velocityNegative
    }

    interface SnapBlockCallback {
        fun onBlockSnap(snapPosition: Int)
        fun onBlockSnapped(snapPosition: Int)
    }

    companion object {
        // Borrowed from ViewPager.java
        private val sInterpolator = Interpolator { input ->
            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"
    }
}


更新

即使我已将 答案 标记为已接受,因为它可以正常工作,我注意到它存在严重问题:


Update

Even though I've marked an answer as accepted, as it works fine, I've noticed it has serious issues:

  1. 平滑滚动似乎无法正常工作(无法滚动到正确的位置).仅滚动工作正常(但具有拖尾"效果):

  1. Smooth scrolling doesn't seem to work fine (doesn't scroll to correct place). Only scrolling that work is as such (but with the "smearing" effect) :

(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)

  • 当切换到诸如希伯来语(עברית")之类的RTL(从右到左)语言环境时,它根本不会让我滚动.

  • When switching to RTL (Right to left) locale such as Hebrew ("עברית"), it doesn't let me scroll at all.

    我注意到onCreateViewHolder被称为很多.实际上,每次滚动时都会调用它,即使它本应回收ViewHolders的次数也是如此.这意味着视图创建过多,也可能意味着内存泄漏.

    I've noticed that onCreateViewHolder is called a lot. In fact it is called every time I scroll, even for times it should have recycled the ViewHolders. This means there is an excessive creation of views, and it might also mean there is a memory leak.

    我已尝试自己修复这些问题,但到目前为止失败了.

    I've tried to fix those myself, but failed so far.

    如果这里有人知道如何解决,我将给予额外的新赏金

    If anyone here knows how to fix it, I will grant the extra, new bounty

    更新:由于我们已获得RTL/LTR的修复程序,因此我在本文中更新了Kotlin解决方案.

    Update: as we got a fix for RTL/LTR, I've updated the Kotlin solution within this post.

    更新:关于#3点,这似乎是因为recyclerView有一个视图池,该视图池太早被填满了.为了解决这个问题,我们可以对其中的每种视图类型使用recyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType, Integer.MAX_VALUE) 来简单地扩大池的大小.这真的是很奇怪的事情.我已将其发布到Google(> 此处 此处 ),但被拒绝,默认情况下该池应为无限制.最后,我决定至少要求对所有视图类型都具有更方便的功能( 这里 ).

    Update: about point #3 , this seems to be because there is a pool of views for the recyclerView, which gets filled too soon. To handle this, we can simply enlarge the pool size, by using recyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType, Integer.MAX_VALUE) for each view type we have in it. Weird thing that this is really needed. I've posted about it to Google (here and here) but was rejected that the pool should be unlimited by default. In the end, I decided to at least request to have a more convinient function to do it for all view types (here).

    推荐答案

    SnapHelper为您要尝试的内容提供了必要的框架,但是需要扩展它以处理视图块.下面的类SnapToBlock扩展了SnapHelper以捕捉到视图块.在该示例中,我对一个块使用了四个视图,但是它可能更多或更少.

    SnapHelper supplies the necessary framework for what you are attempting, but it needs to be extended to handle blocks of views. The class SnapToBlock below extends SnapHelper to snap to blocks of views. In the example, I have used four views to a block but it can be more or less.

    更新:代码已更改,以适应GridLayoutManagerLinearLayoutManager.现在禁止了翻转,因此,在ViewPager中列出更多对齐方法.现在支持水平和垂直滚动以及LTR和RTL布局.

    Update: The code has been change to accommodate GridLayoutManager as well as LinearLayoutManager. Flinging is now inhibited so the snapping works more list a ViewPager. Horizontal and vertical scrolling is now supported as well as LTR and RTL layouts.

    更新:将平滑滚动插值器更改为更类似于ViewPager.

    Update: Changed smooth scroll interpolator to be more like ViewPager.

    更新:为之前/之后的快照添加回调.

    Update: Adding callbacks for pre/post snapping.

    更新:添加对RTL布局的支持.

    Update: Adding support for RTL layouts.

    以下是示例应用的快速视频:

    Here is a quick video of the sample app:

    按如下所示设置布局管理器:

    Set up the layout manager as follows:

    // 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.

    Add the following to attach the SnapToBlock to the RecyclerView.

    SnapToBlock snapToBlock = new SnapToBlock(mMaxFlingPages);
    snapToBlock.attachToRecyclerView(recyclerView);
    

    mMaxFlingPages是一次可以扔掉的最大块数(rowsCol *跨度).

    mMaxFlingPages is the maximum number of blocks (rowsCols * spans) to allow to be flung at one time.

    要在即将完成并已完成快照时回叫,请添加以下内容:

    For call backs when a snap is about to be made and has been completed, add the following:

    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, "<<<<findSnapView is returning null!");
            }
            Log.d(TAG, "<<<<findSnapView snapos=" + snapPos);
            return snapView;
        }
    
        // Does the heavy lifting for findSnapView.
        private int calcTargetPosition(LinearLayoutManager layoutManager) {
            int snapPos;
            int firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
    
            if (firstVisiblePos == RecyclerView.NO_POSITION) {
                return RecyclerView.NO_POSITION;
            }
            initItemDimensionIfNeeded(layoutManager);
            if (firstVisiblePos >= mPriorFirstPosition) {
                // Scrolling toward bottom of data
                int firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition();
                if (firstCompletePosition != RecyclerView.NO_POSITION
                    && firstCompletePosition % mBlocksize == 0) {
                    snapPos = firstCompletePosition;
                } else {
                    snapPos = roundDownToBlockSize(firstVisiblePos + mBlocksize);
                }
            } 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) {
                    int[] toScroll = mLayoutDirectionHelper.calculateDistanceToScroll(layoutManager, snapPos);
                    mRecyclerView.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator);
                }
            }
            mPriorFirstPosition = firstVisiblePos;
    
            return snapPos;
        }
    
        private void initItemDimensionIfNeeded(final RecyclerView.LayoutManager layoutManager) {
            if (mItemDimension != 0) {
                return;
            }
    
            View child;
            if ((child = layoutManager.getChildAt(0)) == null) {
                return;
            }
    
            if (layoutManager.canScrollHorizontally()) {
                mItemDimension = child.getWidth();
                mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getWidth() / mItemDimension);
            } else if (layoutManager.canScrollVertically()) {
                mItemDimension = child.getHeight();
                mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getHeight() / mItemDimension);
            }
            mMaxPositionsToMove = mBlocksize * mMaxFlingBlocks;
        }
    
        private int getSpanCount(RecyclerView.LayoutManager layoutManager) {
            return (layoutManager instanceof GridLayoutManager)
                ? ((GridLayoutManager) layoutManager).getSpanCount()
                : 1;
        }
    
        private int roundDownToBlockSize(int trialPosition) {
            return trialPosition - trialPosition % mBlocksize;
        }
    
        private int roundUpToBlockSize(int trialPosition) {
            return roundDownToBlockSize(trialPosition + mBlocksize - 1);
        }
    
        @Nullable
        protected LinearSmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
            if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
                return null;
            }
            return new LinearSmoothScroller(mRecyclerView.getContext()) {
                @Override
                protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
                    int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                                                                       targetView);
                    final int dx = snapDistances[0];
                    final int dy = snapDistances[1];
                    final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                    if (time > 0) {
                        action.update(dx, dy, time, sInterpolator);
                    }
                }
    
                @Override
                protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                    return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
                }
            };
        }
    
        public void setSnapBlockCallback(@Nullable SnapBlockCallback callback) {
            mSnapBlockCallback = callback;
        }
    
        /*
            Helper class that handles calculations for LTR and RTL layouts.
         */
        private class LayoutDirectionHelper {
    
            // Is the layout an RTL one?
            private final boolean mIsRTL;
    
            @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
            LayoutDirectionHelper(int direction) {
                mIsRTL = direction == View.LAYOUT_DIRECTION_RTL;
            }
    
            /*
                Calculate the amount of scroll needed to align the target view with the layout edge.
             */
            int getScrollToAlignView(View targetView) {
                return (mIsRTL)
                    ? mOrientationHelper.getDecoratedEnd(targetView) - mRecyclerView.getWidth()
                    : mOrientationHelper.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}
             */
            int[] calculateDistanceToScroll(LinearLayoutManager layoutManager, int targetPos) {
                int[] out = new int[2];
    
                int firstVisiblePos;
    
                firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
                if (layoutManager.canScrollHorizontally()) {
                    if (targetPos <= firstVisiblePos) { // scrolling toward top of data
                        if (mIsRTL) {
                            View lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition());
                            out[0] = mOrientationHelper.getDecoratedEnd(lastView)
                                + (firstVisiblePos - targetPos) * mItemDimension;
                        } else {
                            View firstView = layoutManager.findViewByPosition(firstVisiblePos);
                            out[0] = mOrientationHelper.getDecoratedStart(firstView)
                                - (firstVisiblePos - targetPos) * mItemDimension;
                        }
                    }
                }
                if (layoutManager.canScrollVertically()) {
                    if (targetPos <= firstVisiblePos) { // scrolling toward top of data
                        View firstView = layoutManager.findViewByPosition(firstVisiblePos);
                        out[1] = firstView.getTop() - (firstVisiblePos - targetPos) * mItemDimension;
                    }
                }
    
                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.
             */
            int getPositionsToMove(LinearLayoutManager llm, int scroll, int itemSize) {
                int positionsToMove;
    
                positionsToMove = roundUpToBlockSize(Math.abs(scroll) / itemSize);
    
                if (positionsToMove < mBlocksize) {
                    // Must move at least one block
                    positionsToMove = mBlocksize;
                } else if (positionsToMove > mMaxPositionsToMove) {
                    // Clamp number of positions to move so we don't get wild flinging.
                    positionsToMove = mMaxPositionsToMove;
                }
    
                if (scroll < 0) {
                    positionsToMove *= -1;
                }
                if (mIsRTL) {
                    positionsToMove *= -1;
                }
    
                if (mLayoutDirectionHelper.isDirectionToBottom(scroll < 0)) {
                    // Scrolling toward the bottom of data.
                    return roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove;
                }
                // Scrolling toward the top of the data.
                return roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove;
            }
    
            boolean isDirectionToBottom(boolean velocityNegative) {
                //noinspection SimplifiableConditionalExpression
                return mIsRTL ? velocityNegative : !velocityNegative;
            }
        }
    
        public interface SnapBlockCallback {
            void onBlockSnap(int snapPosition);
    
            void onBlockSnapped(int snapPosition);
    
        }
    
        private static final float MILLISECONDS_PER_INCH = 100f;
        @SuppressWarnings("unused")
        private static final String TAG = "SnapToBlock";
    }
    

    上面定义的SnapBlockCallback接口可用于报告视图的适配器位置,该视图位于要捕捉的块的开始处.如果视图不在屏幕上,则在调用时可能无法实例化与该位置关联的视图.

    The SnapBlockCallback interface defined above can be used to report the adapter position of the view at the start of the block to be snapped. The view associated with that position may not be instantiated when the call is made if the view is off screen.

    这篇关于如何捕捉RecyclerView项,以便将每个X项视为要捕捉的单个单元?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆