Search code examples
androidandroid-recyclerviewandroid-animationandroid-selector

How to use AnimatedStateListDrawable inside a RecyclerView


I am trying to make a RecyclerView of a list of Tasks with done and undone states and I want to create a beautiful transition between those states.

So I created an animated-selector to make the transition between the states, but the transitions is not working when I use it inside the RecyclerView, when I test it on a Button/ImageView outside the RecyclerView it works as it should.

Screen with the RecyclerView

aslv_status.xml

<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/done"
        android:drawable="@drawable/ic_done"
        android:state_selected="true" />
    <item
        android:id="@+id/undone"
        android:drawable="@drawable/ic_undone" />
    <transition
        android:drawable="@drawable/avd_done_to_undone"
        android:fromId="@id/done"
        android:toId="@id/undone" />
    <transition
        android:drawable="@drawable/avd_undone_to_undone"
        android:fromId="@id/undone"
        android:toId="@id/done" />
</animated-selector>

I toggle the attribute isDone of the Task in the adapter item onClickListener and seted the animated-selector as the android:src of the AppCompatImageView of my recyclerView adapter item:

item_task.xml

<com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{() -> viewModel.toggleTaskState(task)}" >
    ...
    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/imageview_status_image"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:src="@drawable/aslv_status"
        .../>
    ...

TaskViewModel.kt

fun toggleTaskState(task: Task) = viewModelScope.launch {
    task.isDone = !task.isDone
    taskRepository.updateTask(task)
}

On TaskAdapter on init I call notifyDataSetChanged() when the liveData allTasks change, and on TaskViewHolder binding I updated the AppCompatImageView selected state.

TaskAdapter.kt

private var allTasks = viewModel.allTasks

init {
    viewModel.allTasks.observe(lifecycle, {
        notifyDataSetChanged()
    })
}
...
class TaskViewHolder(private var binding: ItemTaskBinding, private val viewModel: TaskViewModel)
    : RecyclerView.ViewHolder(binding.root) {
    fun bind(task: Task) {
        binding.task = task
        binding.viewModel = viewModel
        binding.imageviewStatusImage.isSelected = task.isDone
        binding.executePendingBindings()
    }
}

I think the problem is the notifyDataSetChanged() called when I change the state of the Task object, because I imagine the RecyclerView updates the layout abruptly and doesn't show the transition state animation, but if I don't call the notifyDataSetChanged() the RecyclerView won't change the list items state.


Solution

  • Responding in case someone goes through the same situation as me.

    In the notifyDataSetChanged() method documentation:

    RecyclerView will attempt to synthesize visible structural change events for adapters that report that they have {@link #hasStableIds() stable IDs} when this method is used. This can help for the purposes of animation and visual object persistence but individual item views will still need to be rebound and relaid out.

    So I setted hasStableIds() to my Adapter and it worked!

    val taskAdapter = TaskAdapter(this, taskViewModel)
    taskAdapter.setHasStableIds(true)