32

I am trying to implement a horizontal recyclerview and each item of the recyclerview will be a vertical recyclerview with a grid layout. The problem that i am facing is that when I try to scroll the child recyclerview vertically sometimes the parent recyclerview takes the scroll and starts scrolling horizontally. The approaches I tried to fix this are,

  1. setNestedScrollingEnabled(false) on the parent recyclerview
  2. In the onTouch() of the child recyclerview I disable touch events on the parent recyclerview by called requestdisallowinterceptTouchevent(false)

None of the above solutions provide a perfect fix for the problem. Any help is appreciated

Mann
  • 560
  • 3
  • 13
nikhil
  • 9,023
  • 22
  • 55
  • 81

14 Answers14

33

The problem seemed interesting to me. So I tried to implement and this is what I achieved (you can also see the video here) which is pretty smooth.

enter image description here

So you can try something like this:

Define CustomLinearLayoutManager extending LinearLayoutManager like this:

public class CustomLinearLayoutManager extends LinearLayoutManager {

    public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

    @Override
    public boolean canScrollVertically() {
        return false;
    }
}

and set this CustomLinearLayoutManager to your parent RecyclerView.

RecyclerView parentRecyclerView = (RecyclerView)findViewById(R.id.parent_rv);
CustomLinearLayoutManager customLayoutManager = new CustomLinearLayoutManager(this, LinearLayoutManager.HORIZONTAL,false);
parentRecyclerView.setLayoutManager(customLayoutManager);
parentRecyclerView.setAdapter(new ParentAdapter(this)); // some adapter

Now for child RecyclerView, define custom CustomGridLayoutManager extending GridLayoutManager:

public class CustomGridLayoutManager extends GridLayoutManager {

    public CustomGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public CustomGridLayoutManager(Context context, int spanCount) {
        super(context, spanCount);
    }

    public CustomGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) {
        super(context, spanCount, orientation, reverseLayout);
    }

    @Override
    public boolean canScrollHorizontally() {
        return false;
    }
}

and set it as layoutManger to the child RecyclerView:

childRecyclerView = (RecyclerView)itemView.findViewById(R.id.child_rv);
childRecyclerView.setLayoutManager(new CustomGridLayoutManager(context, 3));
childRecyclerView.setAdapter(new ChildAdapter()); // some adapter

So basically parent RecyclerView is only listening to horizontal scrolls and child RecyclerView is only listening to vertical scrolls.

Along with that, if you also want to handle diagonal swipe (which is little skewed to either vertical or horizontal), you can include a gesture listener in the parent RecylerView.

public class ParentRecyclerView extends RecyclerView {

    private GestureDetector mGestureDetector;

    public ParentRecyclerView(Context context) {
        super(context);
        mGestureDetector = new GestureDetector(this.getContext(), new XScrollDetector());
       // do the same in other constructors
    }

   // and override onInterceptTouchEvent

   @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev) && mGestureDetector.onTouchEvent(ev);
    }

}

Where XScrollDetector is

class XScrollDetector extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            return Math.abs(distanceY) < Math.abs(distanceX);
        }
}

Thus ParentRecyclerView asks child view (in our case, VerticalRecyclerView) to handle the scroll event. If the child view handles then parent does nothing else parent eventually handles the scroll.

Rohit Arya
  • 6,751
  • 1
  • 26
  • 40
  • Extending linear layout was a great advice. I almost changed my whole implementation to NestedScrollList. Looks like it saved me that effort. – wtk Jul 23 '16 at 11:23
  • 2
    Amazing answer, in my case I didn't need to use the entire solution, only the SimpleOnGestureListener part and it works perfectly. Thanks. – Pedro Okawa Apr 23 '18 at 10:57
  • Indeed, when using `LinearLayoutManager` it's enough to restrict diagonal scrolling. – Micha Nov 05 '19 at 13:54
  • No need of extending LinearLayoutManager because of explicitly disable horizontal / vertical scroll as you can see the implementation of [canScrollHorizontally](https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager.html#canscrollhorizontally) / [canScrollVertically](https://developer.android.com/reference/android/support/v7/widget/LinearLayoutManager.html#canscrollvertically) methods it is already using orientation you provide in constructor. – Shahab Rauf Nov 28 '19 at 08:54
8

setNestedScrollingEnabled(false) on the parent recyclerview

What you could try is setNestedScrollingEnabled(false) on the child RecyclerView, if any. RecyclerView 's nestedscroll-ness is that of a child (that's why it implements NestedScrollingChild).

In the onTouch() of the child recyclerview I disable touch events on the parent recyclerview by called requestdisallowinterceptTouchevent(false)

This should work, but what you should do is requestDisallowInterceptTouchEvent(true), not false. If you subclass RecyclerView, you can override onTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_UP) {
        // ensure we release the disallow request when the finger is lifted
        getParent().requestDisallowInterceptTouchEvent(false);
    } else {
        getParent().requestDisallowInterceptTouchEvent(true);
    }
    // Call the super class to ensure touch handling
    return super.onTouchEvent(event);
}

Or, with a touch listener from outside,

child.setOnTouchListener(new View.OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (v.getId() == child.getId()) {
            if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_UP) {
                // ensure we release the disallow request when the finger is lifted
                child.getParent().requestDisallowInterceptTouchEvent(false);
            } else {
                child.getParent().requestDisallowInterceptTouchEvent(true);
            }
        }
        // Call the super class to ensure touch handling
        return super.onTouchEvent(event);
    }
});
natario
  • 24,954
  • 17
  • 88
  • 158
8

I fixed this issue in a similar project by taking the opposite approach to you (and everyone else here).

Rather than allow the child to tell the parent when to stop looking at events, I let the parent decide when to ignore (based on direction). This approach requires a custom view though which can be a little more work. Below is what I created which would be used as the Outer/Parent view.

public class DirectionalRecyclerView extends RecyclerView {

    private static float LOCK_DIRECTION_THRESHOLD; //The slop
    private float startX;
    private float startY;
    private LockDirection mLockDirection = null;

    public DirectionalRecyclerView(Context context) {
        super(context);
        findThreshold(context);
    }

    public DirectionalRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        findThreshold(context)
    }

    public DirectionalRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        findThreshold(context);
    }

    private void findThreshold(Context context) {
        //last number is number of dp to move before deciding that's a direction not a tap, you might want to tweak it
        LOCK_DIRECTION_THRESHOLD = context.getResources().getDisplayMetrics().density * 12;
    }

    //events start at the top of the tree and then pass down to
    //each child view until they reach where they were initiated
    //unless the parent (this) method returns true for this visitor
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = event.getX();
                startY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (mLockDirection == null) {
                    float currentX = event.getX();
                    float currentY = event.getY();
                    float diffX = Math.abs(currentX - startX);
                    float diffY = Math.abs(currentY - startY);
                    if (diffX > LOCK_DIRECTION_THRESHOLD) {
                        mLockDirection = LockDirection.HORIZONTAL;
                    } else if (diffY > LOCK_DIRECTION_THRESHOLD) {
                        mLockDirection = LockDirection.VERTICAL;
                    }
                } else {
                    //we have locked a direction, check whether we intercept
                    //the future touches in this event chain
                    //(returning true steals children's events, otherwise we'll
                    // just let the event trickle down to the child as usual)
                    return mLockDirection == LockDirection.HORIZONTAL;
                }
                break;
            case MotionEvent.ACTION_UP:
                mLockDirection = null;
                break;
        }
        //dispatch cancel, clicks etc. normally
        return super.onInterceptTouchEvent(event);
    }

    private enum LockDirection {
        HORIZONTAL,
        VERTICAL
    }

}
Nick Cardoso
  • 20,807
  • 14
  • 73
  • 124
  • This worked for me for a different scenario - horizontal RV containing coordinator containing collapsing toolbar and nested scroll view. The only thing that had to change is to set `mLockDirection = null;` in `dispatchTouchEvent()` because in `onInterceptTouchEvent()` and `onTouchEvent()` are not called in all cases. – WindRider Mar 04 '17 at 11:17
  • Thanks for this valueable approach. The approach to handle this kind of stuff in parent is so much easier. I succeeded in making a vertical inside a horizontal viewpager with your modified logic. – Stefan Sprenger May 09 '17 at 07:07
5

Now you can try android:nestedScrollingEnabled because Google fixed a crash with usages of nestedScrollingEnabled (Issue 197932)

Umer
  • 1,566
  • 2
  • 20
  • 31
5

Set the listener to nested RecyclerView

 View.OnTouchListener listener = new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_MOVE
                            ) {
                        v.getParent().requestDisallowInterceptTouchEvent(true);

                    } else {
                        v.getParent().requestDisallowInterceptTouchEvent(false);

                    }
                    return false;
                }
            };

            mRecyclerView.setOnTouchListener(listener);
Shahab Rauf
  • 3,651
  • 29
  • 38
3

Try this. For my use-case it has worked:

nestedRecyclerView.setOnTouchListener(new View.OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return true;
    }
});
Andrew
  • 36,676
  • 11
  • 141
  • 113
3

try the below code, hope it will work.

nestedRecyclerView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    int action = event.getAction();
                   switch (action) {
                  case MotionEvent.ACTION_DOWN:
                     // Disallow parent to intercept touch events.
                     v.getParent().requestDisallowInterceptTouchEvent(true);
                     break;

                 case MotionEvent.ACTION_UP:
                    // Allow parent to intercept touch events.
                    v.getParent().requestDisallowInterceptTouchEvent(false);
            break;
        }

                  // Handle inner(child) touch events.
                    v.onTouchEvent(event);
        return true;
                }
            });
Pradeep Gupta
  • 1,770
  • 1
  • 9
  • 23
3

I used card view for cells and deactivated parent recycler view scrolling in child adapter with child cell itemView setOnClickListener.

holder.itemView.setOnTouchListener { view, _ ->
    view.parent.parent.requestDisallowInterceptTouchEvent(true)
    false
}

we are calling view.parent.parent because of the fact that itemView is a cell layout and it's parent is our child recyclerView and also, we need to reach child recyclerView's parent to prevent parent recyclerView scrolling.

1

Try below code to scroll inner RecyclerView.

innerRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

        @Override
        public void onTouchEvent(RecyclerView recycler, MotionEvent event) {
            // Handle on touch events here
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    // Disallow Parent RecyclerView to intercept touch events.
                    recycler.getParent().requestDisallowInterceptTouchEvent(true);
                    break;

                case MotionEvent.ACTION_UP:
                    // Allow Parent RecyclerView to intercept touch events.
                    recycler.getParent().requestDisallowInterceptTouchEvent(false);
                    break;
            }


        }

        @Override
        public boolean onInterceptTouchEvent(RecyclerView recycler, MotionEvent event) {
            return false;
        }

    });
pRaNaY
  • 24,642
  • 24
  • 96
  • 146
1

IMO, you can try the following inside the Adapter of outer RecyclerView:

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.cardview, parent, false);

        RVAdapter2 recyclerViewAdapter2 = new RVAdapter2();
        RecyclerView innerRV = (RecyclerView) v.findViewById(R.id.rv2);
        // Setup layout manager for items
        LinearLayoutManager layoutManager2 = new LinearLayoutManager(parent.getContext());
        // Control orientation of the items
        layoutManager2.setOrientation(LinearLayoutManager.VERTICAL);
        innerRV.setLayoutManager(layoutManager2);
        innerRV.setAdapter(recyclerViewAdapter2);

        innerRV.setOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
            }
        });

        return new MyViewHolder(v);
    }

For API23, you can also try innerRV.setOnScrollChangeListener because setOnScrollListener is deprecated.

UPDATE:

Another option is using addOnScrollListener instead of setOnScrollListener

Hope it helps!

BNK
  • 23,994
  • 8
  • 77
  • 87
1

Use this code to turn off scroll on recyclerview:

recyclerView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        return true;
    }
});
Austin Dean
  • 384
  • 3
  • 13
inkedTechie
  • 684
  • 5
  • 13
1

You should do this way:

innerRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

        @Override
        public void onTouchEvent(RecyclerView recycler, MotionEvent event) {
            // Handle on touch events here
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:

                    recycler.getParent().requestDisallowInterceptTouchEvent(true);

                    break;

                case MotionEvent.ACTION_UP:

                    recycler.getParent().requestDisallowInterceptTouchEvent(false);

                    break;

                case MotionEvent.ACTION_MOVE:

                    recycler.getParent().requestDisallowInterceptTouchEvent(true);

                    break;
            }


        }

        @Override
        public boolean onInterceptTouchEvent(RecyclerView recycler, MotionEvent event) {
            return false;
        }

    });

Hope this would help you.

Hiren Patel
  • 52,124
  • 21
  • 173
  • 151
0

The problem exists in Android's implementation of the onInterceptTouchEvent() method for RecyclerView. This blog calls out the problem and fixes it as well - http://nerds.headout.com/fix-horizontal-scrolling-in-your-android-app/. The only difference is that there the parent scrolls vertically and child horizontally. But the solution takes care that it should work for your situation as well.

  • Just in case anyone wondered, the linked article now gives a 404... but don't panic, it has been mirrored somewhere in China : https://www.tuicool.com/articles/ENbiqi – Paul W Apr 25 '19 at 18:50
-1

extend a custom layout manager like this

 public class CustomLayoutManager extends LinearLayoutManager {
 private boolean isScrollEnabled = true;

 public CustomGridLayoutManager(Context context) {
 super(context);
        }
  @Override
 public boolean canScrollVertically() {
 return false;
 }
}

Set the layout manager to this "Custom layout Manager"

Sarthak Mishra
  • 1,033
  • 1
  • 11
  • 27