wasabeef recyclerview recycler itemanimator item best animators animations android animation android-recyclerview

itemanimator - Detecta el acabado de la animación en RecyclerView de Android



recyclerview set item animator (4)

The RecyclerView , a diferencia de ListView , no tiene una manera simple de configurar una vista vacía, por lo que uno tiene que administrarlo manualmente, haciendo visible la vista vacía en caso de que el recuento de elementos del adaptador sea 0.

Al implementar esto, primero traté de llamar a la lógica de vista vacía justo después de modificar la estructura subyacente ( ArrayList en mi caso), por ejemplo:

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(); } });

Lo hace, pero tiene un inconveniente: cuando se elimina el último elemento, aparece la vista vacía antes de que finalice la animación de eliminación, inmediatamente después de la eliminación. Así que decidí esperar hasta el final de la animación y luego actualizar la interfaz de usuario.

Para mi sorpresa, no pude encontrar una buena manera de escuchar eventos de animación en RecyclerView. Lo primero que viene a la mente es utilizar el método isRunning así:

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(); } }); } });

Desafortunadamente, la devolución de llamada en este caso se ejecuta inmediatamente, porque en ese momento el ItemAnimator interno todavía no está en el estado "en ejecución". Entonces, las preguntas son: ¿cómo usar adecuadamente el método ItemAnimator.isRunning () y hay una mejor manera de lograr el resultado deseado, es decir, mostrar la vista vacía después de la eliminación de la animación de un solo elemento terminado ?


Actualmente, la única forma de trabajo que he encontrado para resolver este problema es extender ItemAnimator y pasarlo a RecyclerView esta manera:

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

Pero esta técnica no es universal, porque tengo que extenderme desde la implementación concreta de ItemAnimator que usa RecyclerView . En el caso de CoolItemAnimator interior CoolItemAnimator dentro de CoolRecyclerView , mi método no funcionará en absoluto.

PD: Mi colega sugirió envolver ItemAnimator dentro del decorator de la siguiente manera:

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

Sería bueno, a pesar de parecer excesivo para una tarea tan trivial, pero crear el decorador en este caso no es posible de todos modos, porque ItemAnimator tiene un método setListener() que está protegido por paquetes, así que obviamente no puedo envolverlo. como varios métodos finales.


Lo que funcionó para mí es lo siguiente:

  • detectar que un titular de vista fue eliminado
  • en este caso, registre un oyente para recibir una notificación cuando se llame a dispatchAnimationsFinished()
  • cuando todas las animaciones hayan finalizado, llame a un oyente para realizar la tarea ( 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); }}


Para ampliar la respuesta de Roman Petrenko, tampoco tengo una respuesta verdaderamente universal, pero sí encontré que el patrón de Factory es una manera útil de limpiar al menos parte del problema que es este problema.

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); } }; } }

En mi caso, estoy usando una biblioteca que proporciona un FadeInAnimator que ya estaba usando. Utilizo la solución de Roman en el método de fábrica para engancharme en el evento onAnimationEnded, luego paso el evento de vuelta a la cadena.

Luego, cuando estoy configurando mi vista de reciclador, especifico que la devolución de llamada sea mi método para actualizar la vista en función del recuento de elementos de la vista de reciclador:

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

Una vez más, no es totalmente universal en todos y cada uno de los ItemAnimators, pero al menos "consolida el cruft", por lo que si tiene múltiples animadores de elementos diferentes, puede implementar un método de fábrica aquí siguiendo el mismo patrón, y luego su configuración de recyclerview es solo especificar qué ItemAnimator quieres.


Tengo un caso un poco más genérico en el que quiero detectar cuándo la vista del reciclador ha terminado de animar por completo cuando uno o más elementos se eliminan o agregan al mismo tiempo.

Probé la respuesta de Roman Petrenko, pero no funciona en este caso. El problema es que onAnimationFinished se llama para cada entrada en la vista de reciclador. La mayoría de las entradas no han cambiado, por lo que onAnimationFinished se llama más o menos instantáneo. Pero para las adiciones y las eliminaciones, la animación tarda un poco, así que se llama más tarde.

Esto lleva a al menos dos problemas. Supongamos que tiene un método llamado doStuff() que desea ejecutar cuando doStuff() la animación.

  1. Si simplemente llama a doStuff() en onAnimationFinished , lo llamará una vez por cada elemento de la vista de reciclador que podría no ser lo que desea hacer.

  2. Si solo llama a doStuff() la primera vez que se llama a onAnimationFinished puede estar llamando mucho antes de que se complete la última animación.

Si pudieras saber cuántos elementos hay que doStuff() asegúrate de llamar a doStuff() cuando termine la última animación. Pero no he encontrado ninguna forma de saber cuántas animaciones restantes hay en cola.

Mi solución a este problema es permitir que la vista de reciclador comience a animar utilizando el new Handler().post() , luego configure un oyente con isRunning() que se llama cuando la animación está lista. Después de eso, repite el proceso hasta que todas las vistas hayan sido animadas.

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(); }