Close
Close full mode
logoAppDevAssist

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 a menu xml file:

Create a file src/main/res/menu/main_menu.xml. It holds the delete button to show in the menu.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_delete"
android:title="Item"
android:icon="@drawable/ic_delete"
app:showAsAction="always"/>
</menu>

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.RecyclerView
android:id="@+id/recyclerView"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android: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"
/>
<TextView
android: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" />
<Button
android: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" />
<TextView
android: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.ShimmerFrameLayout
android:id="@+id/shimmer_view_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android: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.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android: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.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android: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"
/>
<TextView
android: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" />
<TextView
android: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.myapplication
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.myapplication.models.Property
import 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 = -1
inner 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.VISIBLE
recyclerView.visibility = View.GONE
+ if (property.selected == true) {
+ button.visibility = View.VISIBLE
+ } else {
+ button.visibility = View.GONE
+ }
title.text = property.title
description.text = property.description
Glide.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.GONE
recyclerView.visibility = View.VISIBLE
val 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 = manager
adapter = 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 horizontal RecyclerView items. The adapter will call the showHideDelete callback method to change.
  • The deselectItem and markSelectedItem methods are used to deselect an item and mark one item selected. These methods will change the selected property of the item at index position. It also updates the currentSelectedItemIndex 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.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
+import android.view.Menu
+import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.example.myapplication.models.Property
import com.example.myapplication.network.Api
import com.facebook.shimmer.ShimmerFrameLayout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MainActivity : AppCompatActivity() {
lateinit var data: MutableList<Property>
private lateinit var recyclerView: RecyclerView
private lateinit var manager: RecyclerView.LayoutManager
private lateinit var myAdapter: MyAdapter
private lateinit var swipeRefresh: SwipeRefreshLayout
private lateinit var shrimmerView: ShimmerFrameLayout
+ private var mainMenu: Menu? = null
override 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.GONE
if(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 = manager
adapter = 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 the deleteItem method.
  • The callback method on creating the myAdapter is showHideDelete.
  • The deleteItem method shows one AlertDialog before it deletes an item. It calls the deleteSelectedItem 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.Property
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
private 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.

RecyclerView long press select item
RecyclerView long press select 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

Subscribe to our Newsletter

Previous
How to show a popup Alert in Android with a RecyclerView in Kotlin
Next
How to create a Floating Action Button in Android using Kotlin