2

I've followed Nick Butcher's Material Improvements I/O 2016 talk, and at about 6:00, he starts talking about animating list items. I've implemented the feature exactly as he was doing it, and the bound changes animate correctly, but color changes don't animate, despite him explicitly saying they would:

animation

This is what the code looks like:

This is the relevant part of the RecyclerView.Adapter class:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val item: Pair<String, String> = items[position]

    holder.title.text = item.first
    holder.subtitle.text = item.second

    val isExpanded = position == expandedPosition
    holder.subtitle.visibility = if (isExpanded) View.VISIBLE else View.GONE
    holder.itemView.isActivated = isExpanded
    holder.itemView.setOnClickListener {
        expandedPosition = if (isExpanded) -1 else position
        TransitionManager.beginDelayedTransition(recyclerView)
        notifyDataSetChanged()
    }
}

This is the layout I'm using for the items. ConstraintLayout is kind of overkill for the current layout setup, but I reduced the layout to create a minimal example.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context=".MainActivity"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/item_background"
    android:stateListAnimator="@animator/item_elevation"
    android:padding="8dp">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        tools:text="Title 1"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"/>

    <TextView
        android:id="@+id/subtitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="Subtitle 1"
        app:layout_constraintTop_toBottomOf="@id/title"
        app:layout_constraintStart_toStartOf="parent"
        />

</android.support.constraint.ConstraintLayout>

And this is the background I'm using for the item layout:

<selector xmlns:android="http://schemas.android.com/apk/res/android"
    android:enterFadeDuration="@android:integer/config_mediumAnimTime"
    android:exitFadeDuration="@android:integer/config_mediumAnimTime">

    <item android:state_activated="true"
        android:drawable="@color/colorAccent" />

    <item android:drawable="@color/colorPrimaryDark" />

</selector>
Aleksandar Stefanović
  • 1,583
  • 2
  • 20
  • 36

1 Answers1

4

Make sure that you have stable ids for your dataset and call Recycler.Adapter#setHasStableIds(true). See setHasStableIds().

You will also need to override getItemId() in the adapter to return a stable id. Also see notifyDataSetChanged for a short discussion of stable ids.

RecyclerView will attempt to synthesize visible structural change events for adapters that report that they have 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.

Here is an demonstration with stable ids set to false:

enter image description here

and with stable ids set to true. I have make the transition time very long on the color change so it would be apparent.

enter image description here

RecyclerViewAdapter.java

This is the adapter used in the demo. TransitionManager.beginDelayedTransition(mRecyclerView) has been removed to let RecyclerView better handle the animation.

class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    RecyclerViewAdapter() {
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;
        view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new ItemViewHolder(view);
    }

    private int mExpandedPosition = RecyclerView.NO_POSITION;

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        final ItemViewHolder vh = (ItemViewHolder) holder;
        final TextView subTitle = vh.mSubTitle;

        vh.mTitle.setText(titleForPosition(position));
        subTitle.setText(subTitleForPosition(position));

        final boolean isExpanded = position == mExpandedPosition;

        subTitle.setVisibility((isExpanded) ? View.VISIBLE : View.GONE);
        holder.itemView.setActivated(isExpanded);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mExpandedPosition = isExpanded ? RecyclerView.NO_POSITION : vh.getAdapterPosition();
                notifyDataSetChanged();
            }
        });
    }

    private String titleForPosition(int position) {
        return "Title " + position;
    }

    private String subTitleForPosition(int position) {
        return "Subtitle " + position;
    }

    @Override
    public int getItemCount() {
        return 5;
    }

    @Override
    public long getItemId(int position) {
        return titleForPosition(position).hashCode();
    }

    static class ItemViewHolder extends RecyclerView.ViewHolder {
        private final TextView mTitle;
        private final TextView mSubTitle;

        ItemViewHolder(View itemView) {
            super(itemView);
            mTitle = itemView.findViewById(R.id.title);
            mSubTitle = itemView.findViewById(R.id.subtitle);
        }
    }

    @SuppressWarnings("unused")
    private final static String TAG = "RecyclerViewAdapter";
}
Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • Unfortunately, setting `setHasStableIds` causes noticeable overlapping when the views are being resized. It looks like the currently selected item disappears from the list, and then animates from the start or the end of the list. I will post my layout in the question... – Aleksandar Stefanović Apr 24 '18 at 06:08
  • @AleksandarStefanović Remove `TransitionManager.beginDelayedTransition(mRecyclerView)` from the adapter and let `RecyclerView` do its thing. The color transition issue is resolved? – Cheticamp Apr 24 '18 at 11:35
  • Removing `beginDelayedTransition` fixes the weird resize animation, but the color change still doesn't animate. – Aleksandar Stefanović Apr 24 '18 at 16:12
  • @AleksandarStefanović Are your stable ids set up? What are you returning for [`getItemId()`](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemId(int))? – Cheticamp Apr 24 '18 at 16:16
  • yes, `setHasStableIds` is enabled. As for `getItemId()`, I'm using hash value of the data of the item in the data array, i.e. `items[position].name.hashCode().toLong()` – Aleksandar Stefanović Apr 24 '18 at 16:24
  • If you have a working example, can you post the relevant code in your answer? – Aleksandar Stefanović Apr 24 '18 at 16:45
  • It works now. I'm not sure what the issue was, I think it was animation duration actually. I will accept your answer, but please put the used code into your answer, so that the people who encounter this problem also see the solution (without looking through comments). – Aleksandar Stefanović Apr 24 '18 at 18:53