Android动画Drawable和如何知道动画何时结束

28
我想使用几张图片文件制作动画,AnimationDrawable非常适合这个任务。但是我需要知道动画何时开始和结束(例如添加类似Animation.AnimationListener的监听器)。 经过搜索,我有一种不好的感觉,即AnimationDrawable不支持监听器。 有人知道如何在Android上创建带有监听器的逐帧图像动画吗?
14个回答

47

在阅读了一些资料后,我想到了这个解决方案。我仍然惊讶于AnimationDrawable对象中没有监听器,但是我不想来回传递回调,所以我创建了一个抽象类,其中触发了一个onAnimationFinish()方法。希望这能帮助到某些人。

自定义动画可绘制类:

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

使用这个类:

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

1
优秀的解决方案!不过有一点小问题,别忘了在onAnimationFinish中指定“public”,因为默认情况下它是私有的。 - GuilhE
默认情况下,它对于同一包中的类是可见的 ;) - Tomasito665
2
不过,这种解决方案的坏处是 oneshot='true' 无法正常工作。 - Tomasito665
1
@Tomasito665 不起作用?你应该执行cad.setOneSet(true); 这个对我有效。 - Andrew Indayang
非常有帮助。非常感谢。 - reixa
这实际上与CommonWare的答案相同,只是将解决方案封装到一个单独的类中。 - Miloš Černilovský

26

我需要知道我的一次性AnimationDrawable何时完成,而无需子类化AnimationDrawable,因为我必须在XML中设置动画列表。我编写了这个类,并在Gingerbread和ICS上进行了测试。它可以轻松地扩展以在每个帧上回调。

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

以下是如何使用它的步骤。

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

感谢@benhylau!我认为这是一个非常优雅的解决方案,可以利用现有接口。只想补充一点,它似乎跳过了最后一帧,所以请记得在最后放入一个虚拟帧。 - weiy
3
只是为了澄清,它仅在动画完成时通知最后一帧。如果您想在动画逐帧进行时进行侦听,请使用此更新版本:https://gist.github.com/benhylau/a7d21c8806df9c73da7d - benhylau
我知道我来晚了,但你的方法真的很棒、优雅。谢谢你分享它!我仍然不明白为什么AnimationDrawable在这么长时间后仍没有支持监听器... - Talendar

23

通过覆盖AnimationDrawable类中的selectDrawable方法,可以轻松地跟踪动画结束。完整代码如下:

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

1
我该如何将一个视图(当前类为AnimationDrawable)的背景更改为AnimationDrawable2? - Jonny
1
对我来说,这似乎是一个更好的、不依赖时间的答案。 - Andrew Mackenzie
2
肯定是正确的答案,检查持续时间是否随着时间的推移而消失,或在较弱的设备上。 - tibbi
1
@AmeyaB 你需要将 AnimationDrawable animation = new AnimationDrawable(); 这一行改为 AnimationDrawable2 animation = new AnimationDrawable2();。然后调用 animation.setAnimationFinishListener(this); - 其中 this 是你的 Activity 或 Fragment 等。接下来,你只需要让你的 Activity/Fragment 类 implements AnimationDrawable2.IAnimationFinishListener。最后,添加 @Override public void onAnimationFinished() { ...} 方法,填充你的代码,就可以了。 :-) - ban-geoengineering
1
@AmeyaB 我的回答基于Ruslans并展示了如何实现它:https://dev59.com/-nE95IYBdhLWcg3wp_qn#45439992 - ban-geoengineering
显示剩余2条评论

17

我使用了一个递归函数,每timeBetweenChecks毫秒检查一次当前帧是否是最后一帧。

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

4

计时器不是很适合这种情况,因为像HowsItStack所说的那样,你会卡在尝试在非UI线程中执行的过程中。对于简单的任务,可以使用Handler在一定时间间隔内调用方法,就像这样:

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可以确保该操作仅执行一次。


4

在使用Kotlin时,使用AnimationDrawable非常简单,它有两个函数可用于计算动画持续时间,然后我们可以添加一个带有延迟的可运行对象来创建动画监听器。这里是一个简单的Kotlin扩展。

fun AnimationDrawable.onAnimationFinished(block: () -> Unit) {
    var duration: Long = 0
    for (i in 0..numberOfFrames) {
        duration += getDuration(i)
    }
    Handler().postDelayed({
        block()
    }, duration + 200)
}

3

如果你想在适配器中实现动画,应该使用下面的代码:

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

并在RecycleView适配器中实现

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

2

我也喜欢Ruslan的答案,但是为了满足我的需求,我不得不做出一些更改。

在我的代码中,我已经取消了Ruslan的finished标志,并且我还利用了super.selectDrawable()返回的布尔值。

这是我的代码:

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;

    }

}

以下是如何实现它的示例...

public class MyFragment extends Fragment implements AnimationDrawableWithCallback.IAnimationFinishListener {

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

        // Do whatever you need here

    }

}

如果你只想知道第一次动画循环何时完成,那么你可以在你的片段/活动中设置一个布尔标志。


1

我猜你的代码不起作用,因为你试图从非UI线程修改视图。尝试从你的Activity中调用runOnUiThread(Runnable)。我用它来在菜单动画完成后淡出菜单。这段代码对我有效:

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

1
您可以使用registerAnimationCallback来检查动画的开始和结束。
以下是代码片段:
// 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
}

ImageExt的目的是在动画开始后(在进度上)禁用,以防止用户疯狂点击动画并导致显示错误/错误的向量。使用逐帧动画时,您可能想要像这样触发另一个ImageView。
// Animation.kt
iv1.startAnim {
    iv2.startAnim {
        // iv3, etc
    }
}

但是上述解决方案看起来很丑。如果有更好的方法,请在下面评论或直接编辑此答案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接