I have a RecyclerView (say, rootRecyclerView
) that can have different kinds of rows depending on some API response. I implemented one of them is a horizontal ViewPager2
and another one is implemented with horizontal RecyclerView
(say, childRecyclerView
).
The rootRecyclerView swipes vertically whereas the viewPager2 and childRecyclerView swipes horizontally.
The Problem:
When I swipe on the screen, if the swipe is on the the viewPager2
or childRecyclerView
, the swipe MUST go perfectly straight horizontally. Otherwise, they won’t scroll horizontally; the swipe is taken by the rootRecyclerView
and so the you would see vertical movement.
So, this happens because your thumb would move in a curved/circular direction creating movement in both the X axis and Y axis, and the so the rootRecyclerView intercepts the swipe creating this unpleasant user experience.
I did try to solve the issue, such as adding an OnItemTouchListener
to the childRecyclerView like this:
private float Y_BUFFER = ViewConfiguration.get(getContext()) .getScaledPagingTouchSlop(); // 10; private float preX = 0f; private float preY = 0f; childRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { if(e.getAction()==MotionEvent.ACTION_DOWN){ childRecyclerView.getParent().requestDisallowInterceptTouchEvent(true); } if(e.getAction() == MotionEvent.ACTION_MOVE){ if (Math.abs(e.getX() - preX) > Math.abs(e.getY() - preY)) { childRecyclerView.getParent().requestDisallowInterceptTouchEvent(true); } else if (Math.abs(e.getY() - preY) > Y_BUFFER) { childRecyclerView.getParent().requestDisallowInterceptTouchEvent(false); } } preX = e.getX(); preY = e.getY(); return false; } // ... rest of the code
It solves the problem only for the childRecyclerView
, but I could not solve it for the ViewPager2
.
I have also tried to use GestureDetector
as described in this answer link, and some other combinations of code, but I could not make it work.
Could anyone help me?
Advertisement
Answer
Okay, so after some research, I came to the conclusion of substituting my ViewPager2
with a recyclerView
that will ‘behave like’ a viewPager :/ .
First I replaced my viewPager2
with a horizontal recyclerView
. To make it behave like a viewpager, use SnapHelper
.
RecyclerView childRecyclerView2 = findViewById(R.id.previously_viewPager); // other init like setup layout manager, adapter etc SnapHelper snapHelper = new PagerSnapHelper(); snapHelper.attachToRecyclerView(replacedRecyclerView); // <-- this makes out rv behave like a viewPager
After that, you have to add an OnItemTouchListener
and override onInterceptTouchEvent
just like the code segment in my question:
childRecyclerView2.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { @Override public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { // same as the code segment in the question, //so skipping this part. //just copy it from my question } // ... }
Optional:
In viewPager2, you can get the current focus with getCurrentItem()
, but since we have replaced out viewpager2 with recyclerview, we don’t have that method. So, we need to implement our own equivalent version. If you are a Kotlin guy, you can directly jump to the reference 2 and skip this part. Here is the java version if you need, I’ll skip the explanation though.
Create SnapHelperExt.java
public class SnapHelperExt { public SnapHelperExt(){} public int getSnapPosition(RecyclerView recyclerView, SnapHelper snapHelper){ RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); View snapView = snapHelper.findSnapView(layoutManager); if (snapView != null) { return layoutManager.getPosition(snapView); }else{ return -1; } } }
Next create an interface OnSnapPositionChangeListener
as our listener :
public interface OnSnapPositionChangeListener { void onSnapPositionChange(int position); }
After that, create SnapOnScrollListener.java
:
public class SnapOnScrollListener extends RecyclerView.OnScrollListener { public enum Behavior { NOTIFY_ON_SCROLL, NOTIFY_ON_SCROLL_STATE_IDLE } private SnapHelperExt snapHelperExt; private SnapHelper snapHelper; private Behavior behavior; private OnSnapPositionChangeListener onSnapPositionChangeListener; private int snapPosition = RecyclerView.NO_POSITION; public SnapOnScrollListener(SnapHelper snapHelper, Behavior behavior, OnSnapPositionChangeListener onSnapPositionChangeListener){ this.snapHelper = snapHelper; this.behavior = behavior; this.onSnapPositionChangeListener = onSnapPositionChangeListener; this.snapHelperExt = new SnapHelperExt(); } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (behavior == Behavior.NOTIFY_ON_SCROLL) { maybeNotifySnapPositionChange(recyclerView); } } @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (behavior == Behavior.NOTIFY_ON_SCROLL_STATE_IDLE && newState == RecyclerView.SCROLL_STATE_IDLE) { maybeNotifySnapPositionChange(recyclerView); } } private void maybeNotifySnapPositionChange(RecyclerView recyclerView){ int prevPosition = this.snapHelperExt.getSnapPosition(recyclerView, snapHelper); boolean snapPositionIsChanged = (this.snapPosition!=prevPosition); if(snapPositionIsChanged){ onSnapPositionChangeListener.onSnapPositionChange(prevPosition); this.snapPosition = prevPosition; } } }
Finally, use it in this way:
SnapOnScrollListener snapOnScrollListener = new SnapOnScrollListener( snapHelper, SnapOnScrollListener.Behavior.NOTIFY_ON_SCROLL, position -> { Log.e(TAG, "currently focused page no = "+position); // your code here, do whatever you want } ); childRecyclerView2.addOnScrollListener(snapOnScrollListener);
References: