Search

RecyclerView Part 2: Choice Modes

Bill Phillips

12 min read

Dec 18, 2014

RecyclerView Part 2: Choice Modes

Don’t miss Part 1, where Bill addresses the fundamentals of RecyclerView for ListView experts.

A famous man once said,

In this life, things are much harder than in the afterworld. In this life, you’re on your own.

Is this a true statement? Perhaps that matter is up for debate. When it comes to selecting items in a RecyclerView, though, you are, in fact, on your own: RecyclerView does not give you any tools for doing this. So how do you do it?

I figured this would be a straightforward exercise in rolling my own solution, so I dove right in. Here’s what I found out.

(If you like, you can see how I worked through all this in my GitHub repo. And if you just want to know how to do it the easy way, skip to the section named “TL;DR” at the end.)

Review: Choice Modes And Contextual Action Modes

I set out to implement multiselect like we do in the CriminalIntent app in our Android book: with a contextual action mode. Here’s what that looks like in the code (for brevity, I’m showing only the interesting bits—you can find the whole thing in our solutions):

    listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
    listView.setMultiChoiceModeListener(new MultiChoiceModeListener() {
    
        public boolean onCreateActionMode(ActionMode mode, Menu menu) { ...  }
    
        public void onItemCheckedStateChanged(ActionMode mode, int position,
                long id, boolean checked) { ...  }
    
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            switch (item.getItemId()) {
                case R.id.menu_item_delete_crime:
                    CrimeAdapter adapter = (CrimeAdapter)getListAdapter();
                    CrimeLab crimeLab = CrimeLab.get(getActivity());
                    for (int i = adapter.getCount() - 1; i >= 0; i--) {
                        if (getListView().isItemChecked(i)) {
                            crimeLab.deleteCrime(adapter.getItem(i));
                        }
                    }
                    mode.finish(); 
                    adapter.notifyDataSetChanged();
                    return true;
                default:
                    return false;
            }
  
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) { ...  }
        
        public void onDestroyActionMode(ActionMode mode) { ...  }
    });

ListView has this idea of choice modes. If the ListView is in a particular choice mode, it will handle all the details of displaying a checkable interface, keeping track of check marks and toggling all that stuff back and forth when individual items are tapped. You turn choice modes on by calling ListView.setChoiceMode(), as you see above. To see whether an item is checked, you call ListView.isItemChecked(int) (like you can see above in onActionItemClicked()).

When you use CHOICE_MODE_MULTIPLE_MODAL, long pressing any item in the list will automagically turn multichoice mode on. At the same time, it will activate an Action mode representing the multiselect interaction. The MultiChoiceModeListener above is a listener for that contextual action mode—it’s like a set of option mode callbacks that are only called for this action mode.

In my earlier post on RecyclerView fundamentals, we saw that RecyclerView leaves you on your own for implementing all of this. You have three moving parts that need to be implemented:

  • showing which views are checked and unchecked
  • keeping track of checked and unchecked states for items in the list
  • turning the whole thing off and on in a contextual action mode

In a perfect world, this will be something that you would actually want to do in practice, too. As I was writing this, though, I found my solutions falling short. I could imagine someone reading this post and just shaking their head: “Seriously? I need to roll this myself every time?”

So in this post, I’ll explain enough that you can roll your own easily if you need to, but also provide a library called MultiSelector that’s more of a drop-in solution.

Keeping Tracked Of State

This is the most straightforward, so let’s solve it first. In ListView, this works like so:

    // Check item 0
    mListView.setItemChecked(0, true);

    // Returns true
    mListView.isItemChecked(0);

    // Says what the choice mode currently is
    mListView.getChoiceMode();

Rolling our own looks like this:

    private SparseBooleanArray mSelectedPositions = new SparseBooleanArray();
    private mIsSelectable = false;

    private void setItemChecked(int position, boolean isChecked) {
        mSelectedPositions.put(position, isChecked);
    }

    private boolean isItemChecked(int position) {
        return mSelectedPositions.get(position);
    }

    private void setSelectable(boolean selectable) {
        mIsSelectable = selectable;
    }

    private boolean isSelectable() {
        return mIsSelectable;
    }

This will not update the user interface like ListView.setItemChecked(), but it will do for now.

Of course, you can keep track of selections however you like. A Set of model objects can be a good choice, too.

I put this idea in an object called MultiSelector:

    MultiSelector selector = new MultiSelector();
    selector.setSelected(0, true);
    selector.isSelected(0);
    selector.setSelectable(true);
    selector.isSelectable();

Showing Item Selection State

In ListView from Honeycomb onwards, item selection has been visualized like this: whenever an item is selected, the view is set to the “activated” state by calling setActivated(true). When the view is no longer selected, it is set back to false. With that done, it is straightforward to tweak the selection mode by using XML StateListDrawables to highlight the selection state.

You can do the same thing by hand in your ViewHolder’s bindCrime:

    private class CrimeHolder extends ViewHolder {
        ...

        public void bindCrime(Crime crime) {
            mCrime = crime;
            mSolvedCheckBox.setChecked(crime.isSolved());

            boolean isSelected = mMultiSelector.isSelected(getPosition());
            itemView.setActivated(isSelected);
        }
    }

Of course, if you want to display selection in another way, you can. The sky’s the limit. Drawables and state list animators make the activated state a good default choice, though.

If that were all there was, I wouldn’t have spent so much time on this. But I did, because I got stubborn on some visual details I wanted.

Material animations

Material Design includes this cool new ripple animation. If you read more about it at Implementing Material Design in Your Android App, you see that you can get this behavior for free when you use ?android:selectableItemBackground as your background.

If you are going to use the activated state, though, this is not an option. ?android:selectableItemBackground does not support visualization for the activated state. You can try to roll your own with activated support using a state selector drawable, but that ends up looking like this:

Ripples in a state selector

The selected state responds each time you tap on it. So when you tap the view to turn activated state off, you also get the ripple effect.

This did not make sense to me visually. In my mind, the list has two modes: normal mode, and selection mode. In normal mode, a tap should have the same ripple effect that ?android:selectableItemBackground gives me. In selection mode, though, a tap should simply toggle activated on and off, without any ripple effect. In Lollipop, it would be nice to also have a Material Design affordance: a state list animator to elevate the selected items up in translation.

To get this effect with the out of the box Android APIs, you have to do more than use state list drawables and animators judiciously. You need to actually have two different modes for the view: one in which it uses a default set of drawables & animators, and one in which it uses a different set exclusively for selection. Like this:

Two mode selection view

SwappingHolder

This is where the second tool I wrote comes into play: a ViewHolder subclass called SwappingHolder, which does exactly what I just described. SwappingHolder subclasses the regular ViewHolder and adds six additional properties:

    public Drawable getSelectionModeBackgroundDrawable();
    public Drawable getDefaultModeBackgroundDrawable();

    public StateListAnimator getSelectionModeStateListAnimator();
    public StateListAnimator getDefaultModeStateListAnimator();

    public boolean isSelectable();
    public boolean isActivated();

When you first create it, SwappingHolder will leave its itemView’s background drawable and state list animator alone, stashing those initial values in defaultModeBackgroundDrawable and defaultModeStateListAnimator. If you set selectable to “true,” though, it will switch to the selectionMode version of both those properties. Set selectable back to “false,”” it switches back to the default value. And the activated property? It calls through to itemView’s activated property.

Out of the box, SwappingHolder uses a selectionModeStateListAnimator that elevates the selected item up a little bit when activated, and a selectionModeBackgroundDrawable that uses the colorAccent attribute out of the appcompat Material theme.

So that fixes that. The last bit is to hook everything up to the selection logic in a way that’s easy to turn on and off.

Connecting The Selection Logic

Again, you can do it by hand if you like. There are two steps: updating the ViewHolder when it is bound to a crime, and hooking up click events. To update when bound to a crime, add some more code to bindCrime():

    private class CrimeHolder extends SwappingHolder {
        ...

        public void bindCrime(Crime crime) {
            mCrime = crime;
            mSolvedCheckBox.setChecked(crime.isSolved());

            setSelectable(mMultiSelector.isSelectable());
            setActivated(mMultiSelector.isSelected(getPosition()));
        }
    }

So every time you hook up your ViewHolder to another crime, you need to double check and see whether you’re currently in the selection mode, and whether the item you’re hooking up to is selected.

And then hook up a click listener:

    private class CrimeHolder extends SwappingHolder 
            implements View.OnClickListener {
        ...

        public CrimeHolder(View itemView) {
            super(itemView);

            mSolvedCheckBox = (CheckBox) itemView
                .findViewById(R.id.crime_list_item_solvedCheckBox);
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            if (mMultiSelector.isSelectable()) {
                // Selection is active; toggle activation
                setActivated(!isActivated());
                mMultiSelector.setSelected(getPosition(), isActivated());
            } else {
                // Selection not active
            }
        }
    }

For single select, the onClick() implementation will need to be more complicated than that, because it will need to find the other currently active selection and deselect it.

This isn’t a whole lot of code, but it is boilerplate that you would need to implement every time you write this. I’ve done some more work in MultiSelector that gets rid of the boilerplate.

Turning Everything On And Off

Okay, the last step: turning it on and off. You definitely need to do this for CHOICE_MODE_MULTIPLE_MODAL, and you often need to when using the other choice modes, too.

The simplest solution is to augment your setSelectable() implementation with a notifyDataSetChanged():

    public void setSelectable(boolean isSelectable) {
        mIsSelectable = isSelectable;
        mRecyclerView.getAdapter().notifyDataSetChanged();
    }

In ListView (and in ViewPager), notifyDataSetChanged() was almost always the right solution when you were showing the wrong thing. In RecyclerView, I recommend that you be much more judicious about using it.

Here’s why: the biggest reason to use RecyclerView is that it makes it easy to animate changes to your list content. For example, if you want to delete the first crime from your list, you can animate that like this:

    // Delete the 0th crime from your model
    mCrimes.remove(0);
    // Notify the adapter that it was removed
    mRecyclerView.getAdapter().notifyItemRemoved(0);

Calling notifyDataSetChanged() can break that, because it interrupts those animations.

The RecyclerView’s ItemAnimator will animate the change for you. The default animator will fade out item 0, then shift the other items up one.

What happens if you do notifyDataSetChanged() soon after that? It will kill any pending animations, requery the adaptor and redisplay everything. A heavy hammer, that. Often it’s the right choice anyway, but be aware: if you can update your list content in some other way besides notifyDataSetChanged, do it!

So what other way could we do this? Well… like this:

    public void setSelectable(boolean isSelectable) {
        mIsSelectable = isSelectable;
        for (int i = 0; i < mRecyclerView.getAdapter().getItemCount(); i++) {
            RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForPosition(i);

            if (holder != null) {
                ((SwappingHolder)holder).setSelectable(isSelectable);
            }
        }

    }

We can iterate over all the ViewHolders, cast them to SwappingHolder and manually tell them what the current selectable state is. Yech.

Like with SwappingHolder, MultiSelector takes care of this for you. MultiSelector knows which ViewHolders are hooked up, so this line is all you need to update your user interface:

    mMultiSelector.setSelectable(true);

Using A Contextual Action Mode

Once setSelectable() is implemented, you can achieve the rest of CHOICE_MODE_MULTIPLE_MODAL by using a regular ActionMode.Callback. Call through to your setSelectable() from within the relevant callback methods:

    private ActionMode.Callback mDeleteMode = new ActionMode.Callback() {
        @Override
        public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
            setSelectable(true);
            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode actionMode) {
            setSelectable(false);
        }

        @Override
        public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { ... }

        @Override
        public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { ... }
    }

Then use a long click listener to turn on the action mode:

    private class CrimeHolder extends SwappingHolder
            implements View.OnClickListener, View.OnLongClickListener {

        ...

        public CrimeHolder(View itemView) {
            ...

            itemView.setOnClickListener(this);
            itemView.setOnLongClickListener(this);
            itemView.setLongClickable(true);
        }

        @Override
        public boolean onLongClick(View v) {
            ActionBarActivity activity = (ActionBarActivity)getActivity();
            activity.startSupportActionMode(deleteMode);
            setSelected(this, true);
            return true;
        }
    }

TL;DR: Implementing Choice Modes with a Library

Ok, so that’s everything going on in MultiSelect. What if you don’t care, and would prefer to have an out-of-the-box solution?

One existing solution has been brought to my attention: TwoWayView, a library by Lucas Rocha. I haven’t had time to research the details of how it does what it does, but I can tell you that it sets out to replicate the setChoiceMode() API used by ListView, as well as a lot of other stuff that ListView had. For folks looking to drop-in replace their old ListView with a RecyclerView-based implementation, TwoWayView looks like a great solution. If you’d like to use that, I defer to their documentation.

Of course, by the time my colleagues told me about this, I had already written my own multiselect implementation that looked much different. Maybe you will find it useful, too. I’ve tried to make something small, focused, flexible and easy to use. There’s not a lot of code, and only a limited amount of judiciously chosen “magic.” Here’s how it works.

MultiSelector: Basics

First, import the library. Add the following line to your build.gradle:

compile 'com.bignerdranch.android:recyclerview-multiselect:+'

(You can find the project on GitHub, along with the Javadocs.)

Next, create a MultiSelector instance. In my example app, I did it inside my Fragment:

    public class CrimeListFragment extends Fragment {

        private MultiSelector mMultiSelector = new MultiSelector();

        ...
    }

The MultiSelector knows which items are selected, and is also your interface for controlling item selection across everything it is hooked up to. In this case, that’s everything in the adapter.

To hook up a SwappingHolder to a MultiSelector, pass in the MultiSelector in the constructor, and use click listeners to call through to MultiSelector.tapSelection():

    private class CrimeHolder extends SwappingHolder
            implements View.OnClickListener, View.OnLongClickListener {
        private final CheckBox mSolvedCheckBox;
        private Crime mCrime;

        public CrimeHolder(View itemView) {
            super(itemView, mMultiSelector);

            mSolvedCheckBox = (CheckBox) itemView.findViewById(R.id.crime_list_item_solvedCheckBox);
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            if (mCrime == null) {
                return;
            }
            if (!mMultiSelector.tapSelection(this)) {
                // start an instance of CrimePagerActivity
                Intent i = new Intent(getActivity(), CrimePagerActivity.class);
                i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId());
                startActivity(i);
            }
        }
    }

MultiSelector.tapSelection() simulates tapping a selected item; if the MultiSelector is in selection mode, it returns true and toggles the selection for that item. If not, it returns false and does nothing.

To turn on multiselect mode, call setSelectable(true):

    mMultiSelector.setSelectable(true);

This will toggle the flag on the MultiSelector, and toggle it on all its bound SwappingHolders, too. This is all done for you by SwappingHolder—it extends MultiSelectorBindingHolder, which binds itself to your MultiSelector.

And for basic multiselect, that’s all there is to it. When you need to know whether an item is selected, ask the multiselector:

    for (int i = mCrimes.size(); i > 0; i--) {
        if (mMultiSelector.isSelected(i, 0)) {
            Crime crime = mCrimes.get(i);
            CrimeLab.get(getActivity()).deleteCrime(crime);
            mRecyclerView.getAdapter().notifyItemRemoved(i);
        }
    }

Single selection

To use single selection instead of multiselect, use SingleSelector instead of MultiSelector:

    public class CrimeListFragment extends Fragment {

        private MultiSelector mMultiSelector = new SingleSelector();

        ...
    }

To get the same effect as CHOICE_MODE_MULTIPLE_MODAL, you can either write your own ActionMode.Callback as described above, or use the provided abstract implementation, ModalMultiSelectorCallback:

    private ActionMode.Callback mDeleteMode = new ModalMultiSelectorCallback(mMultiSelector) {

        @Override
        public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
            getActivity().getMenuInflater().inflate(R.menu.crime_list_item_context, menu);
            return true;
        }

        @Override
        public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
            switch (menuItem.getItemId()) {
                case R.id.menu_item_delete_crime:
                    // Delete crimes from model

                    mMultiSelector.clearSelections();
                    return true;
                default:
                    break;
            }
            return false;
        }
    };

ModalMultiSelectorCallback will call MultiSelector.setSelectable(true) and clearSelections() inside onPrepareActionMode, and setSelectable(false) in onDestroyActionMode. Kick it off like any other action mode inside a long click listener:

    private class CrimeHolder extends SwappingHolder
            implements View.OnClickListener, View.OnLongClickListener {
        public CrimeHolder(View itemView) {
            ...

            itemView.setOnLongClickListener(this);
            itemView.setLongClickable(true);
        }

        @Override
        public boolean onLongClick(View v) {
            ActionBarActivity activity = (ActionBarActivity)getActivity();
            activity.startSupportActionMode(mDeleteMode);
            mMultiSelector.setSelected(this, true);
            return true;
        }
    }

Customizing selection visuals

SwappingDrawable uses two sets of drawables and state list animators for its itemView: one while in the default mode, and one while in selection mode. You can customize these by calling one of the various setters:

    public void setSelectionModeBackgroundDrawable(Drawable drawable);
    public void setDefaultModeBackgroundDrawable(Drawable drawable);

    public void setSelectionModeStateListAnimator(int resId);
    public void setDefaultModeStateListAnimator(int resId);

The state list animator setters are safe to call prior to API 21, and will result in a no-op.

Off label customization

If you need to customize what the selected states look like beyond what SwappingHolder offers, you can extend the MultiSelectorBindingHolder abstract class:

    public class MyCustomHolder extends MultiSelectorBindingHolder {
        @Override
        public void setSelectable(boolean selectable) { ... }

        @Override
        public boolean isSelectable() { ... }

        @Override
        public void setActivated(boolean activated) { ... }

        @Override
        public boolean isActivated() { ... }
    }

And if that’s still too restrictive, you can implement the SelectableHolder interface instead, which provides the same methods. It requires a bit more code: you will need to bind your ViewHolder to the MultiSelector by calling mMultiSelector.bindHolder() every time onBindViewHolder is called.

Had Enough?

In this post, we took a look at selecting items in a RecyclerView. From working along, you now know how to show which views are checked and unchecked, track checked and unchecked states for items in the list, and turn the whole thing off and on in a contextual action mode.

Mark Dalrymple

Reviewer Big Nerd Ranch

MarkD is a long-time Unix and Mac developer, having worked at AOL, Google, and several start-ups over the years.  He’s the author of Advanced Mac OS X Programming: The Big Nerd Ranch Guide, over 100 blog posts for Big Nerd Ranch, and an occasional speaker at conferences. Believing in the power of community, he’s a co-founder of CocoaHeads, an international Mac and iPhone meetup, and runs the Pittsburgh PA chapter. In his spare time, he plays orchestral and swing band music.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News