web-dev-qa-db-ja.com

すべてのXアイテムが単一のユニットのようにスナップされるように、RecyclerViewアイテムをスナップする方法は?

バックグラウンド

以下を使用して、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<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"/>
_

here など、一部のライブラリで行われたように、他の側面にスナップすることもできます。

here など、ViewPagerのように機能するRecyclerViewを使用できるライブラリもあります。

問題

多くのアイテムを含むRecyclerView(私の場合は水平)があり、すべてのXアイテム(Xは一定)を単一のユニットとして扱い、それらの各ユニットにスナップするようにしたいとします。

たとえば、少しスクロールすると、0アイテムまたはXアイテムのいずれかにスナップできますが、それらの間のアイテムにはスナップできません。

ある意味では、その動作は通常のViewPagerの場合と似ていますが、各ページにXアイテムが含まれているだけです。

たとえば、上で書いたサンプルコードから続行すると、X == 3であると仮定すると、スナップはこのアイドル状態からのものになります。

enter image description here

このアイドル状態に(十分にスクロールした場合、そうでなければ前の状態のままになります):

enter image description here

上記のライブラリと同様に、より多くのフリングまたはスクロールは、ViewPagerのように処理する必要があります。

次のスナップ点までさらに(同じ方向に)スクロールすると、アイテム「6」、「9」などに到達します...

私が試したもの

代替ライブラリを検索しようとしましたが、これに関するドキュメントも読み込もうとしましたが、有用なものは見つかりませんでした。

ViewPagerを使用することでも可能かもしれませんが、ViewPagerはアイテムを適切にリサイクルしないため、これは最善の方法ではないと思います。スナップ方法については、RecyclerViewよりも柔軟性が低いと思います。

質問

  1. 各Xアイテムをスナップする単一のページとして扱うために、RecyclerViewをすべてのXアイテムをスナップするように設定することは可能ですか?

    もちろん、アイテムはRecyclerView全体に均等に十分なスペースを取ります。

  2. 可能な場合、RecyclerViewが特定のアイテムにスナップしようとしているときに、このアイテムがスナップされる前にコールバックを取得するにはどうすればよいですか?これは、私が尋ねたのと同じ質問に関連しているためです here .


Kotlinソリューション

「Cheticamp」回答に基づいた実用的なKotlinソリューション( here )、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"
    }
}
_

更新

an answer とマークしましたが、正常に機能するため、深刻な問題があることに気付きました:

  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 および here )しかし、プールはデフォルトで無制限であるべきであると拒否されました。最後に、少なくともすべてのビュータイプに対してより便利な機能を要求することにしました( here )。

15

SnapHelperは、試行するものに必要なフレームワークを提供しますが、ビューのブロックを処理するために拡張する必要があります。以下のSnapToBlockクラスは、SnapHelperを拡張してビューのブロックにスナップします。この例では、ブロックに対して4つのビューを使用しましたが、多かれ少なかれ可能です。

更新:GridLayoutManagerLinearLayoutManagerに対応するようにコードが変更されました。フリングが禁止されるようになったため、スナップはViewPagerのリストをさらに機能させます。 LTRおよびRTLレイアウトと同様に、水平および垂直スクロールがサポートされるようになりました。

更新:スムーズスクロール補間をViewPagerのようなものに変更しました。

更新:スナップの前後にコールバックを追加。

更新:RTLレイアウトのサポートを追加。

サンプルアプリの簡単なビデオを次に示します。

enter image description here

次のようにレイアウトマネージャーを設定します。

// 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));

SnapToBlockRecyclerViewに添付するには、次を追加します。

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

mMaxFlingPagesは、一度に投げることができるブロックの最大数(rowsCols * spans)です。

スナップが作成されて完了したときのコールバックの場合、次を追加します。

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インターフェースを使用して、スナップされるブロックの開始位置でのビューのアダプター位置を報告できます。ビューが画面外にある場合、呼び出しが行われたときに、その位置に関連付けられたビューがインスタンス化されない場合があります。

28
Cheticamp

私はそのようなことをします

  1. RecyclerView内のスクロールをブロックします(例 RecyclerViewのスクロールを無効にする方法?

  2. Gesture Fling Detecorを作成し、RecyclerViewに添付します

  3. 内部ジェスチャ検出器は、フリングイベントイベントを検出します
  4. Flingイベントで、サイドを検出します(左右)
  5. RecyclerViewをスクロールして配置します(最初のVisibleアイテム+ const *(左?-1:1))

動作するはずです:)