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
currentSelectedItemIndexvariable holds the selected item's index. - The visibility of the button is changed based on the
selectedproperty. - The
bindRecyclerViewmethod is called for horizontalRecyclerViewitems. The adapter will call theshowHideDeletecallback method to change. - The
deselectItemandmarkSelectedItemmethods are used to deselect an item and mark one item selected. These methods will change theselectedproperty of the item atindexposition. It also updates thecurrentSelectedItemIndexand changes the status of the delete button. - The
deleteSelectedItemmethod 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
showHideDeletemethod is changing the visibility status of the menu. - On clicking the
menu_deletemenu item, we are calling thedeleteItemmethod. - The callback method on creating the
myAdapterisshowHideDelete. - The
deleteItemmethod shows oneAlertDialogbefore it deletes an item. It calls thedeleteSelectedItemmethod 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

