Grokking Android

Getting Down to the Nitty Gritty of Android Development

Selecting Items of a RecyclerView using StateListDrawables

By 20 Comments

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.

20 thoughts on “Selecting Items of a RecyclerView using StateListDrawables”

  1. I had the same problem about theming the CAB.
    Simply add this to your application them near the actionbar style

    @style/action_mode_style
    @style/action_mode_style

    This issue is not documented well ๐Ÿ™‚

    Hope this will solve the them problems

    1. Where is action_mode_style defined? Is it part of the framework?

  2. I am also interested in theming the CAB. Can you go in more detail on how can this be accomplished?

  3. can you post link to the source? can’t find it..

    1. It’s on my github page: https://github.com/writtmeyer/recyclerviewdemo.

  4. About CAB theming, if you use AppCompat you can specify “actionModeBackground” and “actionMenuTextColor” in your base theme

  5. Somehow if I longpress anywhere it is considered to be an element. For example if my list is empty and I make a longpress the empty list acts as it is selectable. Furthermore selected items don’t change style when selected, but I think this and the first problem are related. Do you have a clue how to fix this?

    1. I fixed the problem myself. First problem was that I had to check if my view != null in the onLongPress method and check for my ListItem’s Id in the onClick method. I also fixed the problem with the changing style/colour if an item is selected. I simply forgot to call viewHolder.itemView.setActivated in my onBindViewHolder method. So thanks for the tutorial!

  6. Hello sir, i would like to ask how to use fragments and add some data like using providers. i have my 1st app using content providers and i want to put into fragments. and it seems it needs a lot of changes to do for.

    I’m just a beginner and lots of things coming out from my mind.

    thank you sir..

  7. Hi – I’m grokking this! Almost done with first Enterprise Droid app with exception of having to replace my single select spinner with multi select/color…so that we can be like the iOS team’s app …

    I found one other example – http://v4all123.blogspot.com/2013/09/spinner-with-multiple-selection-in.html but I don’t like the check box at all (too much like SAP or older tech).

    Appreciate your blog post for helping us first timers. Your search blog was helpful as well.

    We’re going into production with our app Friday – potential 90,000 employees …of which I’m guessing 45% droid due to Asia/Emea but Apple is also prevelant due to corporate pushing…

    Would be interested in your take on Android Stats / Future (Geographical, Age or any other indicator of the level of acceptance by a “group” – I’ve seen that iOS is still King of the World but I don’t see what age groups are into the segments) Lastly, interested in APIs – I see the future here…love Google Places/Maps apis…

    Cheers! (Bookmarked now)

    1. Wolfram Rittmeyer

      Hope your app is going to be well accepted!

      I personally don’t think, iOS is king of the world. Though, granted for the enterprise context: Many execs tend to have iDevices and thus iOS often gets more attention by them. Don’t know if I’m in the position to judge about the future of mobile operating systems. But it could very well be a post on it’s own ๐Ÿ™‚

  8. After the long click, the subsequentes clicks (that select and deselect) work well but, they seem have a delay. In your video we can see this delay. It’s normal or just my impression?

  9. I can click multiple choice at once. How do I make it single choice on RecyclerView?

  10. I have a qn.
    My requirement is to get all the files and folders from the content provider.
    For example SD card, internal flash and Cloud providers.

    How do i get this? I have tried the Storage access frame work and also MediaStore but it only gives the item what is asked via URI.
    Please help me out

  11. Great tutorial ๐Ÿ™‚ really helps. Thank you

  12. Thank you for your example.
    Running the app, only 1 item showed at a time in android 23. I didn’t understand that at first.
    Checking the cardview dimensions I saw:
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android&quot;
    android:id="@+id/container_list_item"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    Is 200dp not a bit high?
    Changing it to the following line looked a lot better.
    android:layout_height="wrap_content"
    Cheers, J

    1. Wolfram Rittmeyer

      Thanks. Will correct that!

  13. Thanks for very good tutorial, but I have a problem.
    I have refreshing method in which I swap adapter
    public void refresh() {
    ViewPager vp = (ViewPager) getActivity().findViewById(R.id.pager);
    String path = ((MainPagerAdapter) vp.getAdapter()).getCurrentPath();
    recyclerView.swapAdapter(new DirectoryRecyclerViewAdapter(createFileList(path), vp,this), true);
    }

    and selections no more work after refreshing. Can you help me with that?

  14. Thank you for sharing. Its good information

  15. Fadila Rizki Ramadan

    Attempt to invoke virtual method ‘boolean android.util.SparseBooleanArray.get(int, boolean)’ on a null object reference

    how i can fix this error?

Leave a Reply

Your email address will not be published. Required fields are marked *