Android RecyclerView Swipe To Delete And Undo

Filed Under: Android

In this tutorial, we’ll be discussing and implementing the Swipe to Delete feature on the RecyclerView in our Android Application.

Android Swipe To Delete

Swipe to delete feature is commonly used to delete rows from a RecyclerView.

In order to implement Swipe to delete feature, we need to use the ItemTouchHelper utility class.

ItemTouchHelper.Callback

In order to use ItemTouchHelper class, we need to implement the ItemTouchHelper.Callback.

ItemTouchHelper.Callback class is used primarily for drag and drop and swipe to delete behaviors.

In this tutorial, we’ll be sticking with the swipe to delete only.

Android framework provides us with a basic implementation of ItemTouchHelper.Callback which is SimpleCallback.

We will be creating our own implementation of the Swipe to delete feature.

Following are the major methods that need to be overridden in our class:

getMovementFlags – Here we set the direction of swipe. We return the direction flag in a static method makeMovementFlags.

onMove – This is used for drag and drop. If not needed, return false here.
onSwiped – This gets triggered when the swipe action is detected.

A complete swipe goes the full width of the screen. In order to set a consider a partial swipe as a swipe we need to override the following method:

getSwipeThreshold – Here we return the float value. example 0.5f means that a 50 percent swipe on the RecyclerView row would be considered as a swipe.

onChildDraw – Here we’ll create our custom view that shows that the swipe is happening.

ItemTouchHelper.Callback is used to just swipe the rows. It doesn’t delete them itself. We’ll need to delete it ourself using the RecyclerView Adapter.

Enough Talk. Let’s code.

In the next section, we’ll be creating our android application with a RecyclerView containing the Swipe to Dismiss feature. We’ll provide a Snackbar with the undo option.

Project Structure

android recyclerview swipe to delete

Add the following dependency in the app’s build.gradle:


implementation 'com.android.support:design:28.0.0-rc01'

Code

The code for the activity_main.xml layout 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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:id="@+id/coordinatorLayout"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager" />


    </RelativeLayout>

</android.support.design.widget.CoordinatorLayout>

The code for the SwipeToDeleteCallback.java class which extends the ItemTouchHelper.Callback class is given below:


package com.journaldev.androidrecyclerviewswipetodelete;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.View;

abstract public class SwipeToDeleteCallback extends ItemTouchHelper.Callback {

    Context mContext;
    private Paint mClearPaint;
    private ColorDrawable mBackground;
    private int backgroundColor;
    private Drawable deleteDrawable;
    private int intrinsicWidth;
    private int intrinsicHeight;


    SwipeToDeleteCallback(Context context) {
        mContext = context;
        mBackground = new ColorDrawable();
        backgroundColor = Color.parseColor("#b80f0a");
        mClearPaint = new Paint();
        mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        deleteDrawable = ContextCompat.getDrawable(mContext, R.drawable.ic_delete);
        intrinsicWidth = deleteDrawable.getIntrinsicWidth();
        intrinsicHeight = deleteDrawable.getIntrinsicHeight();
        

    }


    @Override
    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
        return makeMovementFlags(0, ItemTouchHelper.LEFT);
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder viewHolder1) {
        return false;
    }

    @Override
    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);

        View itemView = viewHolder.itemView;
        int itemHeight = itemView.getHeight();

        boolean isCancelled = dX == 0 && !isCurrentlyActive;

        if (isCancelled) {
            clearCanvas(c, itemView.getRight() + dX, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom());
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
            return;
        }

        mBackground.setColor(backgroundColor);
        mBackground.setBounds(itemView.getRight() + (int) dX, itemView.getTop(), itemView.getRight(), itemView.getBottom());
        mBackground.draw(c);

        int deleteIconTop = itemView.getTop() + (itemHeight - intrinsicHeight) / 2;
        int deleteIconMargin = (itemHeight - intrinsicHeight) / 2;
        int deleteIconLeft = itemView.getRight() - deleteIconMargin - intrinsicWidth;
        int deleteIconRight = itemView.getRight() - deleteIconMargin;
        int deleteIconBottom = deleteIconTop + intrinsicHeight;


        deleteDrawable.setBounds(deleteIconLeft, deleteIconTop, deleteIconRight, deleteIconBottom);
        deleteDrawable.draw(c);

        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);


    }

    private void clearCanvas(Canvas c, Float left, Float top, Float right, Float bottom) {
        c.drawRect(left, top, right, bottom, mClearPaint);

    }

    @Override
    public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
        return 0.7f;
    }
}

The class is abstract since we haven’t implemented the onSwipe method. We’ll do that in the MainActivity.java class.

Inside the onChildDraw method we check if the swipe is done or not using the isCancelled boolean.
Based on that we create a view with delete icon.

We’ve set the Swipe threshold to 0.7. That means if the row is swiped less than 70%, the onSwipe method won’t be triggered.

The code for the MainActivity.java class is given below:


package com.journaldev.androidrecyclerviewswipetodelete;

import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.View;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {


    RecyclerView recyclerView;
    RecyclerViewAdapter mAdapter;
    ArrayList<String> stringArrayList = new ArrayList<>();
    CoordinatorLayout coordinatorLayout;

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

        recyclerView = findViewById(R.id.recyclerView);
        coordinatorLayout = findViewById(R.id.coordinatorLayout);

        populateRecyclerView();
        enableSwipeToDeleteAndUndo();


    }

    private void populateRecyclerView() {
        stringArrayList.add("Item 1");
        stringArrayList.add("Item 2");
        stringArrayList.add("Item 3");
        stringArrayList.add("Item 4");
        stringArrayList.add("Item 5");
        stringArrayList.add("Item 6");
        stringArrayList.add("Item 7");
        stringArrayList.add("Item 8");
        stringArrayList.add("Item 9");
        stringArrayList.add("Item 10");

        mAdapter = new RecyclerViewAdapter(stringArrayList);
        recyclerView.setAdapter(mAdapter);


    }

    private void enableSwipeToDeleteAndUndo() {
        SwipeToDeleteCallback swipeToDeleteCallback = new SwipeToDeleteCallback(this) {
            @Override
            public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {

                
                final int position = viewHolder.getAdapterPosition();
                final String item = mAdapter.getData().get(position);

                mAdapter.removeItem(position);


                Snackbar snackbar = Snackbar
                        .make(coordinatorLayout, "Item was removed from the list.", Snackbar.LENGTH_LONG);
                snackbar.setAction("UNDO", new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        
                        mAdapter.restoreItem(item, position);
                        recyclerView.scrollToPosition(position);
                    }
                });

                snackbar.setActionTextColor(Color.YELLOW);
                snackbar.show();

            }
        };

        ItemTouchHelper itemTouchhelper = new ItemTouchHelper(swipeToDeleteCallback);
        itemTouchhelper.attachToRecyclerView(recyclerView);
    }


}

In order to set the ItemTouchHelper onto the RecyclerView, the attachToRecyclerView method is used.

When the Snackbar action is clicked we restore the item in the RecyclerView using the restoreItem method.

The restoreItem method is defined in the RecyclerViewAdapter class.

scrollToPosition scrolls the RecyclerView to the position specified. This is mainly used when an item is inserted at the top of the RecyclerView.

The code for the cardview_row.xml is given below:


<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="64dp"
    android:layout_margin="8dp"
    card_view:cardCornerRadius="0dp"
    card_view:cardElevation="2dp">


    <RelativeLayout
        android:id="@+id/relativeLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="8dp"
        android:paddingLeft="8dp"
        android:paddingRight="8dp">


        <TextView
            android:id="@+id/txtTitle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:text="Item 1"
            android:textAppearance="@style/TextAppearance.Compat.Notification.Title" />
    </RelativeLayout>

</android.support.v7.widget.CardView>

The code for the RecyclerViewAdapter.java class is given below:


package com.journaldev.androidrecyclerviewswipetodelete;

import android.support.v7.widget.RecyclerView;
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<RecyclerViewAdapter.MyViewHolder> {

    private ArrayList<String> data;

    public class MyViewHolder extends RecyclerView.ViewHolder {

        private TextView mTitle;
        RelativeLayout relativeLayout;

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

            mTitle = itemView.findViewById(R.id.txtTitle);
        }
    }

    public RecyclerViewAdapter(ArrayList<String> data) {
        this.data = data;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cardview_row, parent, false);
        return new MyViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        holder.mTitle.setText(data.get(position));
    }

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


    public void removeItem(int position) {
        data.remove(position);
        notifyItemRemoved(position);
    }

    public void restoreItem(String item, int position) {
        data.add(position, item);
        notifyItemInserted(position);
    }

    public ArrayList<String> getData() {
        return data;
    }
}


The output of the above application in action is given below:

android recyclerview swipe to delete output

This brings an end to this tutorial. You can download the project from the link below:

Comments

  1. victor says:

    Hi Anupam, your tutorial works great for me!, save me a lot of time, i disabled the part of the undo part because the logic of my app impact into db, but was a great turorial for me. thank you

  2. Girio Wong says:

    Hi Anupam,

    Thanks for your tutorial. It is clear and working. However, when I try to implement this feature to one of the fragment of my Bottom Navigation app, it clash. Do you know the reason or it can only be used in activity? Please help.

  3. yj says:

    Hi,
    after the deletion, the snackbar with the text and the undo button did not appear. how do i fix that problem

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