4

Is there any way to disable the CardView elevation animation from a recyclerview when an item is changed but to keep the original shadow and item animator?

I've tried to disable the CardView state animator android:stateListAnimator="@null" with no results.

Here's a way to reproduce the issue:

package com.w.cardviewrecyclerviewanim;

import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.CardView;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class MainActivity extends AppCompatActivity {

    RecyclerView items;
    Adapter adapter = new Adapter();
    GridLayoutManager layoutManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        items = findViewById(R.id.items);
        layoutManager = new GridLayoutManager(this, 2, LinearLayoutManager.VERTICAL, false);
        items.setLayoutManager(layoutManager);
        items.setAdapter(adapter);
    }

    class Holder extends RecyclerView.ViewHolder{
        CardView cardView;

        public Holder(@NonNull View itemView) {
            super(itemView);
            cardView = itemView.findViewById(R.id.card_view);
        }

        void bind (){
            cardView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    adapter.notifyItemChanged(getAdapterPosition());
                }
            });
            cardView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    adapter.notifyItemRangeChanged(0, adapter.getItemCount());
                    return true;
                }
            });
        }
    }

    class Adapter extends RecyclerView.Adapter<Holder>{

        @NonNull
        @Override
        public Holder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
            LayoutInflater layoutInflater = LayoutInflater.from(viewGroup.getContext());
            return new Holder(layoutInflater.inflate(R.layout.item, viewGroup, false));
        }

        @Override
        public void onBindViewHolder(@NonNull Holder holder, int position) {
            holder.bind();
        }

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

And here's the item.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="160dp"
    >

    <android.support.v7.widget.CardView
        android:id="@+id/card_view"

        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="10dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"

        app:cardCornerRadius="8dp"
        app:cardElevation="5dp"
        android:stateListAnimator="@null"
        >

    </android.support.v7.widget.CardView>

</android.support.constraint.ConstraintLayout>

I have a simple RecyclerViewer that has 30 items and each item has a CardView with 10dp margin and a cardElevation of 5dp just to have some nice shadow. When an item gets clicked the recyclerview adapter receives a notifyItemChanged(getAdapterPosition()) and on long click all items receives a change event via a notifyItemRangeChanged(0, adapter.getItemCount()) call to the adapter;

With the DefaultItemAnimator whenever an item gets "changed" the CardView shadow gets animated bigger during the transition animation. I want to keep the transition as I'm doing some updates in the item but I don't want to have the shadow/elevation animation. You can see the outcome here: https://gph.is/g/ajmNGen

Think that on long click the application moves into a "multi select" mode where all the items have big changes in layout and I want this change to be animated (by the DefaulItemAnimator) and I would like to keep the shadow as set originally.

Any help is really appreciated guys as I'm pulling my hair while dealing with this.

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
  • You say you want to "keep the transition"; what does this mean? Do you want to do a crossfade of the card contents? Something else? – Ben P. May 14 '19 at 16:54
  • Hi Ben, cross fade indeed. That's the default behavior of the ItemAnimator (DefaultItemAnimator). I want to keep that cross fade. The "shadow" problem is connected to the DefaultItemAnimator and it happens only in the transition, in the cross fade. If I disable the animator [items.setItemAnimator (null)] the shadow problem is "gone" but so is the cross-fade and items just pops from present mode into multi-select and further down the pipeline. – Jean-Arthur Deda May 14 '19 at 19:00
  • Maybe this will shed some light. Let's say on every bind we change the card color randomly. By adding at the end of the bind() function from Holder this little line: `cardView.setCardBackgroundColor(0xff000000 | (int)(Math.random() * (double)0xffffff));` Now, every-time a card is touched, the item gets binded again and has a new color. The cross fade will change smoothly between the old color and the new color, instead of the new color just poping in. The problem is that during the transition/cross fade the shadow is playing an unwanted animation. – Jean-Arthur Deda May 14 '19 at 19:28
  • I believe you are going to have to implement your own item animator. You could pass a `notifyItemChanged()` payload to disable the entire default animation (see https://stackoverflow.com/a/47355363/8298909), but then you wouldn't get a crossfade on the card contents. This Google presentation https://www.youtube.com/watch?v=imsr8NrIAMs covers how to do custom animations (including color fading). – Ben P. May 14 '19 at 19:57
  • Thanks, and good call Ben. However, I've already implemented a new ItemAnimator. Pretty much is just the https://android.googlesource.com/platform/frameworks/support/+/8cf399b/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java with a small change where I don't do the crossfade but instead I keep the newHolder to alpha 1 and just fadeout the oldHolder (which is on top) to alpha 0 to avoid a flicker to background (white) towards alpha .5. The problem is there for the DefaultItemAnimator and the new one. And the funny part, none of them ever touch the translateZ or elevation. – Jean-Arthur Deda May 15 '19 at 06:31
  • And to just add a bit more info, the issue seems to be API related. For instance on API 23 (S5) there's no shadow issue and on API 28 (Google Pixel) it's there alright. – Jean-Arthur Deda May 15 '19 at 08:05
  • I think the key is that the framework draws a difference between an item change that can re-use the ViewHolder and one that cannot. Try overriding `canReuseUpdatedViewHolder()` to just `return true` and see if that fixes the issue; if so, then you can do more sophisticated logic to only re-use ViewHolders when appropriate. – Ben P. May 15 '19 at 13:11
  • Will do, thanks Ben. Another solution, that unfortunately looks more and more like the one, is to not use the default change animation and implement, all the needed transitions, case by case, in the Holder bind via pay loads events. Sigh... – Jean-Arthur Deda May 15 '19 at 17:28

1 Answers1

2

Set a constraint layout inside cardView, then you can clearly see the shadow:-

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.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_width="match_parent"
    android:layout_height="match_parent"
    android:elevation="6dp"
    tools:context=".MainActivity">

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </android.support.constraint.ConstraintLayout>

</android.support.v7.widget.CardView>
Daniel
  • 400
  • 1
  • 2
  • 12
Sandeep Malik
  • 1,972
  • 1
  • 8
  • 17