14

Is there any way to set an onClickListener on a RecyclerView?

I have a RecyclerView with some children in it, and setting an OnClickListener on the parent RecyclerView. However, the onClick doesn't fire when I click on that view. See sample code below -- we want to get clicks on the parent, NOT the children. In this scenario we don't care about clicks on the items.

I have tried doing setFocusable(false), setClickable(false), and setOnClickListener(null) on the children to no avail. In any case I don't think the children are stealing clicks from the parent, because when I click on the area where there is no children, the clicks don't register either.

package com.formagrid.hellotest;

import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.Arrays;
import java.util.List;

public class HelloActivity extends Activity {

    private RecyclerView mRecyclerView;
    private RecyclerAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_hello);

        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mAdapter = new RecyclerAdapter(Arrays.asList("hi", "this", "is", "some", "text"));
        mRecyclerView.setAdapter(mAdapter);
        mRecyclerView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d("patricia", view.toString());
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
    }

    public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.Holder> {

        public class Holder extends RecyclerView.ViewHolder {

            protected TextView textView;

            public Holder(TextView itemView) {
                super(itemView);
                this.textView = itemView;
            }

        }

        private List<String> contents;

        public RecyclerAdapter(List<String> contents) {
            this.contents = contents;
        }

        @Override
        public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new Holder(new TextView(parent.getContext()));
        }

        @Override
        public void onBindViewHolder(Holder holder, int position) {
            holder.textView.setText(contents.get(position));
        }

        @Override
        public int getItemCount() {
            return contents.size();
        }

    }

}
Patricia Li
  • 1,346
  • 10
  • 19

4 Answers4

9

Is there any way to set an onClickListener on a RecyclerView?

No. That is, you can set an OnClickListener, but RecyclerView will never call it. RecyclerView intercepts all touch events, but never calls performClick(), which is how View invokes its listener.

You can, however, simulate an OnClickListener with an OnTouchListener and a GestureDetector. For the GestureDetector's listener, we can use a SimpleOnGestureListener, implementing just the onSingleTapUp() method.

class ClickListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        Toast.makeText(HelloActivity.this, "Clicked", 0).show();
        return true;
    }
};

Then we just need to feed the GestureDetector the MotionEvents from an OnTouchListener, and check the return to decide whether to consume the event, so as to not interfere with scrolling, dragging, etc.

final GestureDetector detector = new GestureDetector(HelloActivity.this, new ClickListener());

mRecyclerView.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if(detector.onTouchEvent(event)) {
                return true;
            }
            return false;
        }
    }
);

Please note that the above solution works with pretty much only a "simple" RecyclerView, like the one described and given in the question. If you're using more involved item handling, like drag and drop or swipe, we'll need to handle our gesture detection a little further up the touch event chain.

To do this, we can subclass RecyclerView and perform the detection in the dispatchTouchEvent() method. If a single tap is detected, we simply call performClick(), which will fire the RecyclerView's OnClickListener.

public class ClickableRecyclerView extends RecyclerView {

    private final GestureDetectorCompat detector;

    public ClickableRecyclerView(Context context) {
        this(context, null);
    }

    public ClickableRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        detector = new GestureDetectorCompat(context, new ClickListener());
    }

    private class ClickListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            performClick();
            return true;
        }
    };

    @Override
    public boolean dispatchTouchEvent(MotionEvent e) {
        detector.onTouchEvent(e);
        return super.dispatchTouchEvent(e);
    }
}

Just use this subclass in place of your regular RecyclerView, and set an OnClickListener on it as you normally would. Additional constructors may be necessary, depending on how you're instantiating this.

Mike M.
  • 38,532
  • 8
  • 99
  • 95
  • 1
    that could get hairy for us. do you happen to know why `RecyclerView` never calls `performClick`? – Patricia Li Jul 08 '16 at 02:53
  • 1
    Well, you'd have to ask the designers for a definite answer, but IMO, it's because it was never meant to be used that way. Usually when presenting the user with a list, you're more concerned with the interaction on the individual items. How might this get hairy for you? If you're concerned about the extra code, you could implement this in a `RecyclerView` subclass, so as not to pollute your `Activity`'s code. Or, if you mean you're already using gestures, this functionality would just need to be added to what you already have. – Mike M. Jul 08 '16 at 03:01
  • 1
    If you're dead set on using your `OnClickListener`, you can subclass `RecyclerView` to call `performClick()` yourself, but you're gonna have to track the touch events to differentiate between a simple click and, e.g., dragging, but that's basically all the `GestureDetector` is doing anyway. – Mike M. Jul 08 '16 at 03:15
  • ahh, gotcha. we're using a decorator to reorder the items on long press, and I was just worried that setting on `onTouchListener` could interfere with that. but if this is the way to go, I'm sure we can make it work - I haven't actually tried it yet. thanks! – Patricia Li Jul 08 '16 at 18:28
  • Oh, uhh, I didn't test this with long press on the items. Honestly, I didn't even think to, given your description - "we don't care about clicks on the items" - and the fact that the API calls it long _click_. Hmm, I'll have to do a test for that when I get a chance a little later. Should still be workable, but we might need to change some things around; e.g., might need to let the `GestureDetector` handle the long press, too. – Mike M. Jul 08 '16 at 19:26
  • Yeah, you're right. That can be hairy. I hadn't done `RecyclerView` drag and drop yet. If you're using a custom `ItemDecoration`, you can just find where you're tracking touch events, and insert your click functionality there. Should be simple, if you're using a `GestureDetector`; just add an `onSingleTapUp()` override to your listener. If you're using an `ItemTouchHelper`, that's gonna be more involved, 'cause it uses its own private `GestureDetector`. I've not yet figured out an elegant solution for that. Atm, I've just forked `ItemTouchHelper`. I'll let ya know if I come up with something. – Mike M. Jul 09 '16 at 06:38
  • Aw, jeez, I think I've been making this way too difficult. I believe I've got a fix that should work with whatever your implementation is, though you might have to fine tune the visuals, since I've no idea what your actual setup is. Lemme know how it works for ya. – Mike M. Jul 10 '16 at 02:49
  • 1
    ok, thanks. I think we just need to play around it for a bit. accepting answer since it more than answers my original question :) – Patricia Li Jul 12 '16 at 20:24
3

This is how I have done in Kotlin style

fun RecyclerView.enableClickListener(){
    val gesture = object : GestureDetector.SimpleOnGestureListener(){
        override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
            this@enableClickListener.performClick()
            return super.onSingleTapConfirmed(e)
        }
    }
    val detector = GestureDetector(this.context, gesture)
    this.setOnTouchListener { v, event -> detector.onTouchEvent(event) }
}

And this is how to use it

yourRecyclerView.apply {
        enableClickListener()
        setOnClickListener {
           // Do what you want  ...
        }
    }

Enjoy :)

S.Prapagorn
  • 621
  • 6
  • 14
0

Here is a trick, It's open for comments.

You can have 2 children items in a FrameLayout. The first being the RecyclerView and the second a View

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
        <View
            android:id="@+id/over_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            />
    </FrameLayout>

Since its a FrameLayout the View would be on top of the RecyclerView and you can set an onClickListener to the View and it would behave as if it was the RecyclerView that was clicked

over_view.setOnClickListener {
    .....
}
-2

a click event on RecycleView ? try like this:

//set android:descendantFocusability="blocksDescendants" on parent layout 
//then setOnClickListener

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
**android:descendantFocusability="blocksDescendants"**
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
  android:layout_width="match_parent"
  android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>

</LinearLayout>
Cgx
  • 753
  • 3
  • 6