web-dev-qa-db-fra.com

Détecter la fin de l'animation dans RecyclerView d'Android

Le RecyclerView, contrairement à ListView, n'a pas un moyen simple de lui donner une vue vide, il faut donc le gérer manuellement, rendant la vue vide visible en cas de nombre d'éléments de l'adaptateur est 0.

En implémentant cela, j'ai d'abord essayé d'appeler la logique de vue vide juste après avoir modifié la structure sous-jacente (ArrayList dans mon cas), par exemple:

btnRemoveFirst.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        devices.remove(0); // remove item from ArrayList
        adapter.notifyItemRemoved(0); // notify RecyclerView's adapter
        updateEmptyView();
    }
});

Il fait la chose, mais a un inconvénient: lorsque le dernier élément est supprimé, la vue vide apparaît avant l'animation de suppression est terminée, immédiatement après la suppression. J'ai donc décidé d'attendre la fin de l'animation, puis de mettre à jour l'interface utilisateur.

À ma grande surprise, je n'ai pas pu trouver un bon moyen d'écouter des événements d'animation dans RecyclerView. La première chose qui me vient à l'esprit est d'utiliser la méthode isRunning comme ceci:

btnRemoveFirst.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        devices.remove(0); // remove item from ArrayList
        adapter.notifyItemRemoved(0); // notify RecyclerView's adapter
        recyclerView.getItemAnimator().isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
            @Override
            public void onAnimationsFinished() {
                updateEmptyView();
            }
        });
    }
});

Malheureusement, dans ce cas, le rappel s'exécute immédiatement, car à ce moment, le ItemAnimator interne n'est toujours pas à l'état "en cours d'exécution". Ainsi, les questions sont: comment utiliser correctement la méthode ItemAnimator.isRunning () et existe-t-il un meilleur moyen d'obtenir le résultat souhaité, c'est-à-dire afficher une vue vide après l'animation de suppression de l'élément unique est terminé?

30
Roman Petrenko

Actuellement, la seule façon de travailler que j'ai trouvée pour résoudre ce problème est d'étendre ItemAnimator et de le passer à RecyclerView comme ceci:

recyclerView.setItemAnimator(new DefaultItemAnimator() {
    @Override
    public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
        updateEmptyView();
    }
});

Mais cette technique n'est pas universelle, car je dois m'étendre de l'implémentation concrète de ItemAnimator utilisée par RecyclerView. En cas de CoolItemAnimator interne privé à l'intérieur de CoolRecyclerView, ma méthode ne fonctionnera pas du tout.


PS: Mon collègue a suggéré de mettre ItemAnimator à l'intérieur du décorateur de la manière suivante:

recyclerView.setItemAnimator(new ListenableItemAnimator(recyclerView.getItemAnimator()));

Ce serait bien, même si cela semble exagéré pour une tâche aussi triviale, mais la création du décorateur dans ce cas n'est pas possible de toute façon, car ItemAnimator a une méthode setListener() qui est protégée par paquet, donc je ne peut évidemment pas envelopper, ainsi que plusieurs méthodes finales.

18
Roman Petrenko

J'ai un cas un peu plus générique où je veux détecter lorsque la vue du recycleur a fini de s'animer complètement lorsqu'un ou plusieurs éléments sont supprimés ou ajoutés en même temps.

J'ai essayé la réponse de Roman Petrenko, mais cela ne fonctionne pas dans ce cas. Le problème est que onAnimationFinished est appelé pour chaque entrée dans la vue du recycleur. La plupart des entrées n'ayant pas changé, onAnimationFinished est appelé plus ou moins instantané. Mais pour les ajouts et les suppressions, l'animation prend un peu de temps, donc elle s'appelle plus tard.

Cela conduit à au moins deux problèmes. Supposons que vous ayez une méthode appelée doStuff() que vous souhaitez exécuter lorsque l'animation est terminée.

  1. Si vous appelez simplement doStuff() dans onAnimationFinished, vous l'appellerez une fois pour chaque élément de la vue du recycleur, ce qui pourrait ne pas être ce que vous voulez faire.

  2. Si vous appelez simplement doStuff() la première fois que onAnimationFinished est appelé, vous pouvez l'appeler bien avant la fin de la dernière animation.

Si vous savez combien d'éléments doivent être animés, vous pouvez vous assurer d'appeler doStuff() lorsque la dernière animation se termine. Mais je n'ai trouvé aucun moyen de savoir combien d'animations restantes sont en attente.

Ma solution à ce problème consiste à laisser la vue du recycleur commencer à animer en utilisant new Handler().post() , puis à configurer un écouteur avec isRunning() = qui est appelé lorsque l'animation est prête. Après cela, il répète le processus jusqu'à ce que toutes les vues aient été animées.

void changeAdapterData() {
    // ...
    // Changes are made to the data held by the adapter
    recyclerView.getAdapter().notifyDataSetChanged();

    // The recycler view have not started animating yet, so post a message to the
    // message queue that will be run after the recycler view have started animating.
    new Handler().post(waitForAnimationsToFinishRunnable);
}

private Runnable waitForAnimationsToFinishRunnable = new Runnable() {
    @Override
    public void run() {
        waitForAnimationsToFinish();
    }
};

// When the data in the recycler view is changed all views are animated. If the
// recycler view is animating, this method sets up a listener that is called when the
// current animation finishes. The listener will call this method again once the
// animation is done.
private void waitForAnimationsToFinish() {
    if (recyclerView.isAnimating()) {
        // The recycler view is still animating, try again when the animation has finished.
        recyclerView.getItemAnimator().isRunning(animationFinishedListener);
        return;
    }

    // The recycler view have animated all it's views
    onRecyclerViewAnimationsFinished();
}

// Listener that is called whenever the recycler view have finished animating one view.
private RecyclerView.ItemAnimator.ItemAnimatorFinishedListener animationFinishedListener =
        new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
    @Override
    public void onAnimationsFinished() {
        // The current animation have finished and there is currently no animation running,
        // but there might still be more items that will be animated after this method returns.
        // Post a message to the message queue for checking if there are any more
        // animations running.
        new Handler().post(waitForAnimationsToFinishRunnable);
    }
};

// The recycler view is done animating, it's now time to doStuff().
private void onRecyclerViewAnimationsFinished() {
    doStuff();
}
12
nibarius

Voici une petite méthode d'extension Kotlin qui s'appuie sur le réponse de nibarius.

fun RecyclerView.executeAfterAllAnimationsAreFinished(
    callback: (RecyclerView) -> Unit
) = post(
    object : Runnable {
        override fun run() {
            if (isAnimating) {
                // itemAnimator is guaranteed to be non-null after isAnimating() returned true
                itemAnimator!!.isRunning {
                    post(this)
                }
            } else {
                callback(this@executeAfterAllAnimationsAreFinished)
            }
        }
    }
)
2
SqueezyMo

Ce qui a fonctionné pour moi est le suivant:

  • détecter qu'un support de vue a été retiré
  • dans ce cas, enregistrez un écouteur pour être averti lorsque dispatchAnimationsFinished() est appelé
  • lorsque toutes les animations sont terminées, appelez un auditeur pour effectuer la tâche (updateEmptyView())

public class CompareItemAnimator extends DefaultItemAnimator implements RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {

private OnItemAnimatorListener mOnItemAnimatorListener;

public interface OnItemAnimatorListener {
    void onAnimationsFinishedOnItemRemoved();
}

@Override
public void onAnimationsFinished() {
    if (mOnItemAnimatorListener != null) {
        mOnItemAnimatorListener.onAnimationsFinishedOnItemRemoved();
    }
}

public void setOnItemAnimatorListener(OnItemAnimatorListener onItemAnimatorListener) {
    mOnItemAnimatorListener = onItemAnimatorListener;
}

@Override
public void onRemoveFinished(RecyclerView.ViewHolder viewHolder) {
    isRunning(this);
}}
2
Dragoş A.

Pour développer la réponse de Roman Petrenko, je n'ai pas non plus de réponse vraiment universelle, mais j'ai trouvé que le modèle Factory était un moyen utile de nettoyer au moins une partie de la cruauté qui est ce problème.

public class ItemAnimatorFactory {

    public interface OnAnimationEndedCallback{
        void onAnimationEnded();
    }
    public static RecyclerView.ItemAnimator getAnimationCallbackItemAnimator(OnAnimationEndedCallback callback){
        return new FadeInAnimator() {
            @Override
            public void onAnimationFinished(RecyclerView.ViewHolder viewHolder) {
                callback.onAnimationEnded();
                super.onAnimationEnded(viewHolder);
            }
        };
    }
}

Dans mon cas, j'utilise une bibliothèque qui fournit un FadeInAnimator que j'utilisais déjà. J'utilise la solution de Roman dans la méthode d'usine pour me connecter à l'événement onAnimationEnded, puis transmettre l'événement en remontant la chaîne.

Ensuite, lorsque je configure ma vue de recyclage, je spécifie le rappel comme étant ma méthode de mise à jour de la vue en fonction du nombre d'éléments de vue de recyclage:

mRecyclerView.setItemAnimator(ItemAnimatorFactory.getAnimationCallbackItemAnimator(this::checkSize));

Encore une fois, il n'est pas totalement universel dans tous les animateurs ItemAnimators, mais il "consolide au moins la cruauté", donc si vous avez plusieurs animateurs d'articles différents, vous pouvez simplement implémenter une méthode d'usine ici suivant le même modèle, puis votre configuration de recyclage. indique simplement quel ItemAnimator vous voulez.

0
Josh Kitchens

Dans ma situation, je voulais supprimer un tas d'éléments (et en ajouter de nouveaux) après la fin d'une animation. Mais l'événement isAnimating est déclenché pour chaque titulaire, donc la fonction de @ SqueezyMo ne ferait pas l'affaire pour exécuter mon action simultanément sur tous les éléments. Ainsi, j'ai implémenté un Listener dans mon Animator avec une méthode pour vérifier si la dernière animation a été effectuée.

Animateur

class ClashAnimator(private val listener: Listener) : DefaultItemAnimator() {

    internal var winAnimationsMap: MutableMap<RecyclerView.ViewHolder, AnimatorSet> =
        HashMap()
    internal var exitAnimationsMap: MutableMap<RecyclerView.ViewHolder, AnimatorSet> =
        HashMap()

    private var lastAddAnimatedItem = -2

    override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean {
        return true
    }

    interface Listener {
        fun dispatchRemoveAnimationEnded()
    }

    private fun dispatchChangeFinishedIfAllAnimationsEnded(holder: ClashAdapter.ViewHolder) {
        if (winAnimationsMap.containsKey(holder) || exitAnimationsMap.containsKey(holder)) {
            return
        }
        listener.dispatchRemoveAnimationEnded() //here I dispatch the Event to my Fragment

        dispatchAnimationFinished(holder)
    }

    ...
}

Fragment

class HomeFragment : androidx.fragment.app.Fragment(), Injectable, ClashAdapter.Listener, ClashAnimator.Listener {
    ...
    override fun dispatchRemoveAnimationEnded() {
        mAdapter.removeClash() //will execute animateRemove
        mAdapter.addPhotos(photos.subList(0,2), picDimens[1]) //will execute animateAdd
    }
}
0
mrj