Reputation: 281
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?
Upvotes: 28
Views: 50262
Reputation: 3924
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(onAnimationFinished: () -> Unit) {
val duration: Int = (0..< numberOfFrames).sumOf { getDuration(it) }
Handler(Looper.getMainLooper()).postDelayed({
onAnimationFinished()
}, duration.toLong())
}
Usage:
myAnimationDrawable.onAnimationFished {
// YOUR CODE HERE
}
Upvotes: 5
Reputation: 1317
This was a kotlin solution I came up based on various sources, including https://stackoverflow.com/a/16475418.
If I overwrote the animDrawable.callback
, then my animation was stuck on a single frame. So, I kept a copy of the original callback and called it in my new callback.
This may have memory leaks (haven't investigated that part of the solution yet): https://stackoverflow.com/a/66078037/599535
val animStateDrawable = myImageView.drawable as? StateListDrawable
val animDrawable = (animStateDrawable?.current as? AnimationDrawable)!!
val lastFrame = animDrawable.getFrame(animDrawable.numberOfFrames - 1)
val existingCallback = animDrawable.callback
animDrawable.callback = object : Drawable.Callback {
override fun invalidateDrawable(who: Drawable) {
existingCallback?.invalidateDrawable(who)
val isLastFrame = lastFrame === who.current
if (isLastFrame) {
animDrawable.callback = existingCallback
// ANIMATION HAS ENDED - DO SOMETHING HERE
}
}
override fun scheduleDrawable(p0: Drawable, p1: Runnable, p2: Long) {
existingCallback?.scheduleDrawable(p0, p1, p2)
}
override fun unscheduleDrawable(p0: Drawable, p1: Runnable) {
existingCallback?.unscheduleDrawable(p0, p1)
}
}
animDrawable.isOneShot = true
animDrawable.start()
One problem with this solution: it won't play the last frame for the full duration. Options are to add the last frame twice in your drawable animation or add a transparent color as the last frame.
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="true" >
<item android:drawable="@drawable/frame_000" android:duration="60"/>
<item android:drawable="@drawable/frame_001" android:duration="60"/>
<item android:drawable="@drawable/frame_002" android:duration="60"/>
<item android:drawable="@drawable/frame_003" android:duration="60"/>
<item android:drawable="@drawable/frame_004" android:duration="60"/>
<!-- option 1: add last frame twice -->
<item android:drawable="@drawable/frame_004" android:duration="0"/>
<!-- option 2: add transparent last frame -->
<item android:drawable="@android:color/transparent" android:duration="0"/>
</animation-list>
Upvotes: 0
Reputation: 1268
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.
Upvotes: 1
Reputation: 7889
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();
}
});
Upvotes: 47
Reputation: 18961
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.
Upvotes: 2
Reputation: 954
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();
}
});
}
Upvotes: 3
Reputation: 42860
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 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>
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();
Upvotes: 0
Reputation: 474
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;
}
}
Upvotes: 0
Reputation: 10358
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();
}
Upvotes: 0
Reputation: 492
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);
}
Upvotes: 17
Reputation: 430
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();
Upvotes: 26
Reputation: 2784
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;
}
}
Upvotes: 23
Reputation: 3366
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.
Upvotes: -4
Reputation: 292
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.
Upvotes: 4
Reputation: 313
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());
Upvotes: 1