Swipe en un RecyclerView

Cómo añadir swipe a un RecyclerView

Uso de swipe

Repositorio en Github

Crear una nueva rama llamada feature/swipe en el proyecto RecyclerViewExample:

git checkout master
git checkout -b feature/swipe

Ficheros del proyecto

res/layout/item_superhero.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
    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_marginHorizontal="16dp"
    android:layout_marginVertical="8dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/ivSuperHero"
            android:layout_width="150dp"
            android:layout_height="150dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:src="@drawable/ic_launcher_background" />

        <TextView
            android:id="@+id/tvSuperHeroName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="21sp"
            android:textStyle="bold"
            android:textColor="@color/black"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/ivSuperHero"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="KotlinMan" />

        <TextView
            android:id="@+id/tvRealName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/ivSuperHero"
            app:layout_constraintTop_toBottomOf="@+id/tvSuperHeroName"
            tools:text="Julio" />

        <TextView
            android:id="@+id/tvPublisher"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            tools:text="Hola" />


    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

res/layout/acitvity_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">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerSuperHero"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.03" />
</androidx.constraintlayout.widget.ConstraintLayout>

adapter/SuperHeroViewHolder.kt

class SuperHeroViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    val binding = ItemSuperheroBinding.bind(view)

    fun render(superHeroModel: SuperHero, onClickListener: (SuperHero) -> Unit) {
        binding.tvSuperHeroName.text = superHeroModel.superhero
        binding.tvRealName.text = superHeroModel.realName
        binding.tvPublisher.text = superHeroModel.publisher
        // Glide.with(binding.ivSuperHero.context).load(superHeroModel.photo).into(binding.ivSuperHero)
        Picasso.get().load(superHeroModel.photo).into(binding.ivSuperHero);
        itemView.setOnClickListener {
            onClickListener(superHeroModel)
        }
    }
}

adapter/SuperHeroAdapter.kt

class SuperHeroAdapter(
    private val superheroList: List<SuperHero>,
    private val onClickListener: (SuperHero) -> Unit
) : RecyclerView.Adapter<SuperHeroViewHolder>() {

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

    override fun onBindViewHolder(holder: SuperHeroViewHolder, position: Int) {
        val item = superheroList[position]
        holder.render(item, onClickListener)
    }

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

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var superHeroMutableList: MutableList<SuperHero> = SuperHeroProvider.superHeroList.toMutableList()
    private lateinit var adapter: SuperHeroAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initRecyclerView()
    }

    private fun initRecyclerView() {
        val manager = LinearLayoutManager(this)
        val decoration = DividerItemDecoration(this, manager.orientation)
        binding.recyclerSuperHero.layoutManager = manager
        adapter =
            SuperHeroAdapter(superHeroMutableList,
                { superHero ->onItemSelected(superHero)}
            )
        binding.recyclerSuperHero.adapter = adapter
        binding.recyclerSuperHero.addItemDecoration(decoration)

        //right swipe on recyclerview
        val itemSwipe =  object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
            override fun onMove(
                recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder,
                target: RecyclerView.ViewHolder
            ): Boolean {
                //TODO("Not yet implemented")
                return false
            }
            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                showDialog(viewHolder)
            }
        }
        val swipe = ItemTouchHelper(itemSwipe)
        swipe.attachToRecyclerView(binding.recyclerSuperHero)
    }

    private fun showDialog(viewHolder: RecyclerView.ViewHolder) {
        val builder = AlertDialog.Builder(this)
        builder.setTitle("Delete Item")
        builder.setMessage("Are you sure to delete the item?")
        builder.setPositiveButton("Confirm") {dialog, which ->
            // val position = viewHolder.adapterPosition
            val position = viewHolder.getBindingAdapterPosition()
            superHeroMutableList.removeAt(position)
            // adapter.notifyItemRemoved(position)
            adapter?.notifyDataSetChanged()
        }
        builder.setNegativeButton("Cancel") {dialog, which ->
            // val position = viewHolder.adapterPosition
            val position = viewHolder.getBindingAdapterPosition()
            adapter?.notifyItemChanged(position)
        }
        builder.show()
    }

    fun onItemSelected(superHero: SuperHero) {
        Toast.makeText(this, superHero.superhero, Toast.LENGTH_SHORT).show()
    }
}

Uso de Material Design en showDialog()

Pregunta a Gemini: how can I improve the dialog fun to use material design 3?

// import com.google.android.material.dialog.MaterialAlertDialogBuilder

private fun showDialog(viewHolder: RecyclerView.ViewHolder) {
    // Use MaterialAlertDialogBuilder for Material 3 styling
    val builder = MaterialAlertDialogBuilder(this)
    builder.setTitle("Delete Item")
    builder.setMessage("Are you sure you want to delete this item?")
    builder.setPositiveButton("Confirm") { dialog, which ->
        val position = viewHolder.bindingAdapterPosition
        // Check if the position is valid before removing
        if (position != RecyclerView.NO_POSITION) {
            superHeroMutableList.removeAt(position)
            adapter.notifyItemRemoved(position)
        }
    }
    builder.setNegativeButton("Cancel") { dialog, which ->
        val position = viewHolder.bindingAdapterPosition
        // Check if the position is valid before notifying the adapter
        if (position != RecyclerView.NO_POSITION) {
            // Restore the item's appearance if the user cancels the swipe
            adapter.notifyItemChanged(position)
        }
    }
    // It's good practice to ensure the dialog is not cancelable by tapping outside
    // for this type of confirmation, to prevent accidental dismissal.
    builder.setCancelable(false)
    builder.show()
}

Key Improvements in This Code:
1.Material 3 Dialog:

By switching to MaterialAlertDialogBuilder, the dialog (including buttons, typography, and shape) will automatically adopt the Material You styling from your app’s theme.

2.More Efficient RecyclerView Updates:

I’ve replaced notifyDataSetChanged() with notifyItemRemoved(position). This is much more efficient because it animates the removal of a single item instead of redrawing the entire list.
In the «Cancel» block, notifyItemChanged(position) is used to correctly reset the state of the swiped item if the user decides not to delete it.

3.Safer Position Handling:

I’ve changed getBindingAdapterPosition() to the property bindingAdapterPosition.
I’ve added a check (position != RecyclerView.NO_POSITION) to prevent potential crashes if the view holder’s position becomes invalid while the dialog is open.

 

Deja una respuesta