Scrolling Sync Multiple RecyclerViews

I have a ViewPager showing a fragment on a page. This snippet contains a list of items inside the RecyclerView. The list of items is always the same size, and the item views also have the same height. When scrolling through one of the RecyclerViews, I want the other RecyclerViews to scroll simultaneously and at the same distance. How to synchronize scrolling RecyclerViews?

+14
source share
6 answers

Here is my solution. The smaller the code, the better ...

lvDetail and lvDetail2 are the RecyclerView you want to sync.

final RecyclerView.OnScrollListener[] scrollListeners = new RecyclerView.OnScrollListener[2]; scrollListeners[0] = new RecyclerView.OnScrollListener( ) { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); lvDetail2.removeOnScrollListener(scrollListeners[1]); lvDetail2.scrollBy(dx, dy); lvDetail2.addOnScrollListener(scrollListeners[1]); } }; scrollListeners[1] = new RecyclerView.OnScrollListener( ) { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); lvDetail.removeOnScrollListener(scrollListeners[0]); lvDetail.scrollBy(dx, dy); lvDetail.addOnScrollListener(scrollListeners[0]); } }; lvDetail.addOnScrollListener(scrollListeners[0]); lvDetail2.addOnScrollListener(scrollListeners[1]); 
+20
source

I believe that it is important for you to understand its work, so I am going to explain the whole procedure that I performed to develop my solution. Please note that this example is only for two RecyclerViews, but doing so with a lot is as easy as using the RecyclerViews array.

The first option that comes to mind is listening to scroll changes on both types of scrolling and when one of them scrolls, use scrollBy (int x, int y) on the other. Unfortunately, software scrolling will also call the listener, so you will end up in a loop.

To solve this problem, you need to configure OnItemTouchListener, which adds the correct ScrollListener when touching RecyclerView and removes it when scrolling stops. This works almost flawlessly, but if you quickly go to the long RecyclerView and then scroll it before it even ends, only the first scroll will be transferred.

To get around this, you will need to make sure that the OnScrollListener is only added if the RecyclerView is not working.

Let's look at the source:

  public class SelfRemovingOnScrollListener extends RecyclerView.OnScrollListener { @Override public final void onScrollStateChanged(@NonNull final RecyclerView recyclerView, final int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { recyclerView.removeOnScrollListener(this); } } } 

This is the class from which you need to extend your OnScrollListeners. This ensures that they will be removed if necessary.

Then I have two listeners, one for each RecyclerView:

 private final RecyclerView.OnScrollListener mLeftOSL = new SelfRemovingOnScrollListener() { @Override public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { super.onScrolled(recyclerView, dx, dy); mRightRecyclerView.scrollBy(dx, dy); } }, mRightOSL = new SelfRemovingOnScrollListener() { @Override public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { super.onScrolled(recyclerView, dx, dy); mLeftRecyclerView.scrollBy(dx, dy); } }; 

And then during initialization, you can configure OnItemTouchListeners. It would be better to configure one listener for the whole view, but RecyclerView does not support this. OnItemTouchListeners still don't pose a problem:

  mLeftRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { private int mLastY; @Override public boolean onInterceptTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) { Log.d("debug", "LEFT: onInterceptTouchEvent"); final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE; if (!ret) { onTouchEvent(rv, e); } return Boolean.FALSE; } @Override public void onTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) { Log.d("debug", "LEFT: onTouchEvent"); final int action; if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && mRightRecyclerView .getScrollState() == RecyclerView.SCROLL_STATE_IDLE) { mLastY = rv.getScrollY(); rv.addOnScrollListener(mLeftOSL); } else { if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) { rv.removeOnScrollListener(mLeftOSL); } } } @Override public void onRequestDisallowInterceptTouchEvent(final boolean disallowIntercept) { Log.d("debug", "LEFT: onRequestDisallowInterceptTouchEvent"); } }); mRightRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { private int mLastY; @Override public boolean onInterceptTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) { Log.d("debug", "RIGHT: onInterceptTouchEvent"); final Boolean ret = rv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE; if (!ret) { onTouchEvent(rv, e); } return Boolean.FALSE; } @Override public void onTouchEvent(@NonNull final RecyclerView rv, @NonNull final MotionEvent e) { Log.d("debug", "RIGHT: onTouchEvent"); final int action; if ((action = e.getAction()) == MotionEvent.ACTION_DOWN && mLeftRecyclerView .getScrollState () == RecyclerView.SCROLL_STATE_IDLE) { mLastY = rv.getScrollY(); rv.addOnScrollListener(mRightOSL); } else { if (action == MotionEvent.ACTION_UP && rv.getScrollY() == mLastY) { rv.removeOnScrollListener(mRightOSL); } } } @Override public void onRequestDisallowInterceptTouchEvent(final boolean disallowIntercept) { Log.d("debug", "RIGHT: onRequestDisallowInterceptTouchEvent"); } }); } 

Please also note that in my particular case, RecyclerViews are not the first to receive a touch event, so I need to intercept it. If this is not your case, you can (should) combine the code from onInterceptTouchEvent (...) into onTouchEvent (...).

Finally, it will crash if your user tries to scroll two RecyclerViews at the same time. The best solution to improve the quality of work is to set android:splitMotionEvents="false" in the direct parent containing RecyclerViews.

You can see an example with this code here .

+16
source

I think I found a very simple and short answer.

as Jorge Antonio Diaz-Benito said: β€œThe first option that comes to mind is to listen to scroll changes on both types of scrolling and when one of them scrolls, use scrollBy (int x, int y) on the other. Unfortunately, the software scrolling will also call the listener, so you end the loop. "

So you need to fix this problem. If you just keep track of who scrolls, they will not loop.

Decision

 public class SelfScrolListener extends RecyclerView.OnScrollListener { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { viewIsScrolling = -1; } } } 

This is your custom OnScrollListener to check if scrollState is IDLE. it's true β†’ nobody scrolls. therefore `int viewIsScolling = -1

Now you need to determine if you can scroll. this is the code:

 int viewIsScrolling = 1; boolean firstIsTouched = false; boolean secondIsTouched = false; SelfScrolListener firstOSL= new SelfScrolListener() { @Override public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { super.onScrolled(recyclerView, dx, dy); if (firstIsTouched) { if (viewIsScrolling == -1) { viewIsScrolling = 0; } if (viewIsScrolling == 0) { secondRecyclerView.scrollBy(dx, dy); } } } }; SelfScrolListener secondOSL= new SelfScrolListener() { @Override public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx, final int dy) { super.onScrolled(recyclerView, dx, dy); if(secondIsTouched){ if (viewIsScrolling == -1) { viewIsScrolling = 1; } if (viewIsScrolling == 1) { firstRecyclerView.scrollBy(dx, dy); } } } }; firstRecyclerView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { firstIsTouched= true; return false; } }); secondRecyclerView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { secondIsTouched= true; return false; } }); firstRecyclerView.addOnScrollListener(firstOSL); secondRecyclerView.addOnScrollListener(secondOSL); 

viewIsScrolling = global int and is set at the beginning of -1; a state that no one scrolls. you can post as many views as you want.

+4
source

First of all, consider using NestedScrollView as a parent for RecyclerViews . This may require some additional configuration, but the general idea is the same:

 <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_1" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_2" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <android.support.v7.widget.RecyclerView android:id="@+id/recycler_3" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </android.support.v4.widget.NestedScrollView> 

If you cannot do this for any reason, you can synchronize the scrolling programmatically. You just need to avoid the endless onScrolled() event onScrolled() , as mentioned in other answers. In other words, when you start programmed scrolling, you do nothing inside onScroll() until you finish programmed scrolling. And do not programmatically scroll through the RecyclerView that was originally scrolled.

Put all the baskets to sync in

 List<RecyclerView> syncRecyclers 

Call

 addSyncListeners() 

Enjoy

 public class SyncScrollActivity extends AppCompatActivity { private List<RecyclerView> syncRecyclers; private boolean isProgrammaticallyScrolling = false; private void addSyncListeners() { for (RecyclerView recyclerView : syncRecyclers) { recyclerView.addOnScrollListener(new SyncOnScrollListener()); } } private class SyncOnScrollListener extends RecyclerView.OnScrollListener { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (!isProgrammaticallyScrolling) { isProgrammaticallyScrolling = true; scrollAll(recyclerView, dx, dy); isProgrammaticallyScrolling = false; } } } private void scrollAll(RecyclerView exceptRecycler, int dx, int dy) { for (RecyclerView recyclerView : syncRecyclers) { if (!recyclerView.equals(exceptRecycler)) { recyclerView.scrollBy(dx, dy); } } } } 
+1
source

Create a variable in your pager adapter that will be responsible for maintaining the current scrollY position for your pages, and whenever your getItem(position) calls the ListView positoin update, look at CacheFragmentStatePagerAdapter at https://github.com/ksoichiro/Android- ObservableScrollView / blob / master / samples / src / main / java / com / github / ksoichiro / android / observablescrollview / samples / ViewPagerTabActivity.java

0
source

version of kotlin, this one doesn't handle fling (yet)

 class ScrollSynchronizer { val boundRecyclerViews: ArrayList<RecyclerView> = ArrayList() var verticalOffset = 0 private val scrollListener = object: RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) // scroll every other rv to the same position boundRecyclerViews.filter { it != recyclerView }.forEach { targetView -> targetView.removeOnScrollListener(this) targetView.scrollBy(dx, dy) targetView.addOnScrollListener(this) } } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (newState == RecyclerView.SCROLL_STATE_IDLE) { verticalOffset = recyclerView.computeVerticalScrollOffset() } } } fun add(view: RecyclerView) { with (view) { if (boundRecyclerViews.contains(view)) { removeOnScrollListener(scrollListener) boundRecyclerViews.remove(this) } addOnScrollListener(scrollListener) boundRecyclerViews.add(this) } } fun scrollToLastOffset(rv: RecyclerView) { rv.removeOnScrollListener(scrollListener) val currentOffset = rv.computeVerticalScrollOffset() if (currentOffset != verticalOffset) { rv.scrollBy(0, verticalOffset - currentOffset) } rv.addOnScrollListener(scrollListener) } fun disable() = boundRecyclerViews.forEach { it.removeOnScrollListener(scrollListener) } fun enable() = boundRecyclerViews.forEach { it.addOnScrollListener(scrollListener) } fun addAll(recyclerViews: List<RecyclerView>) = recyclerViews.forEach { add(it) } } 
0
source

All Articles