110

I'm trying to make a carousel-like view here using RecyclerView, I want the item to snap in the middle of the screen when scrolling, one item at a time. I've tried using recyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

but the view is still scrolling smoothly, I've also tried to implement my own logic using scroll listener like so:

recyclerView.setOnScrollListener(new OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    Log.v("Offset ", recyclerView.getWidth() + "");
                    if (newState == 0) {
                        try {
                               recyclerView.smoothScrollToPosition(layoutManager.findLastVisibleItemPosition());
                                recyclerView.scrollBy(20,0);
                            if (layoutManager.findLastVisibleItemPosition() >= recyclerView.getAdapter().getItemCount() - 1) {
                                Beam refresh = new Beam();
                                refresh.execute(createUrl());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

The swipe from right to left is working fine now, but not the other way around, what am I missing here ?

GabrielOshiro
  • 7,986
  • 4
  • 45
  • 57
Ahmed Awad
  • 1,787
  • 3
  • 16
  • 22

7 Answers7

190

With LinearSnapHelper, this is now very easy.

All you need to do is this:

SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);

Update

Available since 25.1.0, PagerSnapHelper can help achieve ViewPager like effect. Use it as you would use the LinearSnapHelper.

Old workaround:

If you wish for it to behave akin to the ViewPager, try this instead:

LinearSnapHelper snapHelper = new LinearSnapHelper() {
    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        View centerView = findSnapView(layoutManager);
        if (centerView == null) 
            return RecyclerView.NO_POSITION;

        int position = layoutManager.getPosition(centerView);
        int targetPosition = -1;
        if (layoutManager.canScrollHorizontally()) {
            if (velocityX < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        if (layoutManager.canScrollVertically()) {
            if (velocityY < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        final int firstItem = 0;
        final int lastItem = layoutManager.getItemCount() - 1;
        targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
        return targetPosition;
    }
};
snapHelper.attachToRecyclerView(recyclerView);

The implementation above just returns the position next to the current item (centered) based on the direction of the velocity, regardless of the magnitude.

The former one is a first party solution included in the Support Library version 24.2.0. Meaning you have to add this to your app module's build.gradle or update it.

compile "com.android.support:recyclerview-v7:24.2.0"
razzledazzle
  • 6,900
  • 4
  • 26
  • 37
  • This did not work for me (but the solution by @Albert Vila Calvo did). Have you implemented it? Are we supposed to override methods from it? I would think that the class should do all of the work out of the box – galaxigirl Aug 23 '16 at 14:58
  • Yes, hence the answer. I have not tested thoroughly however. To be clear, I did this with a horizontal `LinearLayoutManager` where the views all were regular in size. Nothing else but the snippet above is needed. – razzledazzle Aug 23 '16 at 15:08
  • @galaxigirl apologies, I have understood what you meant and have made some edit acknowledging that, please do comment. This is an alternate solution with the `LinearSnapHelper`. – razzledazzle Aug 28 '16 at 06:26
  • This worked just right for me. @galaxigirl overriding this method helps in that the linear snap helper will otherwise snap to the nearest item when scroll is settling, not exactly just the next/previous item. Thanks a lot for this, razzledazzle – AA_PV Dec 25 '16 at 13:23
  • LinearSnapHelper was the initial solution that worked for me, but it seems that PagerSnapHelper gave the swiping a better animation. – Leonardo Sibela Mar 11 '20 at 14:03
106

Google I/O 2019 Update

ViewPager2 is here!

Google just announced at the talk 'What's New in Android' (aka 'The Android keynote') that they are working on a new ViewPager based on RecyclerView!

From the slides:

Like ViewPager, but better

  • Easy migration from ViewPager
  • Based on RecyclerView
  • Right-to-Left mode support
  • Allows vertical paging
  • Improved dataset change notifications

You can check the latest version here and the release notes here. There is also an official sample. Update Dec. 2021: sample has moved to this other repo.

Personal opinion: I think this is a really needed addition. I've recently had a lot of trouble with the PagerSnapHelper oscillating left right indefinitely - see the ticket I've opened.


New answer (2016)

You can now just use a SnapHelper.

If you want a center-aligned snapping behavior similar to ViewPager then use PagerSnapHelper:

SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

There is also a LinearSnapHelper. I've tried it and if you fling with energy then it scrolls 2 items with 1 fling. Personally I didn't like it, but just decide by yourself - trying it only takes seconds.


Original answer (2016)

After many hours of trying 3 different solutions found here in SO I've finally built a solution that mimics very closely the behavior found in a ViewPager.

The solution is based on the @eDizzle solution, which I believe I've improved enough to say that it works almost like a ViewPager.

Important: my RecyclerView items width is exactly the same as the screen. I haven't tried with other sizes. Also I use it with an horizontal LinearLayoutManager. I think that you will need to adapt the code if you want vertical scroll.

Here you have the code:

public class SnappyRecyclerView extends RecyclerView {

    // Use it with a horizontal LinearLayoutManager
    // Based on https://stackoverflow.com/a/29171652/4034572

    public SnappyRecyclerView(Context context) {
        super(context);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

        int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

        // views on the screen
        int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
        View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
        int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
        View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

        // distance we need to scroll
        int leftMargin = (screenWidth - lastView.getWidth()) / 2;
        int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
        int leftEdge = lastView.getLeft();
        int rightEdge = firstView.getRight();
        int scrollDistanceLeft = leftEdge - leftMargin;
        int scrollDistanceRight = rightMargin - rightEdge;

        if (Math.abs(velocityX) < 1000) {
            // The fling is slow -> stay at the current page if we are less than half through,
            // or go to the next page if more than half through

            if (leftEdge > screenWidth / 2) {
                // go to next page
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                // go to next page
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                // stay at current page
                if (velocityX > 0) {
                    smoothScrollBy(-scrollDistanceRight, 0);
                } else {
                    smoothScrollBy(scrollDistanceLeft, 0);
                }
            }
            return true;

        } else {
            // The fling is fast -> go to next page

            if (velocityX > 0) {
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                smoothScrollBy(-scrollDistanceRight, 0);
            }
            return true;

        }

    }

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);

        // If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
        // This code fixes this. This code is not strictly necessary but it improves the behaviour.

        if (state == SCROLL_STATE_IDLE) {
            LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

            int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

            // views on the screen
            int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
            View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
            int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
            View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

            // distance we need to scroll
            int leftMargin = (screenWidth - lastView.getWidth()) / 2;
            int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
            int leftEdge = lastView.getLeft();
            int rightEdge = firstView.getRight();
            int scrollDistanceLeft = leftEdge - leftMargin;
            int scrollDistanceRight = rightMargin - rightEdge;

            if (leftEdge > screenWidth / 2) {
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                smoothScrollBy(scrollDistanceLeft, 0);
            }
        }
    }

}

Enjoy!

Albert Vila Calvo
  • 15,298
  • 6
  • 62
  • 73
  • Nice catch the on onScrollStateChanged(); otherwise, a small bug is present if we swipe slowly the RecyclerView. Thanks! – Mauricio Sartori Jun 22 '16 at 19:23
  • To support margins (android:layout_marginStart="16dp") I tried with int screenwidth = getWidth(); and it seems to work. – eigil Jun 29 '16 at 12:14
  • AWESOOOMMEEEEEEEEE – raditya gumay Jul 08 '16 at 03:07
  • How does this work with lower API's? Since `RecyclerView` needs to be used with `android.support.v7.widget.RecyclerView` to be adaptable for lower API's and this method requires you to use your own xml tag. – Gary Holiday Aug 05 '16 at 18:02
  • @Albert your solution is great - I was unsuccessful using the new LinearSnapHelper class. Have you implemented it? – galaxigirl Aug 23 '16 at 14:55
  • 1
    @galaxigirl no, I haven't tryied LinearSnapHelper yet. I just updated the answer so that people knows about the official solution. My solution works without any issue on my app. – Albert Vila Calvo Aug 23 '16 at 15:52
  • 1
    @AlbertVilaCalvo I tried LinearSnapHelper. LinearSnapHelper works with a Scroller. It scrolls and snaps based on the gravity and the fling velocity. Higher the speed more pages are scrolled. I wouldn't recommend it for a ViewPager like behaviour. – mipreamble Aug 27 '16 at 21:37
  • @mipreamble made some modification for that behavior, please check http://stackoverflow.com/a/39036351/4803633 and provide feedback. – razzledazzle Aug 28 '16 at 06:25
  • @razzledazzle I like the solution. But, the target position of the item is based on how long the drag is. If the drag is longer then targetPosition is position +/- 2. – mipreamble Aug 29 '16 at 19:39
  • to support a reversed layout order `int lastVisibleItemPosition = linearLayoutManager.getReverseLayout() ? linearLayoutManager.findFirstVisibleItemPosition() : linearLayoutManager.findLastVisibleItemPosition(); View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition); int firstVisibleItemPosition = linearLayoutManager.getReverseLayout() ? linearLayoutManager.findLastVisibleItemPosition() : linearLayoutManager.findFirstVisibleItemPosition(); View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);` – martyglaubitz Sep 06 '16 at 10:45
  • I tested the SnapHelper and worked as a ViewPager (exactly what I wanted) with only 2 lines of code !!! `SnapHelper snapHelper = new PagerSnapHelper(); snapHelper.attachToRecyclerView(mRecyclerView);` – Fernando Bonet Feb 06 '17 at 18:09
  • Worked like a charm.Thanks – Arpan Sharma Feb 15 '17 at 05:49
  • UI misaligned upon rotation of the phone. – Desmond Lua Feb 23 '17 at 11:24
  • How can i achieve same for vertical scrolling recycler view ? – kavie Dec 19 '17 at 05:26
  • @AlbertVilaCalvo so how can I get the current item for that? – Amir Hossein Ghasemi Feb 06 '19 at 14:34
  • It's 2021 - is ViewPager2 still the way? I'm about to give it a go with this sample from Android https://github.com/android/views-widgets-samples/tree/main/ViewPager2 – Chucky Dec 06 '21 at 12:58
52

If the goal is to make the RecyclerView mimic the behavior of ViewPager there is quite easy approach

RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);

LinearLayoutManager layoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
SnapHelper snapHelper = new PagerSnapHelper();
recyclerView.setLayoutManager(layoutManager);
snapHelper.attachToRecyclerView(mRecyclerView);

By using PagerSnapHelper you can get the behavior like ViewPager

Boonya Kitpitak
  • 3,607
  • 1
  • 30
  • 30
31

You need to use findFirstVisibleItemPosition for going in the opposite direction. And for detecting which direction the swipe was in, youll need to get either the fling velocity or the change in x. I approached this problem from a slightly different angle than you have.

Create a new class that extends the RecyclerView class and then override RecyclerView's fling method like so:

@Override
public boolean fling(int velocityX, int velocityY) {
    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

//these four variables identify the views you see on screen.
    int lastVisibleView = linearLayoutManager.findLastVisibleItemPosition();
    int firstVisibleView = linearLayoutManager.findFirstVisibleItemPosition();
    View firstView = linearLayoutManager.findViewByPosition(firstVisibleView);
    View lastView = linearLayoutManager.findViewByPosition(lastVisibleView);

//these variables get the distance you need to scroll in order to center your views.
//my views have variable sizes, so I need to calculate side margins separately.     
//note the subtle difference in how right and left margins are calculated, as well as
//the resulting scroll distances.
    int leftMargin = (screenWidth - lastView.getWidth()) / 2;
    int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
    int leftEdge = lastView.getLeft();
    int rightEdge = firstView.getRight();
    int scrollDistanceLeft = leftEdge - leftMargin;
    int scrollDistanceRight = rightMargin - rightEdge;

//if(user swipes to the left) 
    if(velocityX > 0) smoothScrollBy(scrollDistanceLeft, 0);
    else smoothScrollBy(-scrollDistanceRight, 0);

    return true;
}
eDizzle
  • 573
  • 5
  • 8
  • Where is screenWidth defined? – ltsai Jul 03 '15 at 06:22
  • @Itsai : Oops! Good question. It's a variable I set elsewhere in the class. It's the screen width of the device in pixels, not density pixels, since the smoothScrollBy method requires the number of pixels you want it to scroll by. – eDizzle Jul 09 '15 at 00:42
  • 3
    @ltsai You can change to getWidth() (recyclerview.getWidth()) instead of screenWidth. screenWidth doesn't work correctly when RecyclerView's smaller than screen' width. – VAdaihiep Oct 29 '15 at 08:24
  • I tried to extend RecyclerView but I'm getting "Error inflating class custom RecyclerView". Can you help? Thanks –  Feb 17 '16 at 12:04
  • @bEtTyBarnes : You might want to search for that in another question feed. It's pretty out of scope with the initial question here. – eDizzle Apr 13 '16 at 13:04
  • @bEtTyBarnes : you have to create all three constructors matching super class. – Sajal Apr 14 '16 at 19:56
8

Just add padding and margin to recyclerView and recyclerView item:

recyclerView item:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parentLayout"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_marginLeft="8dp" <!-- here -->
    android:layout_marginRight="8dp" <!-- here  -->
    android:layout_width="match_parent"
    android:layout_height="200dp">

   <!-- child views -->

</RelativeLayout>

recyclerView:

<androidx.recyclerview.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="8dp" <!-- here -->
    android:paddingRight="8dp" <!-- here -->
    android:clipToPadding="false" <!-- important!-->
    android:scrollbars="none" />

and set PagerSnapHelper :

int displayWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
parentLayout.getLayoutParams().width = displayWidth - Utils.dpToPx(16) * 4;
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

dp to px:

public static int dpToPx(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}

result:

enter image description here

Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Farzad
  • 1,975
  • 1
  • 25
  • 47
  • Is there a way to achieve variable padding between items? like if the first and last card dont need to be centered to show more of the next/prev cards vs the intermediate cards being centered? – RamPrasadBismil May 24 '19 at 18:41
2

My solution:

/**
 * Horizontal linear layout manager whose smoothScrollToPosition() centers
 * on the target item
 */
class ItemLayoutManager extends LinearLayoutManager {

    private int centeredItemOffset;

    public ItemLayoutManager(Context context) {
        super(context, LinearLayoutManager.HORIZONTAL, false);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        LinearSmoothScroller linearSmoothScroller = new Scroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    public void setCenteredItemOffset(int centeredItemOffset) {
        this.centeredItemOffset = centeredItemOffset;
    }

    /**
     * ********** Inner Classes **********
     */

    private class Scroller extends LinearSmoothScroller {

        public Scroller(Context context) {
            super(context);
        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            return ItemLayoutManager.this.computeScrollVectorForPosition(targetPosition);
        }

        @Override
        public int calculateDxToMakeVisible(View view, int snapPreference) {
            return super.calculateDxToMakeVisible(view, SNAP_TO_START) + centeredItemOffset;
        }
    }
}

I pass this layout manager to the RecycledView and set the offset required to center items. All my items have the same width so constant offset is ok

anagaf
  • 2,120
  • 1
  • 18
  • 15
0

PagerSnapHelper doesn't work with GridLayoutManager with spanCount > 1, so my solution under this circumstance is:

class GridPagerSnapHelper : PagerSnapHelper() {
    override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int): Int {
        val forwardDirection = if (layoutManager?.canScrollHorizontally() == true) {
            velocityX > 0
        } else {
            velocityY > 0
        }
        val centerPosition = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
        return centerPosition +
            if (forwardDirection) (layoutManager as GridLayoutManager).spanCount - 1 else 0
    }
}
Wenqing MA
  • 73
  • 1
  • 6