Preventing RecyclerView from consuming touch events

vguzzi picture vguzzi · Mar 29, 2016 · Viewed 14.9k times · Source

Problem

I am attempting to simulate the exact behaviour of the Google Play application.

Currently when you're scrolling through a category like 'Top Games' and you touch the list, that cell as well as the RecyclerView handles the touch event, you can tell they are both handling it due to the ripple appearing when you hold your finger down as well as the scrolling coming to a stop. Or if you click on a cell whilst the list is still moving that content is opened straight away.

The default behaviour is for the scrolling to instantly stop if you place your finger down whilst it's moving, and for it then to consume that touch event, meaning the cell never gets told about that touch. This is incredibly irritating when your applications main content is in a RecyclerView and if the user is continuously scrolling then clicking on a cell, it requires them to click twice, as the first click has been consumed by stopping the scroll, even if the list was barely moving.

What I've Tried So Far

At first I thought there must be some kind of flag inside the RecyclerView which tells it not to consume the touch event. I've looked through the Android source code and routed through the Android documentation to no avail.

Then I tried to capture the touch event with onInterceptTouchEvent and manually tell my view that it's being touched;

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    View myCell = recyclerView.findChildViewUnder(event.getX(), event.getY());
    myCell.dispatchTouchEvent(event);
    return super.onInterceptTouchEvent(event);
}

Here, dispatchTouchEvent didn't seem to do anything even though the view I was getting had the corresponding tag which I set in the adapter.

I've tried shooting this problem with my code machine gun at all angles; playing with requestDisallowInterceptTouchEvent, manually calling onTouch and overriding all manner of listeners for the RecyclerView and beyond.

Nothing seems to be working. I'm sure there must be an elegant solution to this and I've skimmed over an important detail somewhere along the path to insanity? Especially because Google's own apps behave in this manner.

Please help! :) Thanks.

The Solution

Thanks to Jagoan Neon a solution has been found. I ended up wrapping his answer inside a custom RecyclerView for ease of use.

public class MultiClickRecyclerView extends RecyclerView {
   public MultiClickRecyclerView(Context context) {
        super(context);
   }

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

   public MultiClickRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
   }

   @Override
   public boolean onInterceptTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN && this.getScrollState() == RecyclerView.SCROLL_STATE_SETTLING) {
            this.stopScroll();
        }
        return super.onInterceptTouchEvent(event);
   }
}

Answer

Jagoan Neon picture Jagoan Neon · Mar 30, 2016

First off you should define an OnItemTouchListener, perhaps as a global variable like this:

RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() {
    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        if (e.getAction() == MotionEvent.ACTION_DOWN && rv.getScrollState() == RecyclerView.SCROLL_STATE_SETTLING) {
            Log.d(TAG, "onInterceptTouchEvent: click performed");
            rv.findChildViewUnder(e.getX(), e.getY()).performClick();
            return true;
        }
        return false;
    }

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

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
};

Why ACTION_DOWN and SCROLL_STATE_SETTLING?

ACTION_DOWN motion event will be triggered when you perform a touch on the RecyclerView, and at that exact point in time RecyclerView changes its scrolling state to SCROLL_STATE_SETTLING as it tries to stop the scrolling. And that's exactly when you want to perform the click.

And remember to return true so that you're telling your RecyclerView that you've consumed the MotionEvent. Otherwise your performClick() might be called more than once.

Add it to your RecyclerView at onResume and remove it at onPause, and you're all set ;)

UPDATE

Add a null checker when you're doing findChildViewUnder - it returns null when you're not touching a specific cell.

View childView = rv.findChildViewUnder(e.getX(), e.getY());
if (childView != null) childView.performClick();

UPDATE 2

Turns out that stopping the RecyclerView from scrolling would suffice. Do this instead:

@Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        if (e.getAction() == MotionEvent.ACTION_DOWN && rv.getScrollState() == RecyclerView.SCROLL_STATE_SETTLING)
            rv.stopScroll();
        return false;
    }