How to snap RecyclerView items so that every X items would be considered like a single unit to snap to?

后端 未结 3 1616
面向向阳花
面向向阳花 2020-11-30 01:27

Background

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

LinearSnapHelper().attachToRecyclerView(recyclerView)

Example

3条回答
  •  猫巷女王i
    2020-11-30 01:53

    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.

    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.

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

    Update: Adding callbacks for pre/post snapping.

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

    Add the following to attach the SnapToBlock to the RecyclerView.

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

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

    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.

提交回复
热议问题