Selecting Items of a RecyclerView using StateListDrawables
Last week, after I published my introduction to RecyclerView
Paul Betts asked on Twitter whether ItemDecorators
are useful for displaying the selection state of items.
Well, I think using them for selection is not the right way to go. Instead, I think that you should stick with StateListDrawables and the activated state.
The use case of the sample
In this sample the user can select multiple items that are shown in purple. The first item gets selected using a long click. Any subsequent click will select or deselect items. Users can also deselect items, that were previously selected, with a single click. The user can then use a bulk delete operation on all selected items. The user should be able to see the number of currently selected items.
Obviously that sounds a lot like a contextual action bar (CAB), which is what I used. BTW: As you can see in the screenshot, the CAB doesn't look like I expected it to look. It should use my android:colorPrimaryDark
for the contextual app bar (Material lingo). But either the theming guide for material design
Here's a screenshot of the final result. You can also find a video near the end of this post.
Overview
The solution that I suggest is Adapter-based. As you might recall from my last post, RecyclerView
doesn't care about the visual representation of individual items. Thus I quickly ruled out to subclass RecyclerView
.
RecyclerView
doesn't itself need to know about the set of items nor about the state these items are in. In this way my proposed solution differs from the way you did selections with ListView or GridView in the past. There you checked items directly using the setItemChecked() method and you set the kind of selection mode with setChoiceMode().
With RecyclerView
any information about the data set belongs to your RecyclerView.Adapter
subclass. Thus anything required to show the selection state of items should also be in your RecyclerView.Adapter
subclass.
The adapter not only stores information about the state of each item, but it also creates the views for each items. That's why I use the adapter for setting the activated state.
Methods for setting the selected state
Based on this use case, I chose to add the following methods to my RecyclerView.Adapter
subclass:
void toggleSelection(int pos)
void clearSelections()
int getSelectedItemCount()
List<Integer> getSelectedItems()
With toggleSelection()
an item changes its selection state. If it was previously selected it gets deselected, and vice versa.
You can always clear all selections with clearSelections()
. You shouldn't forget to do that when you finish the action mode.
The other methods get the number of currently selected items and all positions of the currently selected items.
Necessary changes to the adapter
Here's the relevant part of my Adapter:
public class RecyclerViewDemoAdapter
extends RecyclerView.Adapter
<RecyclerViewDemoAdapter.ListItemViewHolder> {
// ...
private SparseBooleanArray selectedItems;
// ...
public void toggleSelection(int pos) {
if (selectedItems.get(pos, false)) {
selectedItems.delete(pos);
}
else {
selectedItems.put(pos, true);
}
notifyItemChanged(pos);
}
public void clearSelections() {
selectedItems.clear();
notifyDataSetChanged();
}
public int getSelectedItemCount() {
return selectedItems.size();
}
public List<Integer> getSelectedItems() {
List<Integer> items =
new ArrayList<Integer>(selectedItems.size());
for (int i = 0; i < selectedItems.size(); i++) {
items.add(selectedItems.keyAt(i));
}
return items;
}
// ...
}
Notice how I used notifyDataSetChanged()
and notifyItemChanged()
. That's necessary because I do not have access to the View
object itself and thus cannot set the activated state directly. Instead I have to tell Android to ask the Adapter for a new ViewHolder
binding.
How to use those methods from within the Activity
If you have used ListViews
with the Contextual ActionBar in the past, you know that for selecting multiple items you had to set the choice mode to CHOICE_MODE_MULTIPLE_MODAL
and implement the AbsListview.MultiChoiceModeListener interface to achieve the desired result. See this guide on Android's developer site for more details. Now since RecyclerView
doesn't offer this interface (and rightly so), you have to find a way around this.
My solution is to use the GestureDetector
to detect long presses. You can find this code at the end of the Activity. In the long-press callback, I create the actionmode, detect which view was pressed and call toggleSelection()
on the adapter.
public void onLongPress(MotionEvent e) {
View view =
recyclerView.findChildViewUnder(e.getX(), e.getY());
if (actionMode != null) {
return;
}
actionMode =
startActionMode(RecyclerViewDemoActivity.this);
int idx = recyclerView.getChildPosition(view);
myToggleSelection(idx);
super.onLongPress(e);
}
private void myToggleSelection(int idx) {
adapter.toggleSelection(idx);
String title = getString(
R.string.selected_count,
adapter.getSelectedItemCount());
actionMode.setTitle(title);
}
For the actionmode to work, the activity has to implement the ActionMode.Callback interface. I won't go into this. It's described in detail in the official menu guide on the Android site.
To understand of how selections work only two of this interface's methods are interesting:
onActionItemClicked()
Android calls this method when the user presses the delete icon. In this method I get the list of selected items from the adapter and call the
removeData()
method of the adapter for each item - so that Android can smoothly animate them - as shown in the previous post. And, of course, I have to finish the action mode afterwards.onDestroyActionMode()
Android calls this method when it's leaving actionmode prior to showing the normal ActionBar (app bar) again. This happens either when the user selects the check mark or when he/she selects the delete icon.
Here's the code for these two methods:
@Override
public boolean onActionItemClicked(
ActionMode actionMode,
MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.menu_delete:
List<Integer> selectedItemPositions =
adapter.getSelectedItems();
for (int i = selectedItemPositions.size() - 1;
i >= 0;
i--) {
adapter.removeData(selectedItemPositions.get(i));
}
actionMode.finish();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode actionMode) {
this.actionMode = null;
adapter.clearSelections();
}
The StateListDrawable XML file
So far I have shown you how to select items and set the activated state. To finally highlight those items I use StateListDrawables
.
A StateListDrawable
is a drawable that changes its content based on the state of the view. You can define those in XML files. The first matching entry for a given state determines which drawable Android uses. Since I only care about the activated state the XML file is actually very simple:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android=
"http://schemas.android.com/apk/res/android">
<item android:state_activated="true"
android:drawable="@color/primary_dark" />
<item android:drawable="@android:color/transparent" />
</selector>
And of course you have to use this StateListDrawable
somewhere. I use it as the background for each item:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container_list_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/statelist_item_background">
<include layout="@layout/common_item_layout" />
</RelativeLayout>
The following video shows the result of this post's changes:
Sample project
You can download the sample from my github page. I have tagged the revision of last week's post with "simpleSample" while the revision for this week uses the tag "selectionSample".
Feel free to submit pull requests if you have suggestions on how to improve the code.
And that's it for today
This was a short example of how to make use of the RecyclerView.Adapter
and how to benefit from those abstractions. I guess I won't go into much more detail about the adapter in future posts. But I recommend that you take a look at Gabriele Mariotti's great example of how to use an RecyclerView.Adapter with sectioned lists.
I hope this post helps you in your work with RecyclerView.Adapters
. Selection is just one of the many things an adapter is useful for. If you have a look at Gabriele's gist, you can see how to use your adapter to support different view types. Just keep in mind the separation of concerns and don't mix responsibilities.