Today we’ll be developing a RecyclerView Android app with contextual toolbar to let us select, delete or mark the rows of a RecyclerView. Furthermore, we’ll place dividers between RecyclerView rows.
RecyclerView Android with Dividers and Contextual Toolbar Demo
We’ll be developing an application that displays the number of rows selected. Our app will allow us to delete, mark, refresh and select all rows.
A preview of what we’ll going to achieve by the end of this tutorial is given below.
RecyclerView Android Example
ActionMode is used to display the contextual toolbar when a row is long pressed in the list. This enables us to provide a set of alternative toolbar icons.
We’ll be implementing the four action modes present on the top right.
- Reload list
- Mark row text
- Delete row
- Select all rows
To implement the Contextual Toolbar and the above actions, we’ll need to implement the ActionMode.Callback interface in our MainActivity.java
class.
The ActionMode.Callback interface consists of 4 methods that we’ll be overriding.
- onCreateActionMode: The menu.xml file is inflated in this method.
- onPrepareActionMode: This is called every time the Contextual Toolbar is shown.
- onActionItemClicked: This is invoked every time a menu item from the Contextual Toolbar is clicked.
- onDestroyActionMode: This is invoked when the Contextual Toolbar is closed.
RecyclerView android dependencies
Let’s start off by adding the following dependencies in our gradle build file.
compile 'com.android.support:design:25.3.1'
compile 'com.android.support:recyclerview-v7:25.3.1'
Set the activity’s theme to AppTheme.NoActionBar
in the Manifest.xml
file as shown below.
<activity
android:name=".MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
RecyclerView Android Example Project Structure
The code for activity_main.xml
is given below.
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_main" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:visibility="gone"
android:layout_margin="@dimen/fab_margin"
app:backgroundTint="@color/colorPrimary"
app:srcCompat="@android:drawable/ic_input_add" />
</android.support.design.widget.CoordinatorLayout>
The code for content_main.xml
is given below:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto"
xmlns:tools="https://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_main">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</android.support.constraint.ConstraintLayout>
The layout code for each row of the RecyclerView is given in the file recyclerview_list_row.xml
.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_list_row"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:padding="@dimen/fab_margin">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:textSize="16sp"
android:textStyle="bold" />
</RelativeLayout>
The background of the RelativeLayout is a StateListDrawable (bg_list_row.xml) that’ll change its background when the row is selected/deselected.
The code for bg_list_row.xml
is given below:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="https://schemas.android.com/apk/res/android">
<item android:drawable="@color/row_activated" android:state_activated="true" />
<item android:drawable="@android:color/transparent" />
</selector>
The menu that’ll be displayed inside the Contextual Toolbar is defined in the file menu_action_mode.xml
as shown below.
<menu xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:app="https://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete"
android:orderInCategory="300"
android:title="Delete"
app:showAsAction="always" />
<item
android:id="@+id/action_color"
android:icon="@drawable/ic_color_mark"
android:orderInCategory="200"
android:title="Color"
app:showAsAction="always" />
<item
android:id="@+id/action_refresh"
android:icon="@drawable/ic_refresh"
android:orderInCategory="100"
android:title="Refresh"
app:showAsAction="always" />
<item
android:id="@+id/action_select_all"
android:icon="@drawable/ic_select_all"
android:orderInCategory="400"
android:title="ALL"
app:showAsAction="always" />
</menu>
We’ve created a custom ItemDecoration for displaying dividers for each of the rows. The code for DividerItemDecoration.java
is given below.
package com.journaldev.recyclerviewdividersandselectors;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{
android.R.attr.listDivider
};
public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
private Drawable mDivider;
private int mOrientation;
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
throw new IllegalArgumentException("wrong orientation");
}
mOrientation = orientation;
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
public void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
public void drawHorizontal(Canvas c, RecyclerView parent) {
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int left = child.getRight() + params.rightMargin;
final int right = left + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == VERTICAL_LIST) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}
The above code creates a divider line (similar to ListView) after each RecyclerView row based on the orientation.
The code for Model.java
that holds the data for each row is given below.
package com.journaldev.recyclerviewdividersandselectors;
public class Model {
String text;
boolean colored;
public Model(String text, boolean colored) {
this.text = text;
this.colored = colored;
}
}
The code for RecyclerViewAdapter.java is given below:
package com.journaldev.recyclerviewdividersandselectors;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.SparseBooleanArray;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class RecyclerViewAdapter extends RecyclerView.Adapter {
private Context mContext;
private List modelList;
private ClickAdapterListener listener;
private SparseBooleanArray selectedItems;
private static int currentSelectedIndex = -1;
public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener {
public TextView textView;
public RelativeLayout relativeLayout;
public MyViewHolder(View view) {
super(view);
textView = (TextView) view.findViewById(R.id.textView);
relativeLayout = (RelativeLayout) view.findViewById(R.id.relativeLayout);
view.setOnLongClickListener(this);
}
@Override
public boolean onLongClick(View view) {
listener.onRowLongClicked(getAdapterPosition());
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return true;
}
}
public RecyclerViewAdapter(Context mContext, List modelList, ClickAdapterListener listener) {
this.mContext = mContext;
this.modelList = modelList;
this.listener = listener;
selectedItems = new SparseBooleanArray();
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.recyclerview_list_row, parent, false);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position) {
String text = modelList.get(position).text;
holder.textView.setText(text);
if (modelList.get(position).colored)
holder.textView.setTextColor(mContext.getResources().getColor(android.R.color.holo_red_dark));
holder.itemView.setActivated(selectedItems.get(position, false));
applyClickEvents(holder, position);
}
private void applyClickEvents(MyViewHolder holder, final int position) {
holder.relativeLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
listener.onRowClicked(position);
}
});
holder.relativeLayout.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
listener.onRowLongClicked(position);
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return true;
}
});
}
@Override
public int getItemCount() {
return modelList.size();
}
public void toggleSelection(int pos) {
currentSelectedIndex = pos;
if (selectedItems.get(pos, false)) {
selectedItems.delete(pos);
} else {
selectedItems.put(pos, true);
}
notifyItemChanged(pos);
}
public void selectAll() {
for (int i = 0; i < getItemCount(); i++)
selectedItems.put(i, true);
notifyDataSetChanged();
}
public void clearSelections() {
selectedItems.clear();
notifyDataSetChanged();
}
public int getSelectedItemCount() {
return selectedItems.size();
}
public List getSelectedItems() {
List items =
new ArrayList(selectedItems.size());
for (int i = 0; i < selectedItems.size(); i++) {
items.add(selectedItems.keyAt(i));
}
return items;
}
public void removeData(int position) {
modelList.remove(position);
resetCurrentIndex();
}
public void updateData(int position) {
modelList.get(position).colored = true;
resetCurrentIndex();
}
private void resetCurrentIndex() {
currentSelectedIndex = -1;
}
public interface ClickAdapterListener {
void onRowClicked(int position);
void onRowLongClicked(int position);
}
}
The following code snippet is used to change the state of the StateListDrawable.
holder.itemView.setActivated(selectedItems.get(position, false));
The methods selectAll()
, removeData()
and updateData()
would be invoked from the MainActivity.java based on the menu item clicked.
The code for MainActivity.java
is given below.
package com.journaldev.recyclerviewdividersandselectors;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
import static android.view.View.GONE;
public class MainActivity extends AppCompatActivity implements RecyclerViewAdapter.ClickAdapterListener {
RecyclerView recyclerView;
LinearLayoutManager layoutManager;
ArrayList dataModel;
RecyclerViewAdapter mAdapter;
private ActionModeCallback actionModeCallback;
private ActionMode actionMode;
FloatingActionButton fab;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
fab.setVisibility(GONE);
populateDataAndSetAdapter();
}
});
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true);
layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
actionModeCallback = new ActionModeCallback();
populateDataAndSetAdapter();
}
@Override
public void onRowClicked(int position) {
enableActionMode(position);
}
@Override
public void onRowLongClicked(int position) {
enableActionMode(position);
}
private void enableActionMode(int position) {
if (actionMode == null) {
actionMode = startSupportActionMode(actionModeCallback);
}
toggleSelection(position);
}
private void toggleSelection(int position) {
mAdapter.toggleSelection(position);
int count = mAdapter.getSelectedItemCount();
if (count == 0) {
actionMode.finish();
actionMode = null;
} else {
actionMode.setTitle(String.valueOf(count));
actionMode.invalidate();
}
}
private void selectAll() {
mAdapter.selectAll();
int count = mAdapter.getSelectedItemCount();
if (count == 0) {
actionMode.finish();
} else {
actionMode.setTitle(String.valueOf(count));
actionMode.invalidate();
}
actionMode = null;
}
private class ActionModeCallback implements ActionMode.Callback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.menu_action_mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Log.d("API123", "here");
switch (item.getItemId()) {
case R.id.action_delete:
// delete all the selected rows
deleteRows();
mode.finish();
return true;
case R.id.action_color:
updateColoredRows();
mode.finish();
return true;
case R.id.action_select_all:
selectAll();
return true;
case R.id.action_refresh:
populateDataAndSetAdapter();
mode.finish();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mAdapter.clearSelections();
actionMode = null;
}
}
private void deleteRows() {
List selectedItemPositions =
mAdapter.getSelectedItems();
for (int i = selectedItemPositions.size() - 1; i >= 0; i--) {
mAdapter.removeData(selectedItemPositions.get(i));
}
mAdapter.notifyDataSetChanged();
if (mAdapter.getItemCount() == 0)
fab.setVisibility(View.VISIBLE);
actionMode = null;
}
private void updateColoredRows() {
List selectedItemPositions =
mAdapter.getSelectedItems();
for (int i = selectedItemPositions.size() - 1; i >= 0; i--) {
mAdapter.updateData(selectedItemPositions.get(i));
}
mAdapter.notifyDataSetChanged();
actionMode = null;
}
private void populateDataAndSetAdapter() {
dataModel = new ArrayList();
dataModel.add(new Model("Item 1", false));
dataModel.add(new Model("Item 2", false));
dataModel.add(new Model("Item 3", false));
dataModel.add(new Model("Item 4", false));
dataModel.add(new Model("Item 5", false));
dataModel.add(new Model("Item 6", false));
dataModel.add(new Model("Item 7", false));
dataModel.add(new Model("Item 8", false));
dataModel.add(new Model("Item 9", false));
dataModel.add(new Model("Item 10", false));
dataModel.add(new Model("Item 11", false));
dataModel.add(new Model("Item 12", false));
mAdapter = new RecyclerViewAdapter(this, dataModel, this);
recyclerView.setAdapter(mAdapter);
}
}
The following code is used to add dividers between the rows.
recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
onRowClicked()
and onRowLongClicked()
are called every time a RecyclerView row is clicked.
enableActionMode()
is used to show the Contextual Toolbar.
The Contextual Toolbar displays the number of rows selected based upon the getSelectedItemCount()
from the adapter class.
If all the rows are deleted, we show a floating action button that lets the user populate the RecyclerView with dummy data once again.
The output of the above application in action is given below.
Contextual Toolbar is commonly seen in applications like Whatsapp and Inbox.
This brings an end to the RecyclerView Android example with divider and selectors. You can download the final Android RecyclerViewDividersAndSelectors Project from the link below.
Reference: Android Doc