Android AnimationDrawable y saber cuándo finaliza la animación

Quiero hacer una animación con varios archivos de imagen, y para esto el AnimationDrawable funciona muy bien. Sin embargo, necesito saber cuándo comienza la animación y cuándo termina (es decir, agregar un oyente como Animation.AnimationListener). Después de haber buscado las respuestas, tengo un mal presentimiento de que AnimationDrawable no admite oyentes.

¿Alguien sabe cómo crear una animación de imagen cuadro por cuadro con un oyente en Android?

Después de leer un poco, se me ocurrió esta solución. Todavía estoy sorprendido de que no haya un oyente como parte del objeto AnimationDrawable , pero no quería pasar de nuevo las retrollamadas, así que creé una clase abstracta que genera un método onAnimationFinish() . Espero que esto ayude a alguien.

La clase dibujable de animación personalizada:

 public abstract class CustomAnimationDrawableNew extends AnimationDrawable { /** Handles the animation callback. */ Handler mAnimationHandler; public CustomAnimationDrawableNew(AnimationDrawable aniDrawable) { /* Add each frame to our animation drawable */ for (int i = 0; i < aniDrawable.getNumberOfFrames(); i++) { this.addFrame(aniDrawable.getFrame(i), aniDrawable.getDuration(i)); } } @Override public void start() { super.start(); /* * Call super.start() to call the base class start animation method. * Then add a handler to call onAnimationFinish() when the total * duration for the animation has passed */ mAnimationHandler = new Handler(); mAnimationHandler.post(new Runnable() { @Override public void run() { onAnimationStart(); } }; mAnimationHandler.postDelayed(new Runnable() { @Override public void run() { onAnimationFinish(); } }, getTotalDuration()); } /** * Gets the total duration of all frames. * * @return The total duration. */ public int getTotalDuration() { int iDuration = 0; for (int i = 0; i < this.getNumberOfFrames(); i++) { iDuration += this.getDuration(i); } return iDuration; } /** * Called when the animation finishes. */ public abstract void onAnimationFinish(); /** * Called when the animation starts. */ public abstract void onAnimationStart(); } 

Para usar esta clase:

  ImageView iv = (ImageView) findViewById(R.id.iv_testing_testani); iv.setOnClickListener(new OnClickListener() { public void onClick(final View v) { // Pass our animation drawable to our custom drawable class CustomAnimationDrawableNew cad = new CustomAnimationDrawableNew( (AnimationDrawable) getResources().getDrawable( R.drawable.anim_test)) { @Override void onAnimationStart() { // Animation has started... } @Override void onAnimationFinish() { // Animation has finished... } }; // Set the views drawable to our custom drawable v.setBackgroundDrawable(cad); // Start the animation cad.start(); } }); 

Necesitaba saber cuándo finaliza mi One-shot AnimationDrawable, sin tener que crear una subclase de AnimationDrawable ya que debo establecer la lista de animación en XML. Escribí esta clase y la probé en Gingerbread e ICS. Se puede extender fácilmente para dar una callback en cada cuadro.

 /** * Provides a callback when a non-looping {@link AnimationDrawable} completes its animation sequence. More precisely, * {@link #onAnimationComplete()} is triggered when {@link View#invalidateDrawable(Drawable)} has been called on the * last frame. * * @author Benedict Lau */ public abstract class AnimationDrawableCallback implements Callback { /** * The last frame of {@link Drawable} in the {@link AnimationDrawable}. */ private Drawable mLastFrame; /** * The client's {@link Callback} implementation. All calls are proxied to this wrapped {@link Callback} * implementation after intercepting the events we need. */ private Callback mWrappedCallback; /** * Flag to ensure that {@link #onAnimationComplete()} is called only once, since * {@link #invalidateDrawable(Drawable)} may be called multiple times. */ private boolean mIsCallbackTriggered = false; /** * * @param animationDrawable * the {@link AnimationDrawable}. * @param callback * the client's {@link Callback} implementation. This is usually the {@link View} the has the * {@link AnimationDrawable} as background. */ public AnimationDrawableCallback(AnimationDrawable animationDrawable, Callback callback) { mLastFrame = animationDrawable.getFrame(animationDrawable.getNumberOfFrames() - 1); mWrappedCallback = callback; } @Override public void invalidateDrawable(Drawable who) { if (mWrappedCallback != null) { mWrappedCallback.invalidateDrawable(who); } if (!mIsCallbackTriggered && mLastFrame != null && mLastFrame.equals(who.getCurrent())) { mIsCallbackTriggered = true; onAnimationComplete(); } } @Override public void scheduleDrawable(Drawable who, Runnable what, long when) { if (mWrappedCallback != null) { mWrappedCallback.scheduleDrawable(who, what, when); } } @Override public void unscheduleDrawable(Drawable who, Runnable what) { if (mWrappedCallback != null) { mWrappedCallback.unscheduleDrawable(who, what); } } // // Public methods. // /** * Callback triggered when {@link View#invalidateDrawable(Drawable)} has been called on the last frame, which marks * the end of a non-looping animation sequence. */ public abstract void onAnimationComplete(); } 

Aquí es cómo usarlo.

 AnimationDrawable countdownAnimation = (AnimationDrawable) mStartButton.getBackground(); countdownAnimation.setCallback(new AnimationDrawableCallback(countdownAnimation, mStartButton) { @Override public void onAnimationComplete() { // TODO Do something. } }); countdownAnimation.start(); 

El final de la animación puede rastrearse fácilmente anulando el método selectDrawable en la clase AnimationDrawable. El código completo es el siguiente:

 public class AnimationDrawable2 extends AnimationDrawable { public interface IAnimationFinishListener { void onAnimationFinished(); } private boolean finished = false; private IAnimationFinishListener animationFinishListener; public IAnimationFinishListener getAnimationFinishListener() { return animationFinishListener; } public void setAnimationFinishListener(IAnimationFinishListener animationFinishListener) { this.animationFinishListener = animationFinishListener; } @Override public boolean selectDrawable(int idx) { boolean ret = super.selectDrawable(idx); if ((idx != 0) && (idx == getNumberOfFrames() - 1)) { if (!finished) { finished = true; if (animationFinishListener != null) animationFinishListener.onAnimationFinished(); } } return ret; } } 

Utilicé una función recursiva que comprueba si el marco actual es el último marco cada vez que entre los milisegundos.

 private void checkIfAnimationDone(AnimationDrawable anim){ final AnimationDrawable a = anim; int timeBetweenChecks = 300; Handler h = new Handler(); h.postDelayed(new Runnable(){ public void run(){ if (a.getCurrent() != a.getFrame(a.getNumberOfFrames() - 1)){ checkIfAnimationDone(a); } else{ Toast.makeText(getApplicationContext(), "ANIMATION DONE!", Toast.LENGTH_SHORT).show(); } } }, timeBetweenChecks); } 

Un temporizador es una mala elección para esto porque te quedarás atrapado tratando de ejecutar en un subproceso que no sea UI como dijo HowsItStack. Para tareas simples, puede usar un controlador para llamar a un método en un intervalo determinado. Me gusta esto:

 handler.postDelayed(runnable, duration of your animation); //Put this where you start your animation private Handler handler = new Handler(); private Runnable runnable = new Runnable() { public void run() { handler.removeCallbacks(runnable) DoSomethingWhenAnimationEnds(); } }; 

removeCallbacks asegura que esto solo se ejecuta una vez.

si desea implementar su animación en el adaptador, debería utilizar la siguiente clase pública CustomAnimationDrawable extends AnimationDrawable {

 /** * Handles the animation callback. */ Handler mAnimationHandler; private OnAnimationFinish onAnimationFinish; public void setAnimationDrawable(AnimationDrawable aniDrawable) { for (int i = 0; i < aniDrawable.getNumberOfFrames(); i++) { this.addFrame(aniDrawable.getFrame(i), aniDrawable.getDuration(i)); } } public void setOnFinishListener(OnAnimationFinish onAnimationFinishListener) { onAnimationFinish = onAnimationFinishListener; } @Override public void stop() { super.stop(); } @Override public void start() { super.start(); mAnimationHandler = new Handler(); mAnimationHandler.postDelayed(new Runnable() { public void run() { if (onAnimationFinish != null) onAnimationFinish.onFinish(); } }, getTotalDuration()); } /** * Gets the total duration of all frames. * * @return The total duration. */ public int getTotalDuration() { int iDuration = 0; for (int i = 0; i < this.getNumberOfFrames(); i++) { iDuration += this.getDuration(i); } return iDuration; } /** * Called when the animation finishes. */ public interface OnAnimationFinish { void onFinish(); } 

}

e implementación en RecycleView Adaptor

 @Override public void onBindViewHolder(PlayGridAdapter.ViewHolder holder, int position) { final Button mButton = holder.button; mButton.setBackgroundResource(R.drawable.animation_overturn); final CustomAnimationDrawable mOverturnAnimation = new CustomAnimationDrawable(); mOverturnAnimation.setAnimationDrawable((AnimationDrawable) mContext.getResources().getDrawable(R.drawable.animation_overturn)); mOverturnAnimation.setOnFinishListener(new CustomAnimationDrawable.OnAnimationFinish() { @Override public void onFinish() { // your perform } }); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { mOverturnAnimation.start(); } }); } 

Supongo que su código no funciona, porque intenta modificar una vista desde un subproceso que no sea UI. Intenta llamar a runOnUiThread (ejecutable) desde tu actividad. Lo usé para fundir un menú después de que termina una animación para este menú. Este código funciona para mí:

 Animation ani = AnimationUtils.loadAnimation(YourActivityNameHere.this, R.anim.fadeout_animation); menuView.startAnimation(ani); // Use Timer to set visibility to GONE after the animation finishes. TimerTask timerTask = new TimerTask(){ @Override public void run() { YourActivityNameHere.this.runOnUiThread(new Runnable(){ @Override public void run() { menuView.setVisibility(View.GONE); } });}}; timer.schedule(timerTask, ani.getDuration()); 

Tuve el mismo problema cuando tuve que implementar un clic de botón después de detener la animación. Revisé el cuadro actual y el último cuadro de animación dibujable para saber cuándo se detuvo una animación. Tenga en cuenta que no es un oyente, sino solo una forma de saber que la animación se ha detenido.

 if (spinAnimation.getCurrent().equals( spinAnimation.getFrame(spinAnimation .getNumberOfFrames() - 1))) { Toast.makeText(MainActivity.this, "finished", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "Not finished", Toast.LENGTH_SHORT).show(); } 

No conozco todas estas otras soluciones, pero esta es la que se acerca más a simplemente agregar un oyente a la clase AnimationDrawable.

 class AnimationDrawableListenable extends AnimationDrawable{ static interface AnimationDrawableListener { void selectIndex(int idx, boolean b); } public AnimationDrawableListener animationDrawableListener; public boolean selectDrawable(int idx) { boolean selectDrawable = super.selectDrawable(idx); animationDrawableListener.selectIndex(idx,selectDrawable); return selectDrawable; } public void setAnimationDrawableListener(AnimationDrawableListener animationDrawableListener) { this.animationDrawableListener = animationDrawableListener; } } 

Prefiero no buscar la solución de tiempo, ya que me parece que no es lo suficientemente confiable.

Me encanta la solución de Ruslan Yanchyshyn: https://stackoverflow.com/a/12314579/72437

Sin embargo, si observa el código cuidadosamente, recibiremos la callback de la animación, durante el inicio de la animación del último fotogtwig, no el final de la animación.

Propongo otra solución, mediante el uso de un maniquí dibujable en animación dibujable .

animation_list.xml

      

AnimationDrawableWithCallback.java

 import android.graphics.drawable.AnimationDrawable; /** * Created by yccheok on 24/1/2016. */ public class AnimationDrawableWithCallback extends AnimationDrawable { public AnimationDrawableWithCallback(AnimationDrawable aniDrawable) { /* Add each frame to our animation drawable */ for (int i = 0; i < aniDrawable.getNumberOfFrames(); i++) { this.addFrame(aniDrawable.getFrame(i), aniDrawable.getDuration(i)); } } public interface IAnimationFinishListener { void onAnimationFinished(); } private boolean finished = false; private IAnimationFinishListener animationFinishListener; public void setAnimationFinishListener(IAnimationFinishListener animationFinishListener) { this.animationFinishListener = animationFinishListener; } @Override public boolean selectDrawable(int idx) { if (idx >= (this.getNumberOfFrames()-1)) { if (!finished) { finished = true; if (animationFinishListener != null) animationFinishListener.onAnimationFinished(); } return false; } boolean ret = super.selectDrawable(idx); return ret; } } 

Así es como podemos hacer uso de la clase anterior.

  AnimationDrawableWithCallback animationDrawable2 = new AnimationDrawableWithCallback(rowLayoutAnimatorList); animationDrawable2.setAnimationFinishListener(new AnimationDrawableWithCallback.IAnimationFinishListener() { @Override public void onAnimationFinished() { ... } }); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { view.setBackground(animationDrawable2); } else { view.setBackgroundDrawable(animationDrawable2); } // https://stackoverflow.com/questions/14297003/animating-all-items-in-animation-list animationDrawable2.setEnterFadeDuration(this.configMediumAnimTime); animationDrawable2.setExitFadeDuration(this.configMediumAnimTime); animationDrawable2.start(); 

También me gusta la respuesta de Ruslan, pero tuve que hacer un par de cambios para que hiciera lo que necesitaba.

En mi código, me he deshecho de la bandera de Ruslan, y también he utilizado la booleana devuelta por super.selectDrawable() .

Aquí está mi código:

 class AnimationDrawableWithCallback extends AnimationDrawable { interface IAnimationFinishListener { void onAnimationChanged(int index, boolean finished); } private IAnimationFinishListener animationFinishListener; public IAnimationFinishListener getAnimationFinishListener() { return animationFinishListener; } void setAnimationFinishListener(IAnimationFinishListener animationFinishListener) { this.animationFinishListener = animationFinishListener; } @Override public boolean selectDrawable(int index) { boolean drawableChanged = super.selectDrawable(index); if (drawableChanged && animationFinishListener != null) { boolean animationFinished = (index == getNumberOfFrames() - 1); animationFinishListener.onAnimationChanged(index, animationFinished); } return drawableChanged; } } 

Y aquí hay un ejemplo de cómo implementarlo …

 public class MyFragment extends Fragment implements AnimationDrawableWithCallback.IAnimationFinishListener { @Override public void onAnimationChanged(int index, boolean finished) { // Do whatever you need here } } 

Si solo quiere saber cuándo se completó el primer ciclo de animación, puede establecer un indicador booleano en su fragmento / actividad.

había usado el siguiente método y realmente funciona.

 Animation anim1 = AnimationUtils.loadAnimation( this, R.anim.hori); Animation anim2 = AnimationUtils.loadAnimation( this, R.anim.hori2); ImageSwitcher isw=new ImageSwitcher(this); isw.setInAnimation(anim1); isw.setOutAnimation(anim2); 

espero que esto resuelva tu problema.