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); } }
Use btn. setEnabled(false) to temporarily disable it and then btn. setEnabled(true) to enable it again.
To make sure that each view correctly receives the touch events intended for it, override the onInterceptTouchEvent() method.
1.1. You can react to touch events in your custom views and your activities. Android supports multiple pointers, e.g. fingers which are interacting with the screen. The base class for touch support is the MotionEvent class which is passed to Views via the onTouchEvent() method. you override the onTouchEvent() method.
To disable multi touch in recyclerview, you can use android:splitMotionEvents="false" in your recyclerview tag in layout file. By that attribute, you will not receive multi touch in recyclerview.
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; }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With