Android RecyclerView Swipe to Delete Undo

RecyclerView is a ViewGroup ,that display a scrolling list of elements based on large data sets (or data that frequently changes) . RecyclerView widget is more flexible and efficient version of ListView .

In the previous tutorial we have seen example of deleting items of recyclerView on swiping ,In this tutorial we will how to do for swiped items of recyclerView  .



Project Detail
Project Name RecyclerViewSwiptToDeleteUndo
Package com.tutorialsbuzz.recyclerviewswipetodeleteundo
Min Sdk Version 22
Target Sdk Version 29
Compile Sdk Version 29
Theme Theme.AppCompat.Light.DarkActionBar


ItemTouchHelper


ItemTouchHelper is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. It works with a RecyclerView and a Callback class, which configures what type of interactions are enabled and also receives events when user performs these actions.

Add RecyclerView to your layout


Create XML Layout activity_main.xml , this will be set as content view for launcher Activity (MainActivity.kt) and add RecyclerView to your layout file .



file : activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recyclerView"/>

Data Class


file : Model.kt
package com.tutorialsbuzz.recyclerviewswipetodeleteundo

data class Model(val name: String, val version:String) {}

To Load Data Into RecyclerView , We will read JSON File Kept Inside Asset folder and map it to above defined data class .

file : main\assets\android_version.json
[
  {
    "name": "Cupcake",
    "version": "Android 1.5"
  },
  {
    "name": "Donut",
    "version": "Android 1.6"
  },
  {
    "name": "Eclairs",
    "version": "Android 2.0-2.1"
  },
  {
    "name": "Froyo",
    "version": "Android 2.2-2.3"
  },
  {
    "name": "Gingerbread",
    "version": "Android 2.3-2.3.7"
  },
  {
    "name": "Honeycomb",
    "version": "Android 3.0-3.2.6"
  },
  {
    "name": "Icecream",
    "version": "Android 4.0-4.0.4"
  },
  {
    "name": "Jellybean",
    "version": "Android 4.1-4.3.1"
  },
  {
    "name": "Kitkat",
    "version": "Android 4.4-4.4.4"
  },
  {
    "name": "Lolipop",
    "version": "Android 5.0-5.1.1"
  },
  {
    "name": "Marshmallow",
    "version": "Android 6.0-6.0.1"
  },
  {
    "name": "Nougat",
    "version": "Android 7.0-7.1.2"
  },
  {
    "name": "Oreo",
    "version": "Android 8.0-8.1"
  },
  {
    "name": "Pie",
    "version": "Android 9.0"
  }
]


Image Resource 

Name field from json matches to respective png file name kept inside drawable .

Folder : app\src\main\res\drawable

Adapter and ViewHolder For RecyclerView


1. XML Layout For RecyclerView Item 

Create XML Layout file in res/layout and name it row_item.xml , This Layout defines the layout for Items of RecyclerView . Here In this example we have two TextView inside LinearLayout .

Each Row Item Of RecyclerView Includes Regular-layout and swipe layout wrapped inside FrameLayout Please find below diagram and xml code .
  • Regular Layout is shown when items of recyclerView is loaded and when user clicks undo .
  • Swipe layout is shown when user swipes items of recyclerView.

file : row_item.xml
<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:layout_width="match_parent"
             android:layout_height="wrap_content">

    <!-- Swipe Layout-->
    <include layout="@layout/swipe_row_item" />

    <!-- Regular Layout-->
    <include layout="@layout/regular_row_item" />

</FrameLayout>

file : swipe_row_item.xml
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/swipeLayout"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="horizontal"
              android:visibility="visible"
              android:background="@android:color/holo_red_dark"
              android:padding="30dp"
              android:weightSum="3">

    <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="archived"
            android:textColor="@android:color/white"
            android:textSize="24sp"/>


    <TextView
            android:id="@+id/undo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:gravity="end"
            android:paddingBottom="5dp"
            android:paddingLeft="16dp"
            android:paddingRight="16dp"
            android:paddingTop="5dp"
            android:text="undo"
            android:textColor="@android:color/white"
            android:textSize="22sp"
            android:textStyle="bold"/>

</LinearLayout>


file : regular_row_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:id="@+id/regularLayout"
        android:orientation="vertical">

    <ImageView android:layout_width="80dp"
               android:layout_height="80dp"
               android:id="@+id/img"
               android:contentDescription="@string/app_name"/>

    <TextView
            android:id="@+id/txt"
            android:textSize="22sp"
            android:text="Title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textStyle="bold"
            android:layout_toEndOf="@+id/img"
            android:layout_marginStart="20dp"/>

    <TextView
            android:id="@+id/sub_txt"
            android:textSize="18sp"
            android:text="Title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textStyle="italic"
            android:layout_toEndOf="@+id/img"
            android:layout_below="@+id/txt"
            android:layout_marginStart="20dp"/>

</RelativeLayout>

2. Adapter 

Create a Adapter That RecyclerView Can Use , Create a class CustomAdapter extend it to RecyclerView.Adapter .

3. ViewHolder 

Create Inner class ViewHolder extend it to RecyclerView.ViewHolder

file : CustomAdapter.kt
package com.tutorialsbuzz.recyclerviewswipetodeleteundo

import android.content.Context
import android.os.Handler
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.tutorialsbuzz.recyclerviewswipetodeleteundo.R
import kotlinx.android.synthetic.main.regular_row_item.view.*
import kotlinx.android.synthetic.main.swipe_row_item.view.*


class CustomAdapter(val modelList: MutableList<Model>, val context: Context) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private var itemsPendingRemoval: MutableList<Model>?
    var PENDING_REMOVAL_TIMEOUT: Long = 3000
    var handler: Handler? = Handler()
    var pendingRunnables: HashMap<Model, Runnable>? = HashMap()

    init {
        itemsPendingRemoval = mutableListOf<Model>()
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as ViewHolder).bind(modelList.get(position));
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        return ViewHolder(layoutInflater.inflate(R.layout.row_item, parent, false))
    }


    override fun getItemCount(): Int {
        return modelList.size;
    }


    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        fun bind(model: Model): Unit {

            if (itemsPendingRemoval!!.contains(model)) {
                //show swipe layout
                itemView.swipeLayout.visibility = View.VISIBLE
                itemView.regularLayout.visibility = View.GONE

                itemView.undo.setOnClickListener({ view ->
                    undoOpt(model)
                })

            } else {
                //show regular layout
                itemView.swipeLayout.visibility = View.GONE
                itemView.regularLayout.visibility = View.VISIBLE

                itemView.txt.text = model.name
                itemView.sub_txt.text = model.version
                val id = context.resources.getIdentifier(model.name.toLowerCase(), "drawable", context.packageName)
                itemView.img.setBackgroundResource(id)
            }
        }

        private fun undoOpt(model: Model) {
            val pendingRemovalRunnable: Runnable? = pendingRunnables?.get(model)
            pendingRunnables?.remove(model)
            if (pendingRemovalRunnable != null)
                handler?.removeCallbacks(pendingRemovalRunnable)
            itemsPendingRemoval?.remove(model)
            // this will rebind the row in "normal" state
            notifyItemChanged(modelList.indexOf(model))
        }

    }


    fun pendingRemoval(position: Int) {

        val data = modelList.get(position)
        if (!itemsPendingRemoval!!.contains(data)) {
            itemsPendingRemoval?.add(data)
            // this will redraw row in "undo" state
            notifyItemChanged(position)
            // let's create, store and post a runnable to remove the data
            val pendingRemovalRunnable = Runnable {
                remove(modelList.indexOf(data))
            }

            handler?.postDelayed(pendingRemovalRunnable, PENDING_REMOVAL_TIMEOUT)
            // pendingRunnables!![data] = pendingRemovalRunnable
            pendingRunnables?.put(data, pendingRemovalRunnable)
        }
    }

    fun remove(position: Int) {
        val data = modelList.get(position)
        if (itemsPendingRemoval!!.contains(data)) {
            itemsPendingRemoval?.remove(data)
        }
        if (modelList.contains(data)) {
            //dataList.remove(position)
            modelList.removeAt(position)
            notifyItemRemoved(position)
        }
    }

    fun isPendingRemoval(position: Int): Boolean {
        val data = modelList.get(position)
        return itemsPendingRemoval!!.contains(data)
    }

}

ItemTouchHelper CallBack For RecyclerView


Create an abstract class SwipeToDeleteCallback extend this class to ItemTouchHelper.SimpleCallback
  • onSwiped :  Called when a ViewHolder is swiped by the user. At this point, you should update your adapter (e.g. remove the item) and call related Adapter notify event.
  • getSwipeDirs : This callback Returns the swipe directions for the provided ViewHolder.    (Note : To disable swipe at particular viewholder return to ItemTouchHelper.ACTION_STATE_IDLE ) .
file : SwipeToDeleteCallback.kt
package com.tutorialsbuzz.recylerviewswipetodelete

import android.content.Context
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView


abstract class SwipeToDeleteCallback(var context: Context, dragDir: Int, swipeDir: Int) :
    ItemTouchHelper.SimpleCallback(dragDir, swipeDir) {


    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        return false
    }
    
}

Attaching ItemTouchHelper to RecyclerView .


Creates an instance of ItemTouchHelper that will work with the given Callback.
Call attachToRecyclerView on ItemTouchHelper instance by passing recyclerView .

val swipeToDeleteCallback =
 object : SwipeToDeleteCallback(this, 0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
  override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
   adapter.pendingRemoval(viewHolder.adapterPosition)
  }

  override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
   if (adapter.isPendingRemoval(viewHolder.adapterPosition)) {
    return ItemTouchHelper.ACTION_STATE_IDLE
   }
   return super.getSwipeDirs(recyclerView, viewHolder)
  }
 }

val itemTouchHelper = ItemTouchHelper(swipeToDeleteCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)

Swipe Undo


Pending Removals 
  • When user swipes add item to pending removal list  and show swipe layout and hide regular layout.
  • Using Handler Add Runnable to message queue , Runnable will execute after the specified amount of time elapses ,
  • Inside runnable remove the item at the swiped position .

Undo Remove / delete 
  • when undo is clicked remove the handler Callback  at swiped postion and hide swipe layout and show regular layout .

Disable Swipe at Position in RecyclerView
  • If user has already swiped once then disable swipe for that particular item of recyclerView , to do that Inside getSwipeDirs we will check if item is already added to pendingremoval list by return ItemTouchHelper.ACTION_STATE_IDLE else return super.getSwipeDirs(recyclerView, viewHolder)

Activity


file : MainActivity.kt
package com.tutorialsbuzz.recyclerviewswipetodeleteundo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.tutorialsbuzz.recyclerview.CustomAdapter
import com.tutorialsbuzz.recyclerview.Model
import com.tutorialsbuzz.recylerviewswipetodelete.SwipeToDeleteCallback
import kotlinx.android.synthetic.main.activity_main.*
import org.json.JSONArray
import org.json.JSONObject

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val layoutManager: RecyclerView.LayoutManager? = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
        recyclerView.layoutManager = layoutManager

        val modelList = readFromAsset();
        val adapter = CustomAdapter(modelList, this)
        recyclerView.adapter = adapter;

        recyclerView.addItemDecoration(SimpleDividerItemDecoration(this))

        val swipeToDeleteCallback =
            object : SwipeToDeleteCallback(this, 0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
                override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                    adapter.pendingRemoval(viewHolder.adapterPosition)
                }

                override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
                    if (adapter.isPendingRemoval(viewHolder.adapterPosition)) {
                        return ItemTouchHelper.ACTION_STATE_IDLE
                    }
                    return super.getSwipeDirs(recyclerView, viewHolder)
                }
            }

        val itemTouchHelper = ItemTouchHelper(swipeToDeleteCallback)
        itemTouchHelper.attachToRecyclerView(recyclerView)
    }

    private fun readFromAsset(): MutableList<Model> {
        val modeList = mutableListOf<Model>()
        val bufferReader = application.assets.open("android_version.json").bufferedReader()
        val json_string = bufferReader.use {
            it.readText()
        }
        val jsonArray = JSONArray(json_string);

        for (i in 0..jsonArray.length() - 1) {
            val jsonObject: JSONObject = jsonArray.getJSONObject(i)
            val model = Model(jsonObject.getString("name"), jsonObject.getString("version"))
            modeList.add(model)
        }
        return modeList
    }
}



RecyclerView Related
  1. RecyclerView
  2. RecyclerView Item Click Ripple Effect
  3. RecyclerView With Item Divider
  4. RecyclerView With CardView
  5. RecyclerView GridLayout
  6. RecyclerView StaggeredGrid Layout
  7. RecyclerView Swipe To Delete
  8. Android RecyclerView Swipe to Delete Undo
  9. Android RecyclerView Interactive Swipe Like Gmail

No comments:

Post a Comment