RecyclerView horizontal scrolling in the center

I am trying to pretend from a carousel here using RecyclerView, I want the element to snap the middle of the screen when scrolling, one element at a time. I tried using recyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

but the view still scrolls smoothly, I also tried to implement my own logic using a scroll listener as follows:

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

A swipe from right to left is working fine now, but not vice versa, what am I missing here?

+74
android android-recyclerview horizontal-scrolling
Mar 18 '15 at 22:43
source share
7 answers

With LinearSnapHelper , it is now very simple.

All you have to do is the following:

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

Update

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

Old workaround:

If you want it to look like a 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 simply returns the position next to the current element (in the center) depending on the direction of speed, regardless of magnitude.

The first is the first batch solution included in the 24.2.0 support library. This means that you must add this to your build.gradle application build.gradle or update it.

 compile "com.android.support:recyclerview-v7:24.2.0" 
+127
Aug 19 '16 at 10:09 on
source share

Google I / O 2019 Update:

ViewPager2 is here!

Google just announced during the “What's New in Android” (also known as “Android Summary”) performance that they are working on a new RecyclerView ViewPager!

From the slides:

Like ViewPager, but better

  • Simple migration from ViewPager
  • Based on RecyclerView
  • Right to left support
  • Allows vertical paging
  • Improved data set change notifications

You can check the latest version here and release notes here . There is also an official sample .

Personal opinion: I think this is a really necessary addition. Recently, I had a lot of problems with PagerSnapHelper swaying left and right indefinitely - see the ticket I opened.




New answer (2016):

Now you can just use SnapHelper .

If you want the center snap to look like a ViewPager , use the PagerSnapHelper :

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

There is also LinearSnapHelper . I tried this, and if you throw with energy, it scrolls 2 objects with 1 throw. Personally, I did not like this, but I decided on my own - the attempt will take only a few seconds.




Original answer (2016):

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

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

Important: the width of the RecyclerView elements is exactly the same as the screen I have not tried with other sizes. I also use it with a horizontal LinearLayoutManager . I think you will need to adapt the code if you want to scroll vertically.

Here you have the code:

 public class SnappyRecyclerView extends RecyclerView { // Use it with a horizontal LinearLayoutManager // Based on /questions/122509/recyclerview-horizontal-scroll-snap-in-center/747319#747319 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 it!

+57
Jun 14 '16 at 16:01
source share

If the goal is to have a RecyclerView mimic the behavior of a ViewPager , there is a fairly simple 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); 

Using PagerSnapHelper , you can get behavior like ViewPager

+38
Feb 09 '17 at 4:41 on
source share

You need to use findFirstVisibleItemPosition to jump in the opposite direction. And to determine which direction the napkin is in, you need to get either the speed of movement or the change in x. I approached this problem from a slightly different angle than yours.

Create a new class that extends the RecyclerView class and then overrides the Fling RecyclerView method as follows:

 @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; } 
+30
Mar 20 '15 at 16:41
source share

My decision:

 /** * 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 needed for the center elements. All my objects are the same width, so the constant offset is ok

+2
Apr 02 '16 at 10:24
source share

Just add padding and margin for recyclerView and recyclerView item :

recyclerView element:

 <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 install 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

+1
Apr 08 '19 at 12:37
source share

PagerSnapHelper does not work with GridLayoutManager with spanCount> 1, so my solution in this case 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 } } 
0
Apr. 19 '19 at 10:07 on
source share



All Articles