RecyclerView horizontal scroll snap in center
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 ?
Solution 1:
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"
Solution 2:
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!