Détecter la finition de l'animation dans le RecyclerView D'Android

RecyclerView, contrairement à ListView, n'a pas de façon simple de définir une vue vide, donc on doit la gérer manuellement, ce qui rend la vue vide visible dans le cas où le nombre d'items de l'adaptateur est 0.

en implémentant ceci, j'ai d'abord essayé d'appeler la logique de la 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 ne la chose, mais a un inconvénient: lorsque le dernier élément est supprimé, vide, vue apparaît avant animation de l'enlèvement est terminé, immédiatement après l'enlèvement. J'ai donc décidé d'attendre la fin de l'animation et de mettre à jour L'interface utilisateur.

a ma grande surprise, je n'ai pas pu trouver un bon moyen d'écouter des animations dans RecyclerView. La première chose qui vient à l'esprit est d'utiliser isRunning méthode 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, le rappel dans ce cas s'exécute immédiatement, parce qu'à ce moment-là intérieur ItemAnimator n'est toujours pas en état de "fuite". Ainsi, les questions sont:comment pour utiliser correctement ItemAnimator.isRunning() la méthode et est-il un meilleur moyen d'atteindre le résultat souhaité, c'est à dire afficher la vue vide après le retrait de l'animation de l'unique élément est fini?

26
demandé sur Roman Petrenko 2015-11-14 19:27:52

4 réponses

actuellement, la seule façon de résoudre ce problème est d'étendre ItemAnimator et passer à RecyclerView comme ceci:

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

mais cette technique n'est pas universelle, parce que je dois m'étendre du béton ItemAnimator implémentation utilisée par RecyclerView. Dans le cas de l'intérieur privé CoolItemAnimator à l'intérieur CoolRecyclerView, ma méthode ne fonctionnera pas du tout.


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

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

ce serait bien, bien que cela semble exagéré pour une tâche aussi insignifiante, mais créer le décorateur dans ce cas n'est pas possible de toute façon, parce que ItemAnimator a une méthode setListener() qui est protégé par paquet donc je ne peux évidemment pas l'envelopper, ainsi que plusieurs méthodes finales.

17
répondu Roman Petrenko 2018-07-16 14:26:48

j'ai un peu plus générique dans le cas où je veux détecter quand le recycleur, ont fini d'animer complètement lorsqu'un ou plusieurs éléments sont enlevés ou ajoutés en même temps.

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

il en résulte au moins deux problèmes. Supposons que vous avez une méthode appelée doStuff() que vous souhaitez exécuter lorsque l'animation est réalisée.

  1. Si vous appelez simplement doStuff()onAnimationFinished Vous l'appellerez une fois pour chaque article dans la vue recycler qui pourrait ne pas être ce que vous voulez faire.

  2. Si vous appelez doStuff()onAnimationFinished est appelé vous pouvez être appelé cela bien avant que la dernière animation ne soit terminée.

si vous pouviez savoir combien d'éléments il y a à animer, vous pourriez appeler doStuff() lorsque la dernière animation se termine. Mais je n'ai trouvé aucun moyen de savoir combien d'animations il reste.

ma solution à ce problème est de laisser le recycleur voir d'abord l'animation en utilisant new Handler().post(), puis d'un auditeur de isRunning() c'est appelé quand 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();
}
6
répondu nibarius 2017-10-19 12:07:06

Ce qui a fonctionné pour moi est la suivante:

  • détecter qu'une vue titulaire a été supprimé
  • dans ce cas, enregistrez un auditeur à notifier lorsque dispatchAnimationsFinished() s'appelle
  • 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);
}}
1
répondu Dragoş A. 2016-07-04 15:12:37
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 accrocher dans l'événement onAnimationEnded, puis passer l'événement back up the chain.

Puis, quand je suis à la configuration de ma recyclerview, je spécifie que le callback sera ma méthode pour mettre à jour la vue basée sur le nombre d'articles recyclerview:

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

encore une fois, ce n'est pas totalement universel à travers tous les ItemAnimators, mais au moins il "consolide le crift", donc si vous avez plusieurs animateurs d'articles différents, vous pouvez juste mettre en œuvre une méthode d'usine ici en suivant le même modèle, et puis votre configuration recyclerview est juste en spécifiant quel ItemAnimator vous voulez.

0
répondu Josh Kitchens 2016-05-15 22:10:33