如何捕捉 RecyclerView 项目,以便将每个 X 项目视为一个要捕捉的单元?

2023-12-31

背景

可以使用以下方法将 RecyclerView 捕捉到其中心:

LinearSnapHelper().attachToRecyclerView(recyclerView)

Example:

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)
    }
}

活动主文件

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

也可以将其固定到其他侧面,就像在某些库中所做的那样,例如here https://github.com/DevExchanges/SnappingRecyclerview.

还有一些库允许 RecyclerView 像 ViewPager 一样工作,例如here https://github.com/lsjwzh/RecyclerViewPager.

问题

假设我有一个包含许多项目的 RecyclerView(在我的例子中是水平的),并且我希望它将每 X 个项目(X 是常数)视为一个单元,并捕捉到每个单元。

例如,如果我滚动一点,它可能会捕捉到 0 项或 X 项,但不会捕捉到它们之间的某些内容。

在某种程度上,它的行为与普通 ViewPager 的情况类似,只是每个页面都有 X 个项目。

例如,如果我们继续我上面编写的示例代码,假设 X==3 ,则捕捉将来自此空闲状态:

到这个空闲状态(如果我们滚动得足够多,否则将保持在之前的状态):

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

进一步滚动(沿相同方向)到下一个捕捉点将到达项目“6”、“9”等......

我尝试过的

我尝试搜索替代库,也尝试阅读有关此的文档,但没有找到任何可能有用的内容。

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

问题

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

    当然,这些项目将均匀地占用整个 RecyclerView 的足够空间。

  2. 假设有可能,当 RecyclerView 即将捕捉到某个项目(包括在捕捉之前拥有该项目)时,我将如何获得回调?我问这个是因为它与我问的同一问题有关here https://stackoverflow.com/q/47534838/878126.


科特林解决方案

基于“Cheticamp”答案的可行 Kotlin 解决方案(here https://github.com/DevExchanges/SnappingRecyclerview),无需验证您是否具有 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<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

虽然我已经标记了一个答案 https://stackoverflow.com/a/47580753/878126正如所接受的,因为它工作正常,我注意到它有严重的问题:

  1. 平滑滚动似乎无法正常工作(无法滚动到正确的位置)。只有滚动才有效(但具有“涂抹”效果):

    (recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)
    
  2. 当切换到 RTL(从右到左)语言环境(例如希伯来语(“עברתת”))时,它根本不允许我滚动。

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

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

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


更新:随着我们修复了 RTL/LTR,我在这篇文章中更新了 Kotlin 解决方案。


更新:关于第 3 点,这似乎是因为 recyclerView 有一个视图池,它很快就被填满了。为了解决这个问题,我们可以简单地扩大池大小,使用recyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType, Integer.MAX_VALUE) 对于我们其中的每种视图类型。奇怪的是,这确实是需要的。我已将其发布到 Google (here https://issuetracker.google.com/issues/71784689 and here https://issuetracker.google.com/issues/72301192)但被拒绝了,默认情况下池应该是无限的。最后,我决定至少要求有一个更方便的函数来为所有视图类型执行此操作(here https://issuetracker.google.com/issues/110979418).


SnapHelper为您正在尝试的内容提供必要的框架,但需要对其进行扩展以处理视图块。班上SnapToBlock下面延伸SnapHelper捕捉到视图块。在示例中,我对一个块使用了四个视图,但可以更多或更少。

Update:代码已更改以适应GridLayoutManagerLinearLayoutManager。现在禁止投掷,因此捕捉效果更多列出ViewPager。现在支持水平和垂直滚动以及 LTR 和 RTL 布局。

Update:将平滑滚动插值器更改为更像ViewPager.

Update:添加前/后捕捉的回调。

Update:添加对 RTL 布局的支持。

这是示例应用程序的快速视频:

设置布局管理器如下:

// 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 to the RecyclerView.

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

mMaxFlingPages是允许一次抛出的最大块数 (rowsCols * span)。

对于即将进行快照并已完成时的回调,请添加以下内容:

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";
}

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何捕捉 RecyclerView 项目,以便将每个 X 项目视为一个要捕捉的单元? 的相关文章

  • 使用 JSONArray 还是普通数组来存储/读取数据更有效?

    我正在使用一个连接到PHP MySQL返回所有内容的服务器JSON格式 例如 用户列表作为JSONArray of JSONObject 每个对象都包含单个用户的信息 姓名 位置 电话号码等 处理这种格式的信息时 将所有内容保留在其中会更有
  • 如何使用 gradle 从 3 个子模块构建 1 个 jar

    I have 安卓工作室3 gradle 4 1 梯度工具3 classpath com android tools build gradle 3 0 1 当我有一个模块并使用 gradle 工具 2 时 我使用了 task makeJar
  • 如何在应用程序关闭时在 Android 通知中显示操作按钮?

    我有一个安卓应用程序 对于通知 我们必须显示一些操作按钮 当应用程序打开时 我们可以自由地构建通知并显示操作按钮 但是当应用程序关闭时 通知会在 Android 的通知托盘中收到 应用程序开发人员无法控制构建用户界面和操作按钮 我们现在如何
  • 在 ChromeO 上安装未知来源的 apk

    我今天早上更新了我的 Chromebook Asus Flip 以获取 Play 商店 我的 Chromebook 安装了 M53dev 通道版本 它运作良好 我可以安装并运行从 Play 商店下载的 Android 应用程序 我想测试我的
  • Android 中的 Fragment-Fragment 通信

    我在Android编程方面处于初级水平 所以我需要你真诚的帮助 请任何人帮助我 我正在尝试使用片段构建滑动用户界面 所以我真正的疑问是 我有一个Fragment say FragmentA 它有一个TextView and Button在其
  • Android 中的列表(特别是 RecyclerView 和 CardView)如何工作

    请原谅我问这个问题 但我是 Android 开发新手 尽管我正在尝试了解developer android com 网站上的基础知识 但大多数示例 即使他们说它们是为 Android Studio 构建的 尚未设置为使用 Gradle 因此
  • Fragment 问题中的 ExpandableListView

    我正在尝试在片段中实现可扩展列表视图 没有错误出现 当我尝试记录两个的输出时List
  • HMS 核心地图套件在我的 Android 应用程序上根本无法工作

    我正在尝试在我的应用程序中使用华为 HMS 地图套件 我对整体地图很陌生 无论是来自谷歌还是华为 我按照文档中的教程以及华为提供的代码实验室中的说明进行操作 并将我的代码在一起 但是当我运行地图活动时 什么也没有出现 我得到的只是一个空白活
  • 如何使用 SharedPreferences 保存多个值?

    我正在开发一个字典应用程序 在我的应用程序中 我假设用户想要保存最喜欢的单词 我决定使用共享首选项保存这些值 我知道 SQLite 和文件更好 但我坚持使用 SharedPreferences 所以继续使用它 下面是我的代码 Overrid
  • 如何将 Android 添加到 Phonegap 平台版本 3

    经过大量挖掘 我相信这个问题 https stackoverflow com questions 18423444 phonegap 3 doesnt work with andriod studio与我没有添加任何用于构建phonegap
  • 安卓。 CalendarView...一次仅显示一个月的日历

    我正在使用 CalendarView 其中我想一次仅查看一个月的日历并滚动查看下个月 但 CalendarView 一次显示所有月份 下面是我的代码
  • Android模拟器中的网络访问

    我试图通过我的 Android 应用程序访问互联网 但我既成功又失败 我在构建应用程序时启动模拟器 并且应用程序安装得很好 我可以使用浏览器访问互联网 但是 当我尝试这个小代码片段时 InetAddress inet try inet In
  • 使用 Play Integrity API 时,Firebase 电话身份验证会出现缺少客户端标识符错误

    使用 Firebase 电话身份验证注册 登录时 身份验证流程始终会启动 reCAPTCHA 流程 并在返回应用程序后发出missing client identifier error 我的设置之前适用于设备验证 安全网络 API 除了我的
  • Unity c# 四元数:将 y 轴与 z 轴交换

    我需要旋转一个对象以相对于现实世界进行精确旋转 因此调用Input gyro attitude返回表示设备位置的四元数 另一方面 这迫使我根据这个四元数作为默认旋转来计算每个旋转 将某些对象设置为朝上的简单方法如下 Vector3 up I
  • 如何将样式应用于我拥有的所有 TextView? [复制]

    这个问题在这里已经有答案了 可能的重复 设计所有 TextView 或自定义视图 的样式 而不向每个 TextView 添加样式属性 https stackoverflow com questions 6801890 styling all
  • 如何在android中通过蓝牙向配对设备发送短信?

    在我的应用程序中 我想通过蓝牙发送和接收短信 我可以在列表视图中看到配对设备名称和地址的列表 但是当我尝试向配对设备发送文本时 什么也没有发生 在其他设备中没有收到文本 这是我向配对设备发送消息的代码 private void sendDa
  • fs-extra:源和目标不能相同。 (科尔多瓦)

    我在使用 cordova 构建时遇到错误 Error Source and destination must not be the same 构建系统 Ionic ionic cli 4 10 1 ionic framework ionic
  • ECDH使用Android KeyStore生成私钥

    我正在尝试使用 Android KeyStore Provider 生成的私有文件在 Android 中实现 ECDH public byte ecdh PublicKey otherPubKey throws Exception try
  • 如何访问我的 Android 程序中的联系人

    我正在制作一个短信应用程序 并且想要访问我的 Android 应用程序中的联系人 我想访问联系人 就像他们在实际联系人列表中一样 选择后 我需要返回到我的活动 在其中我可以向该人发送短信 或者是否可以访问存储联系人的数据库 我的代码如下所示
  • Android GetPTLAFormat 上的 Phonegap 错误

    我们正在开发一个使用 jQuery 移动和电话间隙的应用程序 一切似乎都工作正常 但是当在连接的 Android 手机上运行应用程序时 我们在 Eclipse logcat 中看到大量类似这样的错误 0 GetPTLAFormat inva

随机推荐