time

Multi selection in RecyclerView

In this post, I am going to implement a RecyclerView with multi selection feature. In multi selection, user can select multiple items from RecyclerView.

Multi selection without ActionMode

Suppose there is a model class called Item which holds name.

public class Item {
    String title;

    public Item(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

We need to modify our Adapter to keep a list of selected elements and a list of all elements.

Now lets see the Adapter.

public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.RecyclerViewHolder>{
    Context context;
    LayoutInflater inflater;
    List<Item> items, selected;

    public class RecyclerViewHolder extends RecyclerView.ViewHolder {
        TextView tv;

        public RecyclerViewHolder(View itemView) {
            super(itemView);
            tv = (TextView) itemView.findViewById(R.id.tv);
        }
    }

    public ItemAdapter(Context context) {
        this.context = context;
        this.inflater = LayoutInflater.from(context);
        this.selected = new ArrayList<>();
        this.items = new ArrayList<>();
    }

    @Override
    public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = inflater.inflate(R.layout.recycle_item, parent, false);

        RecyclerViewHolder viewHolder = new RecyclerViewHolder(v);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(final RecyclerViewHolder holder, int position) {
        final Item item = items.get(position);
        holder.tv.setText(item.getTitle());
        holder.tv.setTag(holder);

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (selected.contains(item)) {
                    selected.remove(item);
                    unhighlightView(holder);
                } else {
                    selected.add(item);
                    highlightView(holder);
                }
            }
        });

        if (selected.contains(item))
            highlightView(holder);
        else
            unhighlightView(holder);
    }

    private void highlightView(RecyclerViewHolder holder) {
        holder.itemView.setBackgroundColor(ContextCompat.getColor(context, R.color.selected));
    }

    private void unhighlightView(RecyclerViewHolder holder) {
        holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent));
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    public void addAll(List<Item> items) {
        clearAll(false);
        this.items = items;
        notifyDataSetChanged();
    }

    public void clearAll(boolean isNotify) {
        items.clear();
        selected.clear();
        if (isNotify) notifyDataSetChanged();
    }

    public void clearSelected() {
        selected.clear();
        notifyDataSetChanged();
    }

    public void selectAll() {
        selected.clear();
        selected.addAll(items);
        notifyDataSetChanged();
    }

    public List<Item> getSelected() {
        return selected;
    }
}

Next is the MainActivity:

public class MainActivity extends AppCompatActivity {
    Activity activity = MainActivity.this;
    RecyclerView rv;
    ItemAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        adapter = new ItemAdapter(this);

        rv = findViewById(R.id.rcView);
        rv.setAdapter(adapter);
        rv.setHasFixedSize(true);
        rv.setLayoutManager(lm);

        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(rv.getContext(), lm.getOrientation());
        rv.addItemDecoration(dividerItemDecoration);

        List<Item> items = new ArrayList<>();
        items.add(new Item("Item 1"));
        items.add(new Item("Item 2"));
        items.add(new Item("Item 3"));
        items.add(new Item("Item 4"));
        items.add(new Item("Item 5"));
        items.add(new Item("Item 6"));
        items.add(new Item("Item 7"));
        items.add(new Item("Item 8"));
        items.add(new Item("Item 9"));

        adapter.addAll(items);
    }

    public void selectAll(View v) {
        adapter.selectAll();
    }

    public void deselectAll(View v) {
        adapter.clearSelected();
    }

    public void doAction(View v) {
        Toast.makeText(activity, String.format("Selected %d items", adapter.getSelected().size()), Toast.LENGTH_SHORT).show();
    }
}

Following is layout for MainActivity

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rcView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipToPadding="false"
        android:paddingBottom="16dp"
        android:paddingTop="16dp"
        android:scrollbars="vertical" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:weightSum="3"
        android:layout_below="@id/rcView"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Select all"
            android:onClick="selectAll"
            android:layout_weight="1" />

        <Button
            android:id="@+id/btn2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Deselect"
            android:onClick="deselectAll"
            android:layout_weight="1" />

        <Button
            android:id="@+id/btn3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Action"
            android:onClick="doAction"
            android:layout_weight="1" />
    </LinearLayout>

</RelativeLayout>

Following is layout for RecyclerView item

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="5dp">

    <TextView
        android:id="@+id/tv"
        android:layout_centerInParent="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Title"
        android:textColor="#000000"
        android:textSize="16sp" />

</RelativeLayout>

When we run above program in Android Studio we will get the result like as shown below.

android_recycleview_multiselect.png

Multi selection with ActionMode

The contextual action mode is a system implementation of ActionMode that focuses user interaction toward performing contextual actions. When a user enables this mode by selecting an item, a contextual action bar appears at the top of the screen to present actions the user can perform on the currently selected item(s). While this mode is enabled, the user can select multiple items (if you allow it), deselect items, and continue to navigate within the activity (as much as you're willing to allow). The action mode is disabled and the contextual action bar disappears when the user deselects all items, presses the BACK button, or selects the Done action on the left side of the bar.

Since API 11 (Android 3.0), ListView and GridView provide a special mode called CHOICE_MODE_MULTIPLE_MODAL that handles this automatically. When you enable this option, the user is able to long-press a list item to switch the ListView/GridView to a selection mode, then select more items with simple taps. While in this mode, the ListView also launches a contextual action mode showing the possible contextual actions on top of the App Bar. Closing the action mode or deselecting the last item switches the ListView back to normal mode again.

In contrast, RecyclerView provides no built-in selection mode at all, since its purpose is to be as simple and modular as possible.

For views that provide contextual actions, you should usually invoke the contextual action mode upon one of two events (or both):

  • The user performs a long-click on the view.
  • The user selects a checkbox or similar UI component within the view.

If you want to invoke the contextual action mode only when the user selects specific views, you should implement the ActionMode.Callback interface. In its callback methods, you can specify the actions for the contextual action bar, respond to click events on action items, and handle other lifecycle events for the action mode.

Now lets see the Adapter

public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.RecyclerViewHolder>{
    Context context;
    LayoutInflater inflater;
    List<Item> items, selected;
    OnClickAction receiver;

    public interface OnClickAction {
        public void onClickAction();
    }

    public class RecyclerViewHolder extends RecyclerView.ViewHolder {
        TextView tv;

        public RecyclerViewHolder(View itemView) {
            super(itemView);
            tv = (TextView) itemView.findViewById(R.id.tv);
        }
    }

    public ItemAdapter(Context context) {
        this.context = context;
        this.inflater = LayoutInflater.from(context);
        this.selected = new ArrayList<>();
        this.items = new ArrayList<>();
    }

    @Override
    public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = inflater.inflate(R.layout.recycle_item, parent, false);

        RecyclerViewHolder viewHolder = new RecyclerViewHolder(v);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(final RecyclerViewHolder holder, int position) {
        final Item item = items.get(position);
        holder.tv.setText(item.getTitle());
        holder.tv.setTag(holder);

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (selected.contains(item)) {
                    selected.remove(item);
                    unhighlightView(holder);
                } else {
                    selected.add(item);
                    highlightView(holder);
                }

                receiver.onClickAction();
            }
        });

        if (selected.contains(item))
            highlightView(holder);
        else
            unhighlightView(holder);
    }

    private void highlightView(RecyclerViewHolder holder) {
        holder.itemView.setBackgroundColor(ContextCompat.getColor(context, R.color.selected));
    }

    private void unhighlightView(RecyclerViewHolder holder) {
        holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent));
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    public void addAll(List<Item> items) {
        clearAll(false);
        this.items = items;
        notifyDataSetChanged();
    }

    public void clearAll(boolean isNotify) {
        items.clear();
        selected.clear();
        if (isNotify) notifyDataSetChanged();
    }

    public void clearSelected() {
        selected.clear();
        notifyDataSetChanged();
    }

    public void selectAll() {
        selected.clear();
        selected.addAll(items);
        notifyDataSetChanged();
    }

    public List<Item> getSelected() {
        return selected;
    }

    public void setActionModeReceiver(OnClickAction receiver) {
        this.receiver = receiver;
    }
}

Next is the MainActivity:

public class MainActivity extends AppCompatActivity implements ItemAdapter.OnClickAction {
    Activity activity = MainActivity.this;
    RecyclerView rv;
    ItemAdapter adapter;
    ActionMode actionMode;

    private ActionMode.Callback actionModeCallback = new ActionMode.Callback() {
        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.cab_menu, menu);
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            switch (item.getItemId()) {
                case R.id.menu_email:
                    Toast.makeText(activity, adapter.getSelected().size() + " selected", Toast.LENGTH_SHORT).show();
                    mode.finish();
                    return true;
                default:
                    return false;
            }
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            actionMode = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        adapter = new ItemAdapter(this);

        rv = findViewById(R.id.rcView);
        rv.setAdapter(adapter);
        rv.setHasFixedSize(true);
        rv.setLayoutManager(lm);

        DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(rv.getContext(), lm.getOrientation());
        rv.addItemDecoration(dividerItemDecoration);

        List<Item> items = new ArrayList<>();
        items.add(new Item("Item 1"));
        items.add(new Item("Item 2"));
        items.add(new Item("Item 3"));
        items.add(new Item("Item 4"));
        items.add(new Item("Item 5"));
        items.add(new Item("Item 6"));
        items.add(new Item("Item 7"));
        items.add(new Item("Item 8"));
        items.add(new Item("Item 9"));

        adapter.addAll(items);
        adapter.setActionModeReceiver((ItemAdapter.OnClickAction) activity);
    }

    public void selectAll(View v) {
        adapter.selectAll();
        if (actionMode == null) {
            actionMode = startActionMode(actionModeCallback);
            actionMode.setTitle("Selected: " + adapter.getSelected().size());
        }
    }

    public void deselectAll(View v) {
        adapter.clearSelected();
        if (actionMode != null) {
            actionMode.finish();
            actionMode = null;
        }
    }

    public void onClickAction() {
        int selected = adapter.getSelected().size();
        if (actionMode == null) {
            actionMode = startActionMode(actionModeCallback);
            actionMode.setTitle("Selected: " + selected);
        } else {
            if (selected == 0) {
                actionMode.finish();
            } else {
                actionMode.setTitle("Selected: " + selected);
            }
        }
    }
}

Following is layout for MainActivity

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rcView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipToPadding="false"
        android:paddingBottom="16dp"
        android:paddingTop="16dp"
        android:scrollbars="vertical" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:weightSum="2"
        android:layout_below="@id/rcView"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Select all"
            android:onClick="selectAll"
            android:layout_weight="1" />

        <Button
            android:id="@+id/btn2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Deselect"
            android:onClick="deselectAll"
            android:layout_weight="1" />
    </LinearLayout>

</RelativeLayout>

Following is layout for cab_menu.xml.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/menu_email"
        android:title="Email"
        android:icon="@android:drawable/ic_dialog_email"
        app:showAsAction="ifRoom"
        />
</menu>

When we run above program in Android Studio we will get the result like as shown below.

android_recycleview_multiselect_am.png

Useful links