How to implement long press to select an item in RecyclerView
How to implement long press to select an item in RecyclerView:
This post will show you how to implement long press to select an item in RecyclerView
in Android
using Kotlin
. This project will render a RecyclerView
and on clicking any item, it will show a tick mark on that item and one delete button in the ToolBar
.
We will use the same project used in the RecyclerView
tutorial series.
YouTube video:
You can watch the YouTube video for this article here:
Project Setup:
We will use one Node.js
backend for this example project. Download it from here and run npm install && npm run start
or yarn && yarn start
to start the server on the 3000
port on your local system.
localhost:3000/horizontal
will return the response used in this example.
Add the icons:
We need two icons to show in the list item if the item is selected or not and to show one delete button in the menu.
app\src\main\res\drawable\ic_check.xml
<vector android:height="24dp" android:tint="#3BD920"android:viewportHeight="24" android:viewportWidth="24"android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"><path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/></vector>
app\src\main\res\drawable\ic_delete.xml
<vector android:height="24dp" android:tint="#FFFFFF"android:viewportHeight="24" android:viewportWidth="24"android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"><path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/></vector>
Update the RecyclerView list item file:
The xml
file app\src\main\res\layout\list_item.xml
is used as the layout file for each item of the RecyclerView
. We need to add the ic_check.xml
icon to this layout. This icon will be hidden by default.
<?xml version="1.0" encoding="utf-8"?><androidx.cardview.widget.CardView android:layout_height="wrap_content"android:layout_width="match_parent"xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"app:cardCornerRadius="8dp"app:cardElevation="8dp"android:layout_margin="5dp"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recyclerView"android:visibility="gone"android:layout_width="match_parent"android:layout_height="wrap_content"/><androidx.constraintlayout.widget.ConstraintLayoutandroid:id="@+id/constraintLayout"android:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/imageView"android:layout_width="0dp"android:layout_height="0dp"app:layout_constraintWidth_percent=".3"app:layout_constraintDimensionRatio="1:1"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintLeft_toLeftOf="parent"android:src="@drawable/ic_launcher_background"/><TextViewandroid:id="@+id/tvTitle"android:layout_width="0dp"android:layout_height="wrap_content"app:layout_constraintStart_toEndOf="@+id/imageView"app:layout_constraintEnd_toStartOf="@id/button"app:layout_constraintTop_toTopOf="parent"android:textSize="25sp"android:fontFamily="sans-serif"android:textColor="#212121"android:layout_marginStart="10dp"android:text="This is a very long title and here it is"android:layout_marginLeft="10dp" /><Buttonandroid:id="@+id/button"android:layout_width="0dp"android:layout_height="0dp"android:layout_marginTop="5dp"android:layout_marginEnd="5dp"+ android:visibility="gone"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent"+ android:background="@drawable/ic_check"app:layout_constraintWidth_percent=".08"app:layout_constraintDimensionRatio="1:1"android:layout_marginRight="5dp" /><TextViewandroid:id="@+id/tvDescription"android:layout_width="0dp"android:layout_height="wrap_content"app:layout_constraintStart_toStartOf="@+id/tvTitle"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toBottomOf="@+id/tvTitle"app:layout_constraintBottom_toBottomOf="parent"android:textSize="18sp"android:layout_marginRight="10dp"android:fontFamily="sans-serif"android:text="This is a very long title and here it is"android:layout_marginEnd="10dp" /></androidx.constraintlayout.widget.ConstraintLayout></androidx.cardview.widget.CardView>
We are using a Button
in this example. You can use any other component if you want.
The other xml
files are unchanged.
layout/activity_main.xml:
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.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"tools:context=".MainActivity"><com.facebook.shimmer.ShimmerFrameLayoutandroid:id="@+id/shimmer_view_container"android:layout_width="match_parent"android:layout_height="wrap_content"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><include layout="@layout/list_item_shrimmer" /><include layout="@layout/list_item_shrimmer" /><include layout="@layout/list_item_shrimmer" /><include layout="@layout/list_item_shrimmer" /><include layout="@layout/list_item_shrimmer" /><include layout="@layout/list_item_shrimmer" /><include layout="@layout/list_item_shrimmer" /></LinearLayout></com.facebook.shimmer.ShimmerFrameLayout><androidx.swiperefreshlayout.widget.SwipeRefreshLayoutandroid:id="@+id/swipeRefresh"android:layout_width="match_parent"android:layout_height="match_parent"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginStart="1dp"android:layout_marginLeft="1dp"android:layout_marginTop="25dp"android:layout_marginEnd="1dp"android:layout_marginRight="1dp"android:layout_marginBottom="1dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.swiperefreshlayout.widget.SwipeRefreshLayout></androidx.constraintlayout.widget.ConstraintLayout>
layout/list_item_shrimmer.xml:
<?xml version="1.0" encoding="utf-8"?><androidx.cardview.widget.CardView android:layout_height="wrap_content"android:layout_width="match_parent"xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_margin="5dp"><androidx.constraintlayout.widget.ConstraintLayoutandroid:id="@+id/constraintLayout"android:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/imageView"android:layout_width="0dp"android:layout_height="0dp"app:layout_constraintWidth_percent=".3"app:layout_constraintDimensionRatio="1:1"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintLeft_toLeftOf="parent"android:background="@android:color/darker_gray"/><TextViewandroid:id="@+id/tvTitle"android:layout_width="0dp"android:layout_height="wrap_content"app:layout_constraintStart_toEndOf="@+id/imageView"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent"android:textSize="15sp"android:fontFamily="sans-serif"android:textColor="#212121"android:layout_marginStart="10dp"android:layout_marginEnd="20dp"android:layout_marginRight="20dp"android:layout_marginTop="10dp"android:background="@android:color/darker_gray"android:layout_marginLeft="10dp" /><TextViewandroid:id="@+id/tvDescription"android:layout_width="0dp"android:layout_height="wrap_content"app:layout_constraintStart_toStartOf="@+id/tvTitle"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toBottomOf="@+id/tvTitle"app:layout_constraintBottom_toBottomOf="parent"android:textSize="10sp"android:layout_marginEnd="50dp"android:layout_marginRight="50dp"android:fontFamily="sans-serif"android:background="@android:color/darker_gray"/></androidx.constraintlayout.widget.ConstraintLayout></androidx.cardview.widget.CardView>
Changes to the model file:
We will add one Boolean
property selected
to the app\src\main\java\com\example\myapplication\models\Property.kt
file. This property will define if the item is selected or not.
data class Property(val id: Int, val title: String = "", val description: String = "", val image: String = "", val horizontal: Boolean = false, val data: List<Property>? = null, var selected: Boolean? = false)
Changes to the adapter:
Following are the changes to the app\src\main\java\com\example\myapplication\MyAdapter.kt
file:
package com.example.myapplicationimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.widget.Buttonimport android.widget.ImageViewimport android.widget.TextViewimport androidx.constraintlayout.widget.ConstraintLayoutimport androidx.recyclerview.widget.LinearLayoutManagerimport androidx.recyclerview.widget.RecyclerViewimport com.bumptech.glide.Glideimport com.example.myapplication.models.Propertyimport org.w3c.dom.Text+ class MyAdapter(private val data: List<Property>, val showHideDelete: (Boolean) -> Unit) :RecyclerView.Adapter<MyAdapter.MyViewHolder>() {private var listData: MutableList<Property> = data as MutableList<Property>+ var currentSelectedItemIndex = -1inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {+ fun bind(property: Property, index: Int) {val title = view.findViewById<TextView>(R.id.tvTitle)val imageView = view.findViewById<ImageView>(R.id.imageView)val description = view.findViewById<TextView>(R.id.tvDescription)val button = view.findViewById<Button>(R.id.button)val constraintLayout = view.findViewById<ConstraintLayout>(R.id.constraintLayout)val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)constraintLayout.visibility = View.VISIBLErecyclerView.visibility = View.GONE+ if (property.selected == true) {+ button.visibility = View.VISIBLE+ } else {+ button.visibility = View.GONE+ }title.text = property.titledescription.text = property.descriptionGlide.with(view.context).load(property.image).centerCrop().into(imageView)+ constraintLayout.setOnLongClickListener { markSelectedItem(index) }+ constraintLayout.setOnClickListener { deselectItem(index) }}fun bindRecyclerView(data: List<Property>) {val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)val constraintLayout = view.findViewById<ConstraintLayout>(R.id.constraintLayout)constraintLayout.visibility = View.GONErecyclerView.visibility = View.VISIBLEval manager: RecyclerView.LayoutManager =LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, true)recyclerView.apply {val data = data as MutableList<Property>+ var myAdapter = MyAdapter(data) { show -> showHideDelete(show) }layoutManager = manageradapter = myAdapter}}}+ fun deselectItem(index: Int) {+ if (currentSelectedItemIndex == index) {+ currentSelectedItemIndex = -1+ listData.get(index).selected = false+ notifyDataSetChanged()+ showHideDelete(false)+ }+ }+ fun markSelectedItem(index: Int): Boolean {+ for (item in listData) {+ item.selected = false+ }+ listData.get(index).selected = true+ currentSelectedItemIndex = index+ notifyDataSetChanged()+ showHideDelete(true)+ return true+ }+ fun deleteSelectedItem() {+ if (currentSelectedItemIndex != -1) {+ listData.removeAt(currentSelectedItemIndex)+ notifyDataSetChanged()+ }+ }override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)return MyViewHolder(v)}override fun getItemCount(): Int {return listData.size}override fun onBindViewHolder(holder: MyViewHolder, position: Int) {if (listData[position].horizontal) {listData[position].data?.let { holder.bindRecyclerView(it) }} else {holder.bind(listData[position], position)}}fun deleteItem(index: Int) {// listData.removeAt(index)// notifyDataSetChanged()}fun setItems(items: List<Property>) {listData = items as MutableList<Property>notifyDataSetChanged()}}
- While creating the adapter, we need to pass a callback method
showHideDelete
. - The
currentSelectedItemIndex
variable holds the selected item's index. - The visibility of the button is changed based on the
selected
property. - The
bindRecyclerView
method is called for horizontalRecyclerView
items. The adapter will call theshowHideDelete
callback method to change. - The
deselectItem
andmarkSelectedItem
methods are used to deselect an item and mark one item selected. These methods will change theselected
property of the item atindex
position. It also updates thecurrentSelectedItemIndex
and changes the status of the delete button. - The
deleteSelectedItem
method removes the currently selected item.
Changes to the MainActivity:
We need to make the following changes to the MainActivity.kt
file:
package com.example.myapplicationimport androidx.appcompat.app.AppCompatActivityimport android.os.Bundleimport android.os.Handlerimport android.os.Looper+import android.view.Menu+import android.view.MenuItemimport android.view.Viewimport android.widget.Toastimport androidx.appcompat.app.AlertDialogimport androidx.recyclerview.widget.LinearLayoutManagerimport androidx.recyclerview.widget.RecyclerViewimport androidx.swiperefreshlayout.widget.SwipeRefreshLayoutimport com.example.myapplication.models.Propertyimport com.example.myapplication.network.Apiimport com.facebook.shimmer.ShimmerFrameLayoutimport retrofit2.Callimport retrofit2.Callbackimport retrofit2.Responseclass MainActivity : AppCompatActivity() {lateinit var data: MutableList<Property>private lateinit var recyclerView: RecyclerViewprivate lateinit var manager: RecyclerView.LayoutManagerprivate lateinit var myAdapter: MyAdapterprivate lateinit var swipeRefresh: SwipeRefreshLayoutprivate lateinit var shrimmerView: ShimmerFrameLayout+ private var mainMenu: Menu? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)manager = LinearLayoutManager(this)swipeRefresh = findViewById(R.id.swipeRefresh)shrimmerView = findViewById(R.id.shimmer_view_container)swipeRefresh.setOnRefreshListener {getAllData()}getAllData()}+ private fun showHideDelete(show: Boolean){+ mainMenu?.findItem(R.id.menu_delete)?.isVisible = show+ }+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {+ mainMenu = menu+ menuInflater.inflate(R.menu.main_menu, menu)+ showHideDelete(false)+ return super.onCreateOptionsMenu(menu)+ }+ override fun onOptionsItemSelected(item: MenuItem): Boolean {+ if(item.itemId == R.id.menu_delete){+ deleteItem()+ }+ return super.onOptionsItemSelected(item)+ }fun getAllData(){Api.retrofitService.getAllData().enqueue(object: Callback<List<Property>>{override fun onResponse(call: Call<List<Property>>,response: Response<List<Property>>) {shrimmerView.stopShimmer()shrimmerView.visibility = View.GONEif(swipeRefresh.isRefreshing){swipeRefresh.isRefreshing = false}if(response.isSuccessful){recyclerView = findViewById<RecyclerView>(R.id.recycler_view).apply{data = response.body() as MutableList<Property>+ myAdapter = MyAdapter(data){show -> showHideDelete(show)}layoutManager = manageradapter = myAdapter}}}override fun onFailure(call: Call<List<Property>>, t: Throwable) {t.printStackTrace()}})}fun deleteItem(){val alertBuilder = AlertDialog.Builder(this)alertBuilder.setTitle("Delete")alertBuilder.setMessage("Do you want to delete this item ?")alertBuilder.setPositiveButton("Delete"){_,_ ->+ if(::myAdapter.isInitialized){+ myAdapter.deleteSelectedItem()+ showHideDelete(false)+ Toast.makeText(this, "Item deleted", Toast.LENGTH_SHORT).show()+ }}alertBuilder.setNegativeButton("No"){_,_ ->}alertBuilder.setNeutralButton("Cancel"){_,_ ->}alertBuilder.show()}}
- The
showHideDelete
method is changing the visibility status of the menu. - On clicking the
menu_delete
menu item, we are calling thedeleteItem
method. - The callback method on creating the
myAdapter
isshowHideDelete
. - The
deleteItem
method shows oneAlertDialog
before it deletes an item. It calls thedeleteSelectedItem
method of the adapter to delete the selected ite.
Changes to the APIService class:
We don't need to make any changes to the APIService
class. Make sure to run the nodejs server to make this project run.
import com.example.myapplication.models.Propertyimport com.squareup.moshi.Moshiimport com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactoryimport retrofit2.Callimport retrofit2.Retrofitimport retrofit2.converter.moshi.MoshiConverterFactoryimport retrofit2.http.GETprivate const val BASE_URL = "http://10.0.2.2:3000/horizontal/"private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()private val retrofit = Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(moshi)).baseUrl(BASE_URL).build()interface ApiService{@GET(".")fun getAllData(): Call<List<Property>>}object Api {val retrofitService: ApiService by lazy{retrofit.create(ApiService::class.java)}}
Output:
If you run the project, it will show one list of items. If you long press on one item, it will show a green tick to the right of the item and the delete button will be visible. You can delete the item by clicking on this button. If you click on the same item again, it will deselect that item.
GitHub:
The code is available on Github. Please use the tut-single-tag
tag to get the code used in this tutorial.
git clone https://github.com/AppDevAssist/recyclerview-kotlin && git checkout tags/tut-single-tag