15

I implemented the new ViewPager for my project. The viewPager2 contains a list of fragment

 private class ViewPagerAdapter extends FragmentStateAdapter {

    private ArrayList<Integer> classifiedIds;

    ViewPagerAdapter(@NonNull Fragment fragment, final ArrayList<Integer> classifiedIds) {
        super(fragment);
        this.classifiedIds = classifiedIds;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return DetailsFragment.newInstance(classifiedIds.get(position));
    }

    @Override
    public int getItemCount() {
        return classifiedIds.size();
    }
}

Inside the fragment I got an horizontal recyclerView

LinearLayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
recyclerViewPicture.setLayoutManager(layoutManager);

The issue is when I try to scroll the recyclerview the viewPager take the touch and swap to the next fragment

When I was using the old ViewPager I didn't have this issue

Vodet
  • 1,491
  • 1
  • 18
  • 36
  • Have you try with non swipable viewpager? You can stop swipe event on viewpager. – Prayag Gediya Aug 21 '19 at 08:47
  • @TakeInfos Yes I just try it and the recyclerview can scroll correctly. But I would like to keep the viewpager swipe – Vodet Aug 21 '19 at 08:49
  • Try `recyclerView.setNestedScrollingEnabled(true);` or this `ViewCompat.setNestedScrollingEnabled(recyclerView, true);` may be it will help. – Prayag Gediya Aug 21 '19 at 09:06
  • Yeah I try it but this is not working very well sometimes the recycler take the touch but sometimes no – Vodet Aug 21 '19 at 09:14
  • 2
    I have one trick but i haven't try it before. You will get last item visible state for recyclerview. so when recyclerview reach at last item you can enable user interaction for viewpager. `viewPager2.setUserInputEnabled(true)` otherwise set it `false` – Prayag Gediya Aug 21 '19 at 09:25

5 Answers5

21

I met the same problem: using AndroidX, a ViewPager2 (with horizontal orientation) having a RecyclerView (with horizontal orientation) inside one of its page.

The working solution I found is from Google issueTracker. Here is my Java translation of the Kotlin class:

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager2.widget.ViewPager2;

// from https://issuetracker.google.com/issues/123006042#comment21

/**
 * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
 * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
 * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
 *
 * This solution has limitations when using multiple levels of nested scrollable elements
 * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
 */

public class NestedScrollableHost extends FrameLayout {

    private int touchSlop = 0;
    private float initialX = 0.0f;
    private float initialY = 0.0f;

    private ViewPager2 parentViewPager() {
        View v = (View)this.getParent();
        while( v != null && !(v instanceof ViewPager2) )
            v = (View)v.getParent();
        return (ViewPager2)v;
    }

    private View child() { return (this.getChildCount() > 0 ? this.getChildAt(0) : null); }

    private void init() {
        this.touchSlop = ViewConfiguration.get(this.getContext()).getScaledTouchSlop();
    }

    public NestedScrollableHost(@NonNull Context context) {
        super(context);
        this.init();
    }

    public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.init();
    }

    public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.init();
    }

    public NestedScrollableHost(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        this.init();
    }

    private boolean canChildScroll(int orientation, Float delta) {
        int direction = (int)(Math.signum(-delta));
        View child = this.child();

        if( child == null )
            return false;

        if( orientation == 0 )
            return child.canScrollHorizontally(direction);
        if( orientation == 1 )
            return child.canScrollVertically(direction);

        return false;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        this.handleInterceptTouchEvent(ev);
        return super.onInterceptTouchEvent(ev);
    }

    private void handleInterceptTouchEvent(MotionEvent ev) {
        ViewPager2 vp = this.parentViewPager();
        if( vp == null )
            return;

        int orientation = vp.getOrientation();

        // Early return if child can't scroll in same direction as parent
        if( !this.canChildScroll(orientation, -1.0f) && !this.canChildScroll(orientation, 1.0f) )
            return;

        if( ev.getAction() == MotionEvent.ACTION_DOWN ) {
            this.initialX = ev.getX();
            this.initialY = ev.getY();
            this.getParent().requestDisallowInterceptTouchEvent(true);
        }
        else if( ev.getAction() == MotionEvent.ACTION_MOVE ) {
            float dx = ev.getX() - this.initialX;
            float dy = ev.getY() - this.initialY;
            boolean isVpHorizontal = (orientation == ViewPager2.ORIENTATION_HORIZONTAL);

            // assuming ViewPager2 touch-slop is 2x touch-slop of child
            float scaleDx = Math.abs(dx) * (isVpHorizontal ? 0.5f : 1.0f);
            float scaleDy = Math.abs(dy) * (isVpHorizontal ? 1.0f : 0.5f);

            if( scaleDx > this.touchSlop || scaleDy > this.touchSlop ) {
                if( isVpHorizontal == (scaleDy > scaleDx) ) {
                    // Gesture is perpendicular, allow all parents to intercept
                    this.getParent().requestDisallowInterceptTouchEvent(false);
                }
                else {
                    // Gesture is parallel, query child if movement in that direction is possible
                    if( this.canChildScroll(orientation, (isVpHorizontal ? dx : dy)) ) {
                        this.getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    else {
                        // Child cannot scroll, allow all parents to intercept
                        this.getParent().requestDisallowInterceptTouchEvent(false);
                    }
                }
            }
        }
    }
}

Then, just embed your nested RecyclerView inside a NestedScrollableHost container:

<mywishlist.sdk.Base.NestedScrollableHost
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/photos"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/photolist_collection_background"
        android:orientation="horizontal">

    </androidx.recyclerview.widget.RecyclerView>

</mywishlist.sdk.Base.NestedScrollableHost>

It solved my scrolling conflict between the nested RecyclerView and its hosting ViewPager2.

Nicolas Buquet
  • 3,880
  • 28
  • 28
  • 4
    This works for me, thanks! I've used a modified version mentioned in the discussion on Google, available here: https://gist.github.com/micer/a6169a84acf200b9d44b2d942600b139 - Kotlin – Micer Aug 24 '20 at 11:26
  • Your link is the original Kotlin version I adapted to Java :-) – Nicolas Buquet Aug 24 '20 at 13:06
  • Yep exactly, just with few minor changes. – Micer Aug 25 '20 at 07:34
  • 2
    It worked for me. FYI, this is a component from Android Widgets --> https://github.com/android/views-widgets-samples/blob/master/ViewPager2/app/src/main/java/androidx/viewpager2/integration/testapp/NestedScrollableHost.kt – GreatNews Dec 12 '21 at 17:32
14

I find a solution it's a know bug as you can see here https://issuetracker.google.com/issues/123006042 maybe they would solve it in the next updates

Thanks to TakeInfos and the exemple project inside the link

 recyclerViewPicture.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
        int lastX = 0;
        @Override
        public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
            switch (e.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastX = (int) e.getX();
                    break;
                case MotionEvent.ACTION_MOVE:
                    boolean isScrollingRight = e.getX() < lastX;
                    if ((isScrollingRight && ((LinearLayoutManager) recyclerViewPicture.getLayoutManager()).findLastCompletelyVisibleItemPosition() == recyclerViewPicture.getAdapter().getItemCount() - 1) ||
                            (!isScrollingRight && ((LinearLayoutManager) recyclerViewPicture.getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0)) {
                       viewPager.setUserInputEnabled(true);
                    } else {
                        viewPager.setUserInputEnabled(false);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    lastX = 0;
                    viewPager.setUserInputEnabled(true);
                    break;
            }
            return false;
        }

        @Override
        public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        }
    });

I'm checking if the user scroll on the right or on the left. If the user reach the end or the start of the recyclerView I'm enable or disable the swipe on the view pager

Vodet
  • 1,491
  • 1
  • 18
  • 36
  • 1
    Check the NestedRecyclerViewSameOrientation.zip Aug 8 2019 inside the issuetracker link. I don t remember but maybe I'm using it for find the solution – Vodet Oct 07 '20 at 07:37
  • Thanks, I downloaded it on https://issuetracker.google.com/action/issues/123006042/attachments/26855080?download=true – Gary Chen Oct 07 '20 at 11:45
1

The simplest would be this: First listen to page change and disable touch on the page where you have recyclerView:

    myPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            when(position){
                2 -> myPager.isUserInputEnabled = false //for recycler view
                else -> myPager.isUserInputEnabled = true
            }
            super.onPageSelected(position)
        }
    })

Then on the View(s) you want the user to be able to swipe Page, do this:

viewForPageSwipe.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    myPager?.isUserInputEnabled = true
                    viewForPageSwipe.onTouchEvent(event)
                    return@setOnTouchListener true
                }
            }
            return@setOnTouchListener false
        }

depending on your logic, you'll probably have to pass the myPager object to your recycleView Fragment.

M. Usman Khan
  • 3,689
  • 1
  • 59
  • 69
0

In my opinion, this solution (stolen from Daniel Knauf post) is much simpler than creating a wrapper but still not official:

recyclerViewPicture.addOnItemTouchListener(
    object : RecyclerView.OnItemTouchListener {
        private var startX = 0f

        override fun onInterceptTouchEvent(
            recyclerView: RecyclerView,
            event: MotionEvent
        ): Boolean =
            when (event.action) {
                MotionEvent.ACTION_DOWN -> startX = event.x
                MotionEvent.ACTION_MOVE -> {
                    val isScrollingRight = event.x < startX
                    val scrollItemsToRight = isScrollingRight && recyclerView.canScrollRight
                    val scrollItemsToLeft = !isScrollingRight && recyclerView.canScrollLeft
                    val disallowIntercept = scrollItemsToRight || scrollItemsToLeft
                    recyclerView.parent.requestDisallowInterceptTouchEvent(disallowIntercept)
                }
                MotionEvent.ACTION_UP -> startX = 0f
                else -> Unit
            }.let { false }

        override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) = Unit
        override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) = Unit
    }
)

val RecyclerView.canScrollRight: Boolean
    get() = canScrollHorizontally(SCROLL_DIRECTION_RIGHT)

val RecyclerView.canScrollLeft: Boolean
    get() = canScrollHorizontally(SCROLL_DIRECTION_LEFT)

private const val SCROLL_DIRECTION_RIGHT = 1
private const val SCROLL_DIRECTION_LEFT = -1
DropDrage
  • 725
  • 8
  • 9
-1

Call ViewGroup#onInterceptTouchEvent(MotionEvent).

See This Documentation

Raafat Alhmidi
  • 1,106
  • 12
  • 18