RecyclerView Android with Dividers and Contextual Toolbar

Filed Under: Android

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 contextual toolbar

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.

  1. Reload list
  2. Mark row text
  3. Delete row
  4. 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.

  1. onCreateActionMode: The menu.xml file is inflated in this method.
  2. onPrepareActionMode: This is called every time the Contextual Toolbar is shown.
  3. onActionItemClicked: This is invoked every time a menu item from the Contextual Toolbar is clicked.
  4. 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

recyclerview android example contextual toolbar

The code for activity_main.xml is given below.


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://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="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://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="http://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="http://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="http://schemas.android.com/apk/res/android"
    xmlns:app="http://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.
android recyclerview contextual toolbar app

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

Comments

  1. Hi Ten says:

    Hi Anupam,

    Thank you for easy explanation, it help me a lot to quick implementation of Contextual Toolbar with Recycleview.

    I am facing issue two issue in current implementation as follow.

    #1 – Selected recycle view items background not apply
    #2 – Contextual Toolbar appear on single tap which is wrong.

    Will you please guide me what could be wrong in my implementation.

    Regards,
    Hi Ten

  2. Mark A Peters says:

    One thing thing to keep in mind about this approach is that the action mode is associated with the activity, not the list. That means that this won’t work entirely correctly if the activity uses a view pager. What happens is that if the action mode is active when the page is changed, it will still be there on the new page, which likely isn’t appropriate.

  3. Kes says:

    zero explanation, I don’t want to just copy and paste your code, I want to UNDERSTAND it.

  4. Vamsi says:

    Sir i have doubt on card view with recycle view

    I have one recycle view in that i have 4 card views after that i have one final button on bottom of screen when i click apply button what the data available in cards are there i want to store in room database

  5. ThunderRoid says:

    private void selectAll() {
    mAdapter.selectAll();
    int count = mAdapter.getSelectedItemCount();

    if (count == 0) {
    actionMode.finish();
    } else {
    actionMode.setTitle(String.valueOf(count));
    actionMode.invalidate();
    }

    // actionMode = null; -> DELETE this line
    }

  6. pierre b samson says:

    Very well done! do you have any extra example using an assync call instead of the populateDataAndSetAdapter.

    I have great difficulty passing the context and ilistener to the adapter in an assync post call.

    Pierre

    1. Anupam says:

      Glad it helped you.

  7. Sanjeev K says:

    Hey, thanks for the article. However I am trying to implement only OnLongClick. I already have a OnClick events so if I am using this unfortunately the item gets selected without performing the OnClick action events. How should I modify the code?

  8. Someone Whofoundthisuseful says:

    Thanks very much for this great post. It has really helped me in my app. Hope you wouldn’t mind me using code published here in my project and that you’d let me know otherwise.

    1. Anupam says:

      Ofcourse, everything here is free!

Leave a Reply

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

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages