10

I'm using a ViewPager2 with two fragments in a vertical orientation. When the user swipes down to the second fragment, there is a RecyclerView that scrolls content in the same vertical direction.

The issue is that when I scroll the contents of the RecyclerView, sometimes the ViewPager2 catches the scroll events and sometimes the RecyclerView catches the scroll events.

I would like it so that when the user is scrolling to the top of the RecyclerView, the ViewPager only swipes back up to the first fragment when the user has reached the top of the contents in the RecyclerView.

I've tried using recyclerView.isNestedScrollingEnabled = false without much luck. I also tried putting the RecyclerView into a NestedScrollView, but that is not recommended because the RecyclerView then creates every single ViewHolder it needs for the dataset and that is obviously not efficient.

Calvin Rai
  • 816
  • 1
  • 8
  • 17

3 Answers3

25

So...I was able to figure it out by just reading some documentation . I'll post the answer here so that it helps anyone else having a similar issue:

Since ViewPager2 does not supported nested scroll views very well, unlike NestedScrollView, we need to wrap our nested scrollview with a custom wrapper in our layout to be able to handle the touch and swipe events that are getting intercepted by our nested scroll views parent. In our case, the child would be the RecyclerView and the parent would be the ViewPager2.

You can find the wrapper class here. Simply add it to your project and then wrap your scrollable view in it, similar to below:

    <NestedScrollableHost
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/my_recycler_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical" />

    </NestedScrollableHost>

There are a couple things to note here: The documentation says that this solution will not work for scrollable views that are within other scrollable views within the ViewPager. This solution only works for immediate scroll views of the ViewPager.

Another note is that the wrapper class uses the requestDisallowInterceptTouchEvent() to make sure that the child scrollable view tells the parent not to scroll if the child needs to scroll instead.

Calvin Rai
  • 816
  • 1
  • 8
  • 17
  • anybody can provide java version of NestedScrollableHost class? – Ali Zarei Apr 04 '20 at 09:35
  • 1
    In my case vertical recyclerview is facing scroll issues, it is jerky and most of the times viewpager2 picks the swipe action and horizontal swipe occurs. – Usman Rana Jan 25 '22 at 08:15
4

The best solution i got was to use gestureDetector.SimpleOnGestureListener inside on recyclerView.addOnItemTouchListener(this).

Step-1: In OnCreate() method. The GestureListener class is created on Step-3.

gestureDetector = new GestureDetector(getActivity(), new GestureListener());

Step-2: Call recyclerView.addOnItemTouchListener() method.

recyclerView.addOnItemTouchListener(this);

Step-3: Create class GestureListener which extend GestureDetector.SimpleOnGestureListener.

public class GestureListener extends GestureDetector.SimpleOnGestureListener {
    private final int Y_BUFFER = 10;

    @Override
    public boolean onDown(MotionEvent e) {
        // Prevent ViewPager from intercepting touch events as soon as a DOWN is detected.
        // If we don't do this the next MOVE event may trigger the ViewPager to switch
        // tabs before this view can intercept the event.
        Log.d("vp", "true1");
        recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
        return super.onDown(e);
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (Math.abs(distanceX) > Math.abs(distanceY)) {
            Log.d("vp2", "true");
            // Detected a horizontal scroll, allow the viewpager from switching tabs
            recyclerView.getParent().requestDisallowInterceptTouchEvent(false);
        } else if (Math.abs(distanceY) > Y_BUFFER) {
            // Detected a vertical scroll prevent the viewpager from switching tabs
            Log.d("vp3", "false");
            recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
        }
        return super.onScroll(e1, e2, distanceX, distanceY);
    }
}

Step-4: Call gestureDetector.onTouchEvent(e) from onInterceptTouchEvent().

@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
    gestureDetector.onTouchEvent(e);
    return false;
}

UPDATE

Kotlin Version

This is the kotlin version of the previous answer.

val gestureDetector = GestureDetector(requireActivity(), object: GestureDetector.SimpleOnGestureListener() {
    override fun onDown(e: MotionEvent?): Boolean {
        rv.parent.requestDisallowInterceptTouchEvent(true)
        return super.onDown(e)
    }

    override fun onScroll(
        e1: MotionEvent?,
        e2: MotionEvent?,
        distanceX: Float,
        distanceY: Float
    ): Boolean {
        if (abs(distanceX) > abs(distanceY)) {
            rv.parent.requestDisallowInterceptTouchEvent(false)
        } else if (abs(distanceY) > 10) {
            rv.parent.requestDisallowInterceptTouchEvent(true)
        }
        return super.onScroll(e1, e2, distanceX, distanceY)
    }
})

rv.addOnItemTouchListener(object: RecyclerView.OnItemTouchListener {
    override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {}
    override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
    override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
        gestureDetector.onTouchEvent(e)
        return false
    }
})
Wachid Susilo
  • 496
  • 4
  • 11
Prakash
  • 69
  • 7
  • This was save my time. It is very helped me when i have recyclerView inside the viewPager2. – Prakash Apr 19 '20 at 10:52
  • This is really help me. Thanks. I've added the kotlin version into your answer. – Wachid Susilo Jul 08 '22 at 16:07
  • The solution works, but keep in mind your RecyclerView has to be a direct child of your ViewPager2; there cannot be any other layout wrapping your RecyclerView. – moyo Jan 20 '23 at 08:25
-1

class NestedScrollableHost : FrameLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    private var touchSlop = 0
    private var initialX = 0f
    private var initialY = 0f
    private val parentViewPager: ViewPager2?
        get() {
            var v: View? = parent as? View
            while (v != null && v !is ViewPager2) {
                v = v.parent as? View
            }
            return v as? ViewPager2
        }

    private val child: View? get() = if (childCount > 0) getChildAt(0) else null

    init {
        touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    }

    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            0 -> child?.canScrollHorizontally(direction) ?: false
            1 -> child?.canScrollVertically(direction) ?: false
            else -> throw IllegalArgumentException()
        }
    }

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        handleInterceptTouchEvent(e)
        return super.onInterceptTouchEvent(e)
    }

    private fun handleInterceptTouchEvent(e: MotionEvent) {
        val orientation = parentViewPager?.orientation ?: return

        // Early return if child can't scroll in same direction as parent
        if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
            return
        }

        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            parent.requestDisallowInterceptTouchEvent(true)
        } else if (e.action == MotionEvent.ACTION_MOVE) {
            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL

            // assuming ViewPager2 touch-slop is 2x touch-slop of child
            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                if (isVpHorizontal == (scaledDy > scaledDx)) {
                    // Gesture is perpendicular, allow all parents to intercept
                    parent.requestDisallowInterceptTouchEvent(false)
                } else {
                    // Gesture is parallel, query child if movement in that direction is possible
                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        // Child can scroll, disallow all parents to intercept
                        parent.requestDisallowInterceptTouchEvent(true)
                    } else {
                        // Child cannot scroll, allow all parents to intercept
                        parent.requestDisallowInterceptTouchEvent(false)
                    }
                }
            }
        }
    }
}
Nur Gazi
  • 47
  • 2