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.
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):
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.
Useful links