Search code examples
androidandroid-recyclerviewtouchontouchlistenermotionevent

How to pass a click event to RecyclerView's items?


I have an ordinary RecyclerView, and on top of it a transparent View that implements GestureListener, which basically have the same size of the RecyclerView.

The GestureListener will listen to scroll and fling gestures, and pass this MotionEvent to the RecyclerView underneath it.

I have already made the RecyclerView able to scroll and fling. However, I can't find a way to pass a click event down to the RecyclerView's items as well.

I already know that this is because ACTION_DOWN is consumed in the GestureListener. In fact, GestureListener has a onSingleTap() method for you to override, and this method was called whenever I perform a click.

According to this post, I tried to set an OnTouchListener to my itemView and listen to ACTION_UP events. However, the onTouch() method is never called.

Below is how I do it:
1. Create a callback in the transparent GestureListener

   @Override
    public boolean onSingleTapUp(MotionEvent e) {
        if (scrollDetector == null) return false;
        scrollDetector.onSingleTap(e);
        return true;
    }
  1. Configure the callback in the activity, and pass the MotionEvent to the RecyclerView

    @Override
    public void onSingleTap(MotionEvent e) {
        mRecyclerView.onTouchEvent(e);
    }
    
  2. Set OnTouchListener to the itemView in the adapter:

    itemView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_UP) {
                v.performClick();
                return true;
            }
            return false;
        }
    });
    

Using debugger, I can see that mRecyclerView.onTouchEvent(e) was called; but the onTouch() of itemView was not called.

So... How should I correctly pass the MotionEvent to the itemView?

You may ask - "Why do you place a GestureListener on top of the RecyclerView?"
This is because I need to change the height of the RecyclerView when the RecyclerView is scrolled. However, if I do this using RecyclerView's addOnScrollListener, the value of dy will fluctuate between negative and positive values, because dy is affected by its height as well. And the fluctuation will also be reflected to the UI.
Therefore I need a scroll detector that does not change its height when scrolled, and just pass the scroll and fling values to RecyclerView by programmatically calling scrollBy() and fling().


Solution

  • Stupid me. I should use dispatchTouchEvent(MotionEvent e) instead of onTouchEvent(MotionEvent e).

    However, this is not enough.
    Simply calling dispatchTouchEvent(e) using the MotionEvent from GestureListener is not working, because that e is an ACTION_UP event.

    To simulate a click, you need both ACTION_DOWN and ACTION_UP.

    And itemView does not need to set OnTouchListener since you have already simulate

    Code:

            @Override
            public void onSingleTap(MotionEvent e) {
                long downTime = SystemClock.uptimeMillis();
                long upTime = downTime + 100;
                MotionEvent downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
                        e.getX(), e.getY(), 0);
                mRecyclerView.dispatchTouchEvent(downEvent);
                MotionEvent upEvent = MotionEvent.obtain(upTime, upTime, MotionEvent.ACTION_UP,
                        e.getX(), e.getY(), 0);
                mRecyclerView.dispatchTouchEvent(upEvent);
                downEvent.recycle();
                upEvent.recycle();
            }