93

I am using following code for handling row clicks. (source)

static class RecyclerTouchListener implements RecyclerView.OnItemTouchListener {

    private GestureDetector gestureDetector;
    private ClickListener clickListener;

    public RecyclerTouchListener(Context context, final RecyclerView recyclerView, final ClickListener clickListener) {
        this.clickListener = clickListener;
        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
                if (child != null && clickListener != null) {
                    clickListener.onLongClick(child, recyclerView.getChildPosition(child));
                }
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {

        View child = rv.findChildViewUnder(e.getX(), e.getY());
        if (child != null && clickListener != null && gestureDetector.onTouchEvent(e)) {
            clickListener.onClick(child, rv.getChildPosition(child));
        }
        return false;
    }

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

This works however, if I want to have say a delete button on each row. I am not sure to how to implement that with this.

I attached OnClick listener to delete button which works (deletes the row) but it also fires the onclick on full row.

Can anybody help me in how to avoid full row click if a single button is clicked.

Thanks.

Mattia Maestrini
  • 32,270
  • 15
  • 87
  • 94
Ashwani K
  • 7,880
  • 19
  • 63
  • 102

7 Answers7

144

this is how I handle multiple onClick events inside a recyclerView:

Edit : Updated to include callbacks (as mentioned in other comments). I have used a WeakReference in the ViewHolder to eliminate a potential memory leak.

Define interface :

public interface ClickListener {

    void onPositionClicked(int position);
    
    void onLongClicked(int position);
}

Then the Adapter :

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
    
    private final ClickListener listener;
    private final List<MyItems> itemsList;

    public MyAdapter(List<MyItems> itemsList, ClickListener listener) {
        this.listener = listener;
        this.itemsList = itemsList;
    }

    @Override public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.my_row_layout), parent, false), listener);
    }

    @Override public void onBindViewHolder(MyViewHolder holder, int position) {
        // bind layout and data etc..
    }

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

    public static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {

        private ImageView iconImageView;
        private TextView iconTextView;
        private WeakReference<ClickListener> listenerRef;

        public MyViewHolder(final View itemView, ClickListener listener) {
            super(itemView);

            listenerRef = new WeakReference<>(listener);
            iconImageView = (ImageView) itemView.findViewById(R.id.myRecyclerImageView);
            iconTextView = (TextView) itemView.findViewById(R.id.myRecyclerTextView);

            itemView.setOnClickListener(this);
            iconTextView.setOnClickListener(this);
            iconImageView.setOnLongClickListener(this);
        }

        // onClick Listener for view
        @Override
        public void onClick(View v) {

            if (v.getId() == iconTextView.getId()) {
                Toast.makeText(v.getContext(), "ITEM PRESSED = " + String.valueOf(getAdapterPosition()), Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(v.getContext(), "ROW PRESSED = " + String.valueOf(getAdapterPosition()), Toast.LENGTH_SHORT).show();
            }
            
            listenerRef.get().onPositionClicked(getAdapterPosition());
        }


        //onLongClickListener for view
        @Override
        public boolean onLongClick(View v) {

            final AlertDialog.Builder builder = new AlertDialog.Builder(v.getContext());
            builder.setTitle("Hello Dialog")
                    .setMessage("LONG CLICK DIALOG WINDOW FOR ICON " + String.valueOf(getAdapterPosition()))
                    .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {

                        }
                    });

            builder.create().show();
            listenerRef.get().onLongClicked(getAdapterPosition());
            return true;
        }
    }
}

Then in your activity/fragment - whatever you can implement : Clicklistener - or anonymous class if you wish like so :

MyAdapter adapter = new MyAdapter(myItems, new ClickListener() {
            @Override public void onPositionClicked(int position) {
                // callback performed on click
            }

            @Override public void onLongClicked(int position) {
                // callback performed on click
            }
        });

To get which item was clicked you match the view id i.e. v.getId() == whateverItem.getId()

Hope this approach helps!

Community
  • 1
  • 1
Mark
  • 9,604
  • 5
  • 36
  • 64
  • 1
    Thanks, I was using this pattern only for my implementation. However the issue was something else. Have a look at here http://stackoverflow.com/questions/30287411/issue-with-cardview-and-onclicklistener-in-recyclerview – Ashwani K May 17 '15 at 13:37
  • How would you get to know that which row's button has been clicked? – Bharat Dodeja Mar 21 '16 at 06:47
  • `getAdapterPosition()` gets the row. – Mark Mar 21 '16 at 08:32
  • 1
    Where is "this" being pulled from? There is no this within the adapter unless you match up the context, and when I do that for some reason it is Casting View.OnclickListener to it as well – Lion789 Mar 24 '16 at 23:40
  • 2
    `this` is referencing itself, the ViewHolder (which is a separate static class in this case) - you are setting the listener on the Viewholder which has your data bound to it in `onBindViewHolder()`, it has nothing to do with the context in the Adapter. I don't know what problems you're exactly having, but this solution works fine. – Mark Mar 25 '16 at 10:29
  • Thanks dear.. after 11 hr hunting for this issue on google and stackoverflow, finally i found a simple and impressive solution for recycleview click problem. – JSB Apr 21 '16 at 06:16
  • My touch feedback of the view has disabled once this is implemented, Has anybody encountered – Yasitha Waduge Jun 01 '16 at 15:39
  • 1
    @YasithaChinthaka Have you tried setting this attribute : `android:background="?attr/selectableItemBackground"` in your xml for the view? – Mark Jun 01 '16 at 23:20
  • i don't know which way you made it work properly, but for me it hadn't worked untill i have made similar implementation but in `onBindViewHolder()` – Sirop4ik Aug 15 '16 at 10:59
  • 2
    Just wanna drop by and say thanks, this solution is really easy to follow and implement. – CodeGeass May 11 '17 at 09:44
  • 1
    Thank you , this helped me a lot ! – Laidi Oussama Jul 25 '17 at 09:06
  • Where to put the interface declarations? In the Adapter class itself? Edit: Oh nvm. It is a file when created a new Java Class and select "Interface". – GeneCode Mar 27 '22 at 03:10
  • To get which button is clicked in SAME ROW, I found out that I pass the clicked view as Button (typecast), and made use of the tag property of the button. Works like a charm – GeneCode Mar 27 '22 at 03:47
61

I find that typically:

  • I need to use multiple listeners because I have several buttons.
  • I want my logic to be in the activity and not the adapter or viewholder.

So @mark-keen's answer works well but having an interface provides more flexibility:

public static class MyViewHolder extends RecyclerView.ViewHolder {

    public ImageView iconImageView;
    public TextView iconTextView;

    public MyViewHolder(final View itemView) {
        super(itemView);

        iconImageView = (ImageView) itemView.findViewById(R.id.myRecyclerImageView);
        iconTextView = (TextView) itemView.findViewById(R.id.myRecyclerTextView);

        iconTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickListener.iconTextViewOnClick(v, getAdapterPosition());
            }
        });
        iconImageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickListener.iconImageViewOnClick(v, getAdapterPosition());
            }
        });
    }
}

Where onClickListener is defined in your adapter:

public MyAdapterListener onClickListener;

public interface MyAdapterListener {

    void iconTextViewOnClick(View v, int position);
    void iconImageViewOnClick(View v, int position);
}

And probably set through your constructor:

public MyAdapter(ArrayList<MyListItems> newRows, MyAdapterListener listener) {

    rows = newRows;
    onClickListener = listener;
}

Then you can handle the events in your Activity or wherever your RecyclerView is being used:

mAdapter = new MyAdapter(mRows, new MyAdapter.MyAdapterListener() {
                    @Override
                    public void iconTextViewOnClick(View v, int position) {
                        Log.d(TAG, "iconTextViewOnClick at position "+position);
                    }

                    @Override
                    public void iconImageViewOnClick(View v, int position) {
                        Log.d(TAG, "iconImageViewOnClick at position "+position);
                    }
                });
mRecycler.setAdapter(mAdapter);
LordParsley
  • 3,808
  • 2
  • 30
  • 41
  • This is another way, and builds on top of my answer (one similar I use myself), however how are you passing `onClickListener` to the static nested Viewholder class? Unless I'm missing something I can't see how you pass it to your ViewHolder. Also if you just use one interface method you could use a Lambda expression, which condenses everything down. – Mark Sep 19 '16 at 08:38
  • public MyAdapterListener onClickListener; is a member variable defined in your adapter in the code above and set in your adapter constructor. (Alternatively, but not shown above, you could also use a custom setter like setOnClickListener.) – LordParsley Sep 22 '16 at 10:36
  • 6
    I was just asking how you are accessing a member/instance variable in your Adapter class from within a static nested class (ViewHolder). – Mark Sep 22 '16 at 12:11
  • Like Mark, I was not able to nest inside the adapter. see my answer on how to avoid having to nest – Tony BenBrahim Nov 07 '16 at 23:58
  • @MarkKeen ..exactly the same question I had . – user2695433 May 29 '17 at 06:00
9

I wanted a solution that did not create any extra objects (ie listeners) that would have to be garbage collected later, and did not require nesting a view holder inside an adapter class.

In the ViewHolder class

private static class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        private final TextView ....// declare the fields in your view
        private ClickHandler ClickHandler;

        public MyHolder(final View itemView) {
            super(itemView);
            nameField = (TextView) itemView.findViewById(R.id.name);
            //find other fields here...
            Button myButton = (Button) itemView.findViewById(R.id.my_button);
            myButton.setOnClickListener(this);
        }
        ...
        @Override
        public void onClick(final View view) {
            if (clickHandler != null) {
                clickHandler.onMyButtonClicked(getAdapterPosition());
            }
        }

Points to note: the ClickHandler interface is defined, but not initialized here, so there is no assumption in the onClick method that it was ever initialized.

The ClickHandler interface looks like this:

private interface ClickHandler {
    void onMyButtonClicked(final int position);
} 

In the adapter, set an instance of 'ClickHandler' in the constructor, and override onBindViewHolder, to initialize `clickHandler' on the view holder:

private class MyAdapter extends ...{

    private final ClickHandler clickHandler;

    public MyAdapter(final ClickHandler clickHandler) {
        super(...);
        this.clickHandler = clickHandler;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder viewHolder, final int position) {
        super.onBindViewHolder(viewHolder, position);
        viewHolder.clickHandler = this.clickHandler;
    }

Note: I know that viewHolder.clickHandler is potentially getting set multiple times with the exact same value, but this is cheaper than checking for null and branching, and there is no memory cost, just an extra instruction.

Finally, when you create the adapter, you are forced to pass a ClickHandlerinstance to the constructor, as so:

adapter = new MyAdapter(new ClickHandler() {
    @Override
    public void onMyButtonClicked(final int position) {
        final MyModel model = adapter.getItem(position);
        //do something with the model where the button was clicked
    }
});

Note that adapter is a member variable here, not a local variable

Tony BenBrahim
  • 7,040
  • 2
  • 36
  • 49
  • Thankyou for your answer :) One thing i want to add here don't use adapter.getItem(position) rather than use yourmodel.get(position) – Khubaib Raza Aug 06 '19 at 22:25
6

Just wanted to add another solution if you already have a recycler touch listener and want to handle all of the touch events in it rather than dealing with the button touch event separately in the view holder. The key thing this adapted version of the class does is return the button view in the onItemClick() callback when it's tapped, as opposed to the item container. You can then test for the view being a button, and carry out a different action. Note, long tapping on the button is interpreted as a long tap on the whole row still.

public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener
{
    public static interface OnItemClickListener
    {
        public void onItemClick(View view, int position);
        public void onItemLongClick(View view, int position);
    }

    private OnItemClickListener mListener;
    private GestureDetector mGestureDetector;

    public RecyclerItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener)
    {
        mListener = listener;

        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener()
        {
            @Override
            public boolean onSingleTapUp(MotionEvent e)
            {
                // Important: x and y are translated coordinates here
                final ViewGroup childViewGroup = (ViewGroup) recyclerView.findChildViewUnder(e.getX(), e.getY());

                if (childViewGroup != null && mListener != null) {
                    final List<View> viewHierarchy = new ArrayList<View>();
                    // Important: x and y are raw screen coordinates here
                    getViewHierarchyUnderChild(childViewGroup, e.getRawX(), e.getRawY(), viewHierarchy);

                    View touchedView = childViewGroup;
                    if (viewHierarchy.size() > 0) {
                        touchedView = viewHierarchy.get(0);
                    }
                    mListener.onItemClick(touchedView, recyclerView.getChildPosition(childViewGroup));
                    return true;
                }

                return false;
            }

            @Override
            public void onLongPress(MotionEvent e)
            {
                View childView = recyclerView.findChildViewUnder(e.getX(), e.getY());

                if(childView != null && mListener != null)
                {
                    mListener.onItemLongClick(childView, recyclerView.getChildPosition(childView));
                }
            }
        });
    }

    public void getViewHierarchyUnderChild(ViewGroup root, float x, float y, List<View> viewHierarchy) {
        int[] location = new int[2];
        final int childCount = root.getChildCount();

        for (int i = 0; i < childCount; ++i) {
            final View child = root.getChildAt(i);
            child.getLocationOnScreen(location);
            final int childLeft = location[0], childRight = childLeft + child.getWidth();
            final int childTop = location[1], childBottom = childTop + child.getHeight();

            if (child.isShown() && x >= childLeft && x <= childRight && y >= childTop && y <= childBottom) {
                viewHierarchy.add(0, child);
            }
            if (child instanceof ViewGroup) {
                getViewHierarchyUnderChild((ViewGroup) child, x, y, viewHierarchy);
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e)
    {
        mGestureDetector.onTouchEvent(e);

        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView view, MotionEvent motionEvent){}

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    }
}

Then using it from activity / fragment:

recyclerView.addOnItemTouchListener(createItemClickListener(recyclerView));

    public RecyclerItemClickListener createItemClickListener(final RecyclerView recyclerView) {
        return new RecyclerItemClickListener (context, recyclerView, new RecyclerItemClickListener.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                if (view instanceof AppCompatButton) {
                    // ... tapped on the button, so go do something
                } else {
                    // ... tapped on the item container (row), so do something different
                }
            }

            @Override
            public void onItemLongClick(View view, int position) {
            }
        });
    }
vipes
  • 922
  • 1
  • 9
  • 17
2

You need to return true inside onInterceptTouchEvent() when you handle click event.

Milad Faridnia
  • 9,113
  • 13
  • 65
  • 78
  • 1
    Hi, can u be more elaborate on this. I am using following code to bind to delete button btnDelete = (ImageButton) itemView.findViewById(R.id.btnDelete); btnDelete.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { remove(getLayoutPosition()); } }); – Ashwani K May 17 '15 at 06:50
  • Like onTouchEvent () the return value is indicates whether the event has been handled or not, and when its not the event passed to the full row. – Eliyahu Shwartz May 18 '15 at 07:33
2

Just put an override method named getItemId Get it by right click>generate>override methods>getItemId Put this method in the Adapter class

Anurag Bhalekar
  • 830
  • 7
  • 9
1

You can check if you have any similar entries first, if you get a collection with size 0, start a new query to save.

OR

more professional and faster way. create a cloud trigger (before save)

check out this answer https://stackoverflow.com/a/35194514/1388852

Hatim
  • 1,516
  • 1
  • 18
  • 30