简介
作为苦逼的程序员,产品和设计提出来的需求咱也没法拒绝,这不,前两天设计就给提了个需求,要求在帧动画结束后,把原位置的动画替换成一段文字。我们知道,在android
中,帧动画的实现类为animationdrawable
,而这玩意儿又不像animator
一样可以通过addlistener
之类的方法监听动画的开始、结束等事件,那我们该怎么监听animationdrawable
的结束事件呢?
目前网上大多数的做法都是获取帧动画的总时长,然后用handler
做一个postdelayed
执行结束后的事情。这种方法怎么说呢?能用,但是不够精准也不够优雅,本文我们将从源码层面解析animationdrawable
是如何将一帧帧的图片组合起来展示成连续的动画的,再从中寻求动画监听的切入点。
注:只想看实现的朋友们可以直接跳到 包装drawable.callback 这一节看最终实现
imageview如何展示drawable
animationdrawable
说到底它也就是个drawable
,而我们一般都是使用imageview
作为drawable
展示的布局,那我们就以此作为入口开始分析drawable
在imageview
中是如何被展示的。
回想一下,我们想要给一个imageview
设置图片一般可以用下面几种方法:
setimagebitmap
setimageresource
setimageuri
setimagedrawable
setimagebitmap
会将bitmap
包装成一个bitmapdrawable
,然后再调用setimagedrawable
方法。
setimageresource
和setimageuri
方法会通过resolveuri
方法从resource
或uri
中解析出drawable
,然后调用updatedrawable
方法
setimagedrawable
方法则会直接调用updatedrawable
方法
最终殊途同归走到updatedrawable
方法中
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
方法
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
自身绘制到imageview
的canvas
上
drawablecontainer
查看animationdrawable
的继承关系我们可以得知它继承自drawablecontainer
,从命名中我们就能看出来,它是drawable
的容器,我们来看一下它所实现的draw
方法:
public void draw(canvas canvas) {
if (mcurrdrawable != null) {
mcurrdrawable.draw(canvas);
}
if (mlastdrawable != null) {
mlastdrawable.draw(canvas);
}
}
mlastdrawable
是为了完成动画的切换效果(出入场动画)所准备的,我们可以不用关心它。
我们可以发现,它的内部有一个名为mcurrdrawable
的成员变量,我们可以合理猜测它是通过切换mcurrdrawable
指向的目标drawable
来完成展示不同图片的功能,那么事实是这样吗?
没错,drawablecontainer
给我们提供了一个selectdrawable
方法,用来切换不同的图片:
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
分别赋值给它的两个成员变量mcurrdrawable
和mcurindex
,然后调用invalidateself
方法执行重绘:
public void invalidateself() {
final callback callback = getcallback();
if (callback != null) {
callback.invalidatedrawable(this);
}
}
invalidateself
被定义实现在drawable
类中,还记得我之前让大家注意的callback
吗?在设置图片这一步时,它就被赋值了,实际上这个接口被view
所实现,所以在前面我们可以看到调用setcallback
时,我们传入的参数为this
不过imageview
在继承view
的同时也重写了这个invalidatedrawable
方法,最终调用了invalidate
方法执行重绘,此时,一张新的图片就被展示到我们的屏幕上了
//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
方法往里一帧帧的添加图片:
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
数组中:
public void addframe(drawable dr, int dur) {
int pos = super.addchild(dr);
mdurations[pos] = dur;
}
想让animationdrawable
动起来的话,我们得要调用它的start
方法,那我们就从这个方法开始分析:
public void start() {
manimating = true;
if (!isrunning()) {
// start from 0th frame.
setframe(0, false, manimationstate.getchildcount() > 1
|| !manimationstate.moneshot);
}
}
这里将manimating
状态置为true
,然后调用setframe
方法从第0帧开始展示图片
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
方法调度展示下一张图片,时间为当前帧持续时间后
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
里面看实现:
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
接口:
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
我们从源码层面分析了帧动画是如何运作的,那么怎么监听动画事件相信各位应该都能得出结论了吧?没错,就是重设drawable
的callback
当drawable
被设置到控件中后,控件会将自身作为drawable.callback
设置给drawable
,那么我们只需要重新给drawable
设置一个drawable.callback
,在其中调用view
回调方法的同时,加入自己的监听逻辑即可
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
动画就动不起来了
尾声
为了这么简单一个小功能,还得跑到源码里看怎么实现,对此我的感受是:一入安卓深似海,从此头发是路人
发表评论