如何完美监听帧动画?AnimationDrawable深度解析

简介

作为苦逼的程序员,产品和设计提出来的需求咱也没法拒绝,这不,前两天设计就给提了个需求,要求在帧动画结束后,把原位置的动画替换成一段文字。我们知道,在Android中,帧动画的实现类为AnimationDrawable,而这玩意儿又不像Animator一样可以通过addListener之类的方法监听动画的开始、结束等事件,那我们该怎么监听AnimationDrawable的结束事件呢?

目前网上大多数的做法都是获取帧动画的总时长,然后用Handler做一个postDelayed执行结束后的事情。这种方法怎么说呢?能用,但是不够精准也不够优雅,本文我们将从源码层面解析AnimationDrawable是如何将一帧帧的图片组合起来展示成连续的动画的,再从中寻求动画监听的切入点。

注:只想看实现的朋友们可以直接跳到 包装Drawable.Callback 这一节看最终实现

ImageView如何展示Drawable

AnimationDrawable说到底它也就是个Drawable,而我们一般都是使用ImageView作为Drawable展示的布局,那我们就以此作为入口开始分析DrawableImageView中是如何被展示的。

回想一下,我们想要给一个ImageView设置图片一般可以用下面几种方法:

  • setImageBitmap
  • setImageResource
  • setImageURI
  • setImageDrawable

setImageBitmap会将Bitmap包装成一个BitmapDrawable,然后再调用setImageDrawable方法。

setImageResourcesetImageURI方法会通过resolveUri方法从ResourceUri中解析出Drawable,然后调用updateDrawable方法

setImageDrawable方法则会直接调用updateDrawable方法

最终殊途同归走到updateDrawable方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void updateDrawable(Drawable d) {
...
if (mDrawable != null) {
sameDrawable = mDrawable == d;
mDrawable.setCallback(null);
unscheduleDrawable(mDrawable);
...
}

mDrawable = d;

if (d != null) {
d.setCallback(this);
...
} else {
...
}
}

可以看到,这里将我们设置的图片资源赋值到mDrawable上。注意,这里有一个Drawable动起来的关键点,同时也是我们动画监听的最终切入点:Drawable.setCallback(this),我们后面分析帧切换的时候会详细去聊它。

我们知道,一个控件想要绘制内容得在onDraw方法中操作Canvas,所以让我们再来看看onDraw方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

if (mDrawable == null) {
return; // couldn't resolve the URI
}

if (mDrawableWidth == 0 || mDrawableHeight == 0) {
return; // nothing to draw (empty bounds)
}

...
mDrawable.draw(canvas);
...
}

可以看到,这里调用了Drawable.draw方法将Drawable自身绘制到ImageViewCanvas

DrawableContainer

查看AnimationDrawable的继承关系我们可以得知它继承自DrawableContainer,从命名中我们就能看出来,它是Drawable的容器,我们来看一下它所实现的draw方法:

1
2
3
4
5
6
7
8
public void draw(Canvas canvas) {
if (mCurrDrawable != null) {
mCurrDrawable.draw(canvas);
}
if (mLastDrawable != null) {
mLastDrawable.draw(canvas);
}
}

mLastDrawable是为了完成动画的切换效果(出入场动画)所准备的,我们可以不用关心它。

我们可以发现,它的内部有一个名为mCurrDrawable的成员变量,我们可以合理猜测它是通过切换mCurrDrawable指向的目标Drawable来完成展示不同图片的功能,那么事实是这样吗?

没错,DrawableContainer给我们提供了一个selectDrawable方法,用来切换不同的图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean selectDrawable(int index) {
if (index == mCurIndex) {
return false;
}

...

if (index >= 0 && index < mDrawableContainerState.mNumChildren) {
final Drawable d = mDrawableContainerState.getChild(index);
mCurrDrawable = d;
mCurIndex = index;
...
} else {
mCurrDrawable = null;
mCurIndex = -1;
}

...

invalidateSelf();

return true;
}

可以看到,和我们猜想的一样,在DrawableContainer的内部有一个子类DrawableContainerState用于保存所有的Drawable,它继承自Drawable.ConstantState,是用来储存Drawable间的常量状态和数据的。在DrawableContainerState中有一个mDrawables数组用于保存所有的Drawable,通过addChild方法将Drawable加入到这个数组中

而在selectDrawable方法中,它通过getChild方法去获取当前应该显示的Drawable,并将其和index分别赋值给它的两个成员变量mCurrDrawablemCurIndex,然后调用invalidateSelf方法执行重绘:

1
2
3
4
5
6
public void invalidateSelf() {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}

invalidateSelf被定义实现在Drawable类中,还记得我之前让大家注意的Callback吗?在设置图片这一步时,它就被赋值了,实际上这个接口被View所实现,所以在前面我们可以看到调用setCallback时,我们传入的参数为this

不过ImageView在继承View的同时也重写了这个invalidateDrawable方法,最终调用了invalidate方法执行重绘,此时,一张新的图片就被展示到我们的屏幕上了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//ImageView.invalidateDrawable
public void invalidateDrawable(@NonNull Drawable dr) {
if (dr == mDrawable) {
if (dr != null) {
// update cached drawable dimensions if they've changed
final int w = dr.getIntrinsicWidth();
final int h = dr.getIntrinsicHeight();
if (w != mDrawableWidth || h != mDrawableHeight) {
mDrawableWidth = w;
mDrawableHeight = h;
// updates the matrix, which is dependent on the bounds
configureBounds();
}
}
/* we invalidate the whole view in this case because it's very
* hard to know where the drawable actually is. This is made
* complicated because of the offsets and transformations that
* can be applied. In theory we could get the drawable's bounds
* and run them through the transformation and offsets, but this
* is probably not worth the effort.
*/
invalidate();
} else {
super.invalidateDrawable(dr);
}
}

AnimationDrawable

DrawableContainer分析完后,我们可以很自然的想到,AnimationDrawable就是通过DrawableContainer这种可以切换图片的机制,每隔一定时间执行一下selectDrawable便可以达成帧动画的效果了。

我们先回想一下,在代码中怎么构造出一个多帧的AnimationDrawable?没错,用默认构造方法实例化出来后,调用它的addFrame方法往里一帧帧的添加图片:

1
2
3
4
5
6
public void addFrame(@NonNull Drawable frame, int duration) {
mAnimationState.addFrame(frame, duration);
if (!mRunning) {
setFrame(0, true, false);
}
}

可以看到AnimationDrawable也有一个内部类AnimationState,继承自DrawableContainerState,它的addFrame方法就是调用DrawableContainerState.addChild方法添加图片,同时将这张图片的持续时间保存在mDurations数组中:

1
2
3
4
public void addFrame(Drawable dr, int dur) {
int pos = super.addChild(dr);
mDurations[pos] = dur;
}

想让AnimationDrawable动起来的话,我们得要调用它的start方法,那我们就从这个方法开始分析:

1
2
3
4
5
6
7
8
9
public void start() {
mAnimating = true;

if (!isRunning()) {
// Start from 0th frame.
setFrame(0, false, mAnimationState.getChildCount() > 1
|| !mAnimationState.mOneShot);
}
}

这里将mAnimating状态置为true,然后调用setFrame方法从第0帧开始展示图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void setFrame(int frame, boolean unschedule, boolean animate) {
if (frame >= mAnimationState.getChildCount()) {
return;
}
mAnimating = animate;
mCurFrame = frame;
selectDrawable(frame);
if (unschedule || animate) {
unscheduleSelf(this);
}
if (animate) {
// Unscheduling may have clobbered these values; restore them
mCurFrame = frame;
mRunning = true;
scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
}
}

这里可以看到,和我们所想的一样,调用了DrawableContainer.selectDrawable切换当前展示图片,由于我们之前将mAnimating赋值为了true,所以会调用scheduleSelf方法调度展示下一张图片,时间为当前帧持续时间后

1
2
3
4
5
6
public void scheduleSelf(@NonNull Runnable what, long when) {
final Callback callback = getCallback();
if (callback != null) {
callback.scheduleDrawable(this, what, when);
}
}

scheduleSelf方法调用了Drawable.Callback.scheduleDrawable方法,我们去View里面看实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
if (verifyDrawable(who) && what != null) {
final long delay = when - SystemClock.uptimeMillis();
if (mAttachInfo != null) {
mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
Choreographer.CALLBACK_ANIMATION, what, who,
Choreographer.subtractFrameDelay(delay));
} else {
// Postpone the runnable until we know
// on which thread it needs to run.
getRunQueue().postDelayed(what, delay);
}
}
}

实际上两个分支最终都是通过Handler实现延时调用,而调用的Runnable对象就是之前scheduleSelf传入的this。没错,AnimationDrawable实现了Runnable接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void run() {
nextFrame(false);
}

private void nextFrame(boolean unschedule) {
int nextFrame = mCurFrame + 1;
final int numFrames = mAnimationState.getChildCount();
final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1);

// Loop if necessary. One-shot animations should never hit this case.
if (!mAnimationState.mOneShot && nextFrame >= numFrames) {
nextFrame = 0;
}

setFrame(nextFrame, unschedule, !isLastFrame);
}

可以看到,在一帧持续时间结束后,便会调用nextFrame方法,计算下一帧的index,然后调用setFrame方法切换下一帧,形成一个循环,这样一帧帧的图片便动了起来,形成了帧动画

包装Drawable.Callback

我们从源码层面分析了帧动画是如何运作的,那么怎么监听动画事件相信各位应该都能得出结论了吧?没错,就是重设DrawableCallback

Drawable被设置到控件中后,控件会将自身作为Drawable.Callback设置给Drawable,那么我们只需要重新给Drawable设置一个Drawable.Callback,在其中调用View回调方法的同时,加入自己的监听逻辑即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
val animDrawable = imageView.drawable as AnimationDrawable
val callback = object : Drawable.Callback {
override fun invalidateDrawable(who: Drawable) {
imageView.invalidateDrawable(who)
if (animDrawable.getFrame(animDrawable.numberOfFrames - 1) == current
&& animDrawable.isOneShot
&& animDrawable.isRunning
&& animDrawable.isVisible
) {
val lastFrameDuration = getDuration(animDrawable.numberOfFrames - 1)
postDelayed({ ...//结束后需要做的事 }, lastFrameDuration.toLong())
}
}

override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
imageView.scheduleDrawable(who, what, `when`)
}

override fun unscheduleDrawable(who: Drawable, what: Runnable) {
imageView.unscheduleDrawable(who, what)
}
}
//注意一定需要用一个成员变量或其他方式持有这个Callback
//因为Drawable.Callback是以弱引用的形式被保存在Drawable内的,很容易被回收
mCallbackHolder = callback
animDrawable.callback = callback
animDrawable.start()

以上的代码便是示例,当满足动画运行到最后一帧,且满足结束状态时,在最后一帧的持续时间后处理结束后需要做的事

AnimationDrawable切换Visible状态为false时,动画会被暂停,如果在动画结束后触发setVisible(false)事件,也会触发invalidateDrawable回调,所以这里需要额外判断一下isVisible

自己包装的Drawable.Callback一定需要找个东西将它强引用起来,因为Drawable.Callback是以弱引用的形式被保存在Drawable内的,很容易被回收,一旦被回收,整个AnimationDrawable动画就动不起来了

尾声

为了这么简单一个小功能,还得跑到源码里看怎么实现,对此我的感受是:一入安卓深似海,从此头发是路人