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
    }
}


No comments:

Post a Comment