Grokking Android

Getting Down to the Nitty Gritty of Android Development

Selecting Items of a RecyclerView using StateListDrawables

By

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 is not correct, or maybe it's the current L preview, or - more likely - it's my code. Any takers? Please let me know, if you know what's wrong here. Thanks!

Here's a screenshot of the final result. You can also find a video near the end of this post.

RecyclerView with Contextual ActionBar and selected items
RecyclerView with Contextual ActionBar and selected items

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:

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:

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:

A short video showing the Contextual ActionBar and the selection of items

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.

Wolfram Rittmeyer lives in Germany and has been developing with Java for many years.

He has been interested in Android for quite a while and has been blogging about all kind of topics around Android.

You can find him on Google+ and Twitter.