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.