0

I have set up my Activity to have a gestureDetectorCompat that detects left and right swipes, but my Activity also has a horizontal RecyclerView in it.

When I try to scroll the RecyclerView Activity getsureDetector triggers too, which is undesirable.

Here is the code related to gestureDetector if it helps.

private GestureDetectorCompat mDetector;
private static final int SWIPE_MIN_DISTANCE = 120;
private static final int SWIPE_MAX_OFF_PATH = 250;
private static final int SWIPE_THRESHOLD_VELOCITY = 200;
mDetector = new GestureDetectorCompat(this, new SwipeDetector());
public boolean onTouchEvent(MotionEvent event){
        this.mDetector.onTouchEvent(event);
        return super.onTouchEvent(event);
    }

public boolean dispatchTouchEvent(MotionEvent ev){
        if (mDetector != null){
            if (mDetector.onTouchEvent(ev)){
                return true;
            }
        }
        return super.dispatchTouchEvent(ev);
    }

public class SwipeDetector extends GestureDetector.SimpleOnGestureListener{
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY){

            if (Math.abs(e1.getY() - e2.getY()) > SWIPE_MAX_OFF_PATH){
                return false;
            }

            if( e2.getX() > e1.getX() ){
                if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY){
                    Left();
                    return true;
                }
            }

            if( e1.getX() > e2.getX() ){
                if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY){
                    Right();
                    return true;
                }
            }
            return false;
        }
    }

1 Answers1

0

To only trigger the RecyclerView's touch events without passing them back to the parental views, you need to "consume" that event by returning true in the corresponding touch methods, like this:

recyclerView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return true; //consume touch event
            }
        });

Also, remove the call to onTouchEvent() inside your parent view (activity)'s dispatchTouchEvent so that it doesn't trigger the touch event before the touch event is propagated to its child views (in your case, the RecyclerView) and get consumed.


Alternatively, you can intercept all the touch events passed to RecyclerView and handle them yourself (like consuming them conditionally) by overriding onInterceptTouchEvent in RecyclerView's item touch listener. This is helpful for when you also have to deal with child views inside the RecyclerView and want to conditionally let the child views get or not get touch events (dispatching them). An example would be something like this:

recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener(){

            @Override
            public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
                return true;
            }

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

            }

            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

            }
        });

You can also have more refined control over when to intercept events and when not to by checking the action of the incoming touch event/recyclerview state and return true to consume or false to pass onto other parent views. For example:

@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
    if (e.getAction() == MotionEvent.ACTION_DOWN && rv.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
        Log.d(TAG, "onInterceptTouchEvent: click performed");
        rv.findChildViewUnder(e.getX(), e.getY()).performClick();
        return true;
    }
    return false;
}
Jack
  • 5,354
  • 2
  • 29
  • 54
  • Actually I haven't touched the recyclerview touch events so they are default, I only need it scroll when there are more items than space to show them like it does by default. Can I somehow avoid rewriting this functionallity or should I just @Override it and copy paste default code and change return false(that I assume is there) to return true? – Mazzinger Pawa Apr 19 '19 at 03:34
  • If you leave `onTouchEvent` blank, I think the default behavior will remain. All you need to do is change the behavior of `onInterceptTouchEvent` – Jack Apr 19 '19 at 03:37
  • what do you mean by "default code"? do you have other code for your RecyclerView's touch events? If so what are you using, you can probably still override `onInterceptTouchEvent` but it's somewhere else. If you don't have other code however, only modify the default `onInterceptTouchEvent` and leave all the other methods alone should be fine – Jack Apr 19 '19 at 03:42
  • Basically, if you don't have any code to deal with RecyclerViews, then something like the example in my answer should just work fine and all the default behavior shouldn't be changed (except for consuming the event of course). – Jack Apr 19 '19 at 03:51
  • I meant looking up the way it is done by default and just copy pasting that and changing it. No, no other code, the bare minumum in fact, it is a very simple RecyclerView. Anyway what you are saying looks like exactly what I need, but I tried setting `onInterceptTouchEvent` to `return true` and now it won't scroll at all, setting it to `false` does let me scroll the `RecyclerView` so functionallity is still there – Mazzinger Pawa Apr 19 '19 at 04:05
  • Ah I see what you mean, let me update my answer, I think there's a better way to deal with this in your situation – Jack Apr 19 '19 at 04:25
  • I have updated the answer, did it solve your problem? – Jack Apr 19 '19 at 05:32
  • For some reason no, from everything I've read so far it sounds like it should work. I've tried using different views, but still touch event doesn't get consumed. Maybe the problem is my implementation of `gestureDetector` – Mazzinger Pawa Apr 19 '19 at 16:43
  • I don't think it's the problem with your `gestureDetector`, because you have to consume to touch events in the child views to avoid the parent views to process them (although you can deal with them manually in the parent view to achieve the same result, it would be much more complicated). What if in the `onTouch` method, you just return true, does the RecyclerView still scroll? – Jack Apr 19 '19 at 20:26
  • For some stupid reason, I messed up the view hierarchy and the corresponding methods to consume the touch events, I think simply returning true in `onTouch` should work for you. – Jack Apr 19 '19 at 20:47
  • Ok so I think I figured it out, the problem was in the `dispatchTouchEvent`, it triggers at the very beggining of the touch event and by calling `gestureDetector` from there it goes around the usual touch event lifecycle (had to spend some time figuring that out since I mindlessly copied the gestureDetector from somewhere), so no `true` down the line could affect it. But apperently this was done this way since otherwise if any other clickable view was the start of the touch event the gestureDetector would not get triggered, – Mazzinger Pawa Apr 22 '19 at 06:09
  • and it was good that it was there since my activity screen that uses the gesture was almost entirely made of clickable views. Without this modified `dispatchTouchEvent` I would have to `setOnTouchListener` for every cliclable view individually and have them call `gestureDetector(event)` also on top of rewriting the `onClick` handlers for them. luckily the left and right swipes that I needed for my gestureDetector would most likely occur on only one ListView which takes up most of the screen space, so I just added a gestureDetector for it too, – Mazzinger Pawa Apr 22 '19 at 06:09
  • but since I needed an `onItemClick` functionallity I added an `onSingleTap` method to it like shown here https://stackoverflow.com/a/5174757/9672320 Furthermore, that RecyclerView is now nested inside each of the ListView items(each one is unique to each item, so different size and etc) and if user tries to scroll it, it consumes the touch event so `ListView`'s `onTouch` that calls the `gestureDetector` does not get triggered. – Mazzinger Pawa Apr 22 '19 at 06:10
  • Ideally of course I would love for a) gestureDetector working everywhere on the screen without the hassle of going through every clickable view, maybe can be simplified if I use ViewGroup for the children or smth. b) touch event not being consumed if the recyclerView does not need to be scrolled when it already fits on the screen. But even at this point the current solution is mostly acceptable @jackz314 – Mazzinger Pawa Apr 22 '19 at 06:11
  • Oh this reminded me, you have `dispatchTouchEvent` in your parent view as well, can you try removing `mDetector.onTouchEvent(ev)` inside the `dispatchTouchEvent` method, that should fix it (if you return true now inside its child views, this parent view shouldn't be triggered). I don't think you need to override every child view's touch listener to make the `onTouchEvent` in this parent view to get triggered – Jack Apr 22 '19 at 06:14
  • In fact, I'm not 100% sure but I think you can even remove the touch listener for the recyclerview and just let the default behavior happen, that way, I think the touch events won't get consumed when the recyclerview is unscrollable. Just remove the call to `onTouchEvent` and see what happens – Jack Apr 22 '19 at 06:24