28

I want to do an animation with several image-files, and for this the AnimationDrawable works very well. However, I need to know when the animation starts and when it ends (i.e add a listener like the Animation.AnimationListener). After having searched for answers, I'm having a bad feeling the AnimationDrawable does not support listeners..

Does anyone know how to create a frame-by-frame image animation with a listener on Android?

Jiri Tousek
  • 12,211
  • 5
  • 29
  • 43
LostDroid
  • 281
  • 1
  • 3
  • 5

14 Answers14

47

After doing some reading, I came up with this solution. I'm still surprised there isn't a listener as part of the AnimationDrawable object, but I didn't want to pass callbacks back and forward so instead I created an abstract class which raises an onAnimationFinish() method. I hope this helps someone.

The custom animation drawable class:

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

To use this class:

    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();
        }
    });
reixa
  • 6,903
  • 6
  • 49
  • 68
Ricky
  • 7,785
  • 2
  • 34
  • 46
26

I needed to know when my one-shot AnimationDrawable completes, without having to subclass AnimationDrawable since I must set the animation-list in XML. I wrote this class and tested it on Gingerbread and ICS. It can easily be extended to give a callback on each frame.

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

Here is how to use it.

AnimationDrawable countdownAnimation = (AnimationDrawable) mStartButton.getBackground();
countdownAnimation.setCallback(new AnimationDrawableCallback(countdownAnimation, mStartButton) {
    @Override
    public void onAnimationComplete() {
        // TODO Do something.
    }
});
countdownAnimation.start();
benhylau
  • 430
  • 5
  • 9
  • Thanks @benhylau! I thought this was a very elegant solution piggybacking off of existing interface. Just wanted to add that this seems to skip the last frame, so just remember to put in a dummy frame at the end. – weiy Mar 31 '15 at 18:05
  • 3
    Just to clarify, that only notifies on the last frame when the animation completes. If you want to listen in as the animation progresses through each frame, use this updated version: https://gist.github.com/benhylau/a7d21c8806df9c73da7d – benhylau Apr 19 '15 at 07:26
  • I know I'm late, but your method is really great and elegant. Thanks for sharing it! I still don't get why AnimationDrawable doesn't have support for listeners after all this time... – Talendar Aug 28 '18 at 04:33
23

Animation end can be easily tracked by overriding selectDrawable method in AnimationDrawable class. Complete code is the following:

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;
    }
}
Ruslan Yanchyshyn
  • 2,774
  • 1
  • 24
  • 22
  • 1
    How can I change the background of a View (current class is AnimationDrawable) to AnimationDrawable2 ? – Jonny Jan 04 '13 at 10:39
  • 1
    This seems like a better, non-timing dependant answer to me. – Andrew Mackenzie Dec 31 '13 at 00:13
  • 2
    definitely the proper answer, checking the duration goes off over time, or on weaker devices – tibbi Nov 10 '15 at 13:04
  • 1
    How did you create and set the `AnimationDrawable2` on the view though? – AmeyaB Aug 19 '16 at 00:44
  • This is a good approach. But it has a subtle bug. When idx is getNumberOfFrames() - 1, it is before the duration of the last frame started. Let's say there are 3 frames, and each duration is 200 milliseconds. This callback will be called when 400 milliseconds passed, not 600. – 김준호 Nov 03 '16 at 06:04
  • 1
    @AmeyaB You need to change your `AnimationDrawable animation = new AnimationDrawable();` line to `AnimationDrawable2 animation = new AnimationDrawable2();`. Then call `animation.setAnimationFinishListener(this);` - where `this` is your Activity or Fragment, etc. Then, you just need your Activity/Fragment class to `implements AnimationDrawable2.IAnimationFinishListener`. Finally, add the `@Override public void onAnimationFinished() { ...}` method, populate it with you code, and you should be sorted. :-) – ban-geoengineering Aug 01 '17 at 13:30
  • 1
    @AmeyaB My answer is based on Ruslans and also shows how to implement it: https://stackoverflow.com/a/45439992/1617737 – ban-geoengineering Aug 01 '17 at 14:00
17

I used a recursive function that checks to see if the current frame is the last frame every timeBetweenChecks milliseconds.

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);
}
cdbitesky
  • 1,390
  • 1
  • 13
  • 30
hitch45
  • 472
  • 4
  • 12
4

A timer is a bad choice for this because you will get stuck trying to execute in a non UI thread like HowsItStack said. For simple tasks you can just use a handler to call a method at a certain interval. Like this:

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 assures this only executes once.

Kingpin
  • 292
  • 1
  • 9
4

This is so simple when it come to using Kotlin, AnimationDrawable has two functions we could use to calculate the animation duration, then we could add a runnable with delay to create an Animation listener. here is a simple Kotlin extension.

fun AnimationDrawable.onAnimationFinished(block: () -> Unit) {
    var duration: Long = 0
    for (i in 0..numberOfFrames) {
        duration += getDuration(i)
    }
    Handler().postDelayed({
        block()
    }, duration + 200)
}
Mohamed Ibrahim
  • 3,714
  • 2
  • 22
  • 44
3

if you want to impliment your animation in adapter - should use next public class 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();
}

}

and implementation in RecycleView Adapter

@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();
        }
    });
}
kazimad
  • 917
  • 9
  • 8
2

I also like Ruslan's answer, but I had to make a couple of changes in order to get it to do what I required.

In my code, I have got rid of Ruslan's finished flag, and I have also utilised the boolean returned by super.selectDrawable().

Here's my code:

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;

    }

}

And here is an example of how to implement it...

public class MyFragment extends Fragment implements AnimationDrawableWithCallback.IAnimationFinishListener {

    @Override
    public void onAnimationChanged(int index, boolean finished) {

        // Do whatever you need here

    }

}

If you only want to know when the first cycle of animation has completed, then you can set a boolean flag in your fragment/activity.

ban-geoengineering
  • 18,324
  • 27
  • 171
  • 253
1

I guess your Code does not work, because you try to modify a View from a non-UI-Thread. Try to call runOnUiThread(Runnable) from your Activity. I used it to fade out a menu after an animation for this menu finishes. This code works for me:

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());
lalalei
  • 323
  • 1
  • 3
  • 6
1

You can use registerAnimationCallback to check your animation start and end.

Here's the snippet code:

// ImageExt.kt
fun ImageView.startAnim(block: () -> Unit) {
    (drawable as Animatable).apply {
        registerAnimationCallback(
                drawable,
                object : Animatable2Compat.AnimationCallback() {
                    override fun onAnimationStart(drawable: Drawable?) {
                        block.invoke()
                        isClickable = false
                        isEnabled = false
                    }

                    override fun onAnimationEnd(drawable: Drawable?) {
                        isClickable = true
                        isEnabled = true
                    }
        })
    }.run { start() }
}
// Fragment.kt
imageView.startAnim {
    // do something after the animation ends here
}

The purpose of the ImageExt was to disable after animation start (on progress) to prevent user spamming the animation and resulting in the broken / wrong vector shown.

With frame-by-frame, you might want to trigger another ImageView like this.

// Animation.kt
iv1.startAnim {
    iv2.startAnim {
        // iv3, etc
    }
}

But the above solutions looks ugly. If anyone has a better approach, please comment below, or edit this answer directly.

mochadwi
  • 1,190
  • 9
  • 32
  • 87
0

I had the same problem when I had to implement a button click after animation stopped. I checked the current frame and the lastframe of animation drawable to know when an animation is stopped. Note that it is not a listener but just a way to know it animation has stopped.

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();
            }
Illegal Argument
  • 10,090
  • 2
  • 44
  • 61
0

I don't know about all these other solutions, but this is the one that comes closest to simply adding a listener to the AnimationDrawable class.

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;
        }
    }
Anarchofascist
  • 474
  • 5
  • 15
0

I prefer not to go for timing solution, as it seems to me isn't reliable enough.

I love Ruslan Yanchyshyn's solution : https://stackoverflow.com/a/12314579/72437

However, if you notice the code carefully, we will receive animation end callback, during the animation start of last frame, not the animation end.

I propose another solution, by using a dummy drawable in animation drawable.

animation_list.xml

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    <item android:drawable="@drawable/card_selected_material_light" android:duration="@android:integer/config_mediumAnimTime" />
    <item android:drawable="@drawable/card_material_light" android:duration="@android:integer/config_mediumAnimTime" />
    <item android:drawable="@drawable/dummy" android:duration="@android:integer/config_mediumAnimTime" />
</animation-list>

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

This is how we can make use of the above class.

    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();
Community
  • 1
  • 1
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
-4

i had used following method and it is really works.

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

i hope this will solve your problem.

user609239
  • 3,356
  • 5
  • 25
  • 26