Getting your clicks on RecyclerView

RecyclerView is a great class that you should consider over ListView for building list interfaces. It offers more flexibility and has built-in hooks that make implementing animations and custom layouts much easier compared to ListView.

Unfortunately RecyclerView is missing a couple of features that ListView had built-in. For example the ability to add an OnItemClickListener that triggers when an item is clicked. RecyclerView allows you to set an OnClickListener in your adapter, but passing on that click listener from your calling code, to the adapter and to the ViewHolder, is complicated for catching a simple item click.

Fortunately, RecyclerView supplies a addOnItemTouchListener that will catch all touch events on the item View. You can hook up a GestureDetector to figure out what happens and trigger an action from there. This has some problems as well. If you don’t implement this correctly, the GestureDetector will steal touch events and will mess up things like ripples. Also, the target view is never actually receiving the touch events in that case so your code has to emulate what happens when you click on a view to handle haptic feedback, sound effects and accessibility events.

I came up with a solution, which is to let the view which is the item in your RecyclerView, or more precisely, the ViewHolder.getItemView() handle the click.

The resulting code to hook up a click listener now looks like this:

ItemClickSupport.addTo(mRecyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
    @Override
    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
        // do it
    }
});

Users of TwoWayView may notice how similar this is to ItemClickSupport in that library. Actually, I used TwoWayViews’ version in the Bundle app before this, but encountered problems because it was using the touch listener technique. Once I implemented my own ItemClickSupport I went back to check the internals of the version in TwoWayView and noticed that both implementations are pretty similar. I really like the elegant API that Lucas came up with when implementing this in TwoWayView: no more passing around click listeners!

The main difference compared to the TwoWayView version is that my version uses a OnChildAttachStateChangeListener to set a OnClickListener on the itemView of the ViewHolder without using a custom OnTouchListener.

Here’s the implementation:

public class ItemClickSupport {
    private final RecyclerView mRecyclerView;
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;
    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mOnItemClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
        }
    };
    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            if (mOnItemLongClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
            return false;
        }
    };
    private RecyclerView.OnChildAttachStateChangeListener mAttachListener
            = new RecyclerView.OnChildAttachStateChangeListener() {
        @Override
        public void onChildViewAttachedToWindow(View view) {
            if (mOnItemClickListener != null) {
                view.setOnClickListener(mOnClickListener);
            }
            if (mOnItemLongClickListener != null) {
                view.setOnLongClickListener(mOnLongClickListener);
            }
        }

        @Override
        public void onChildViewDetachedFromWindow(View view) {

        }
    };

    private ItemClickSupport(RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
        mRecyclerView.setTag(R.id.item_click_support, this);
        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
    }

    public static ItemClickSupport addTo(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support == null) {
            support = new ItemClickSupport(view);
        }
        return support;
    }

    public static ItemClickSupport removeFrom(RecyclerView view) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
        if (support != null) {
            support.detach(view);
        }
        return support;
    }

    public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
        return this;
    }

    public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
        mOnItemLongClickListener = listener;
        return this;
    }

    private void detach(RecyclerView view) {
        view.removeOnChildAttachStateChangeListener(mAttachListener);
        view.setTag(R.id.item_click_support, null);
    }

    public interface OnItemClickListener {

        void onItemClicked(RecyclerView recyclerView, int position, View v);
    }

    public interface OnItemLongClickListener {

        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
    }
}

You also need to define R.id.item_click_support using ids.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="item_click_support" type="id" />
</resources>

This code can be used freely, if you need to show your legal dept something, send them here.

I prefer setting a click listener using this method over passing around OnClickListeners and such. Of course it only covers the simple case where the whole item needs to be clickable.

So next time you need your items to be clicked…consider this technique ;)

Thanks to Mike Wolfson and Dave Smith for proofreading this post.



Questions, remarks or other feedback? Discuss this post on Google+