简介
作为苦逼的程序员,产品和设计提出来的需求咱也没法拒绝,这不,前两天设计就给提了个需求,要求在帧动画结束后,把原位置的动画替换成一段文字。我们知道,在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
方法中
1 | private void updateDrawable(Drawable d) { |
可以看到,这里将我们设置的图片资源赋值到mDrawable
上。注意,这里有一个Drawable
动起来的关键点,同时也是我们动画监听的最终切入点:Drawable.setCallback(this)
,我们后面分析帧切换的时候会详细去聊它。
我们知道,一个控件想要绘制内容得在onDraw
方法中操作Canvas
,所以让我们再来看看onDraw
方法
1 | protected void onDraw(Canvas canvas) { |
可以看到,这里调用了Drawable.draw
方法将Drawable
自身绘制到ImageView
的Canvas
上
DrawableContainer
查看AnimationDrawable
的继承关系我们可以得知它继承自DrawableContainer
,从命名中我们就能看出来,它是Drawable
的容器,我们来看一下它所实现的draw
方法:
1 | public void draw(Canvas canvas) { |
mLastDrawable
是为了完成动画的切换效果(出入场动画)所准备的,我们可以不用关心它。
我们可以发现,它的内部有一个名为mCurrDrawable
的成员变量,我们可以合理猜测它是通过切换mCurrDrawable
指向的目标Drawable
来完成展示不同图片的功能,那么事实是这样吗?
没错,DrawableContainer
给我们提供了一个selectDrawable
方法,用来切换不同的图片:
1 | public boolean selectDrawable(int index) { |
可以看到,和我们猜想的一样,在DrawableContainer
的内部有一个子类DrawableContainerState
用于保存所有的Drawable
,它继承自Drawable.ConstantState
,是用来储存Drawable
间的常量状态和数据的。在DrawableContainerState
中有一个mDrawables
数组用于保存所有的Drawable
,通过addChild
方法将Drawable
加入到这个数组中
而在selectDrawable
方法中,它通过getChild
方法去获取当前应该显示的Drawable
,并将其和index
分别赋值给它的两个成员变量mCurrDrawable
和mCurIndex
,然后调用invalidateSelf
方法执行重绘:
1 | public void invalidateSelf() { |
invalidateSelf
被定义实现在Drawable
类中,还记得我之前让大家注意的Callback
吗?在设置图片这一步时,它就被赋值了,实际上这个接口被View
所实现,所以在前面我们可以看到调用setCallback
时,我们传入的参数为this
不过ImageView
在继承View
的同时也重写了这个invalidateDrawable
方法,最终调用了invalidate
方法执行重绘,此时,一张新的图片就被展示到我们的屏幕上了
1 | //ImageView.invalidateDrawable |
AnimationDrawable
DrawableContainer
分析完后,我们可以很自然的想到,AnimationDrawable
就是通过DrawableContainer
这种可以切换图片的机制,每隔一定时间执行一下selectDrawable
便可以达成帧动画的效果了。
我们先回想一下,在代码中怎么构造出一个多帧的AnimationDrawable
?没错,用默认构造方法实例化出来后,调用它的addFrame
方法往里一帧帧的添加图片:
1 | public void addFrame(@NonNull Drawable frame, int duration) { |
可以看到AnimationDrawable
也有一个内部类AnimationState
,继承自DrawableContainerState
,它的addFrame
方法就是调用DrawableContainerState.addChild
方法添加图片,同时将这张图片的持续时间保存在mDurations
数组中:
1 | public void addFrame(Drawable dr, int dur) { |
想让AnimationDrawable
动起来的话,我们得要调用它的start
方法,那我们就从这个方法开始分析:
1 | public void start() { |
这里将mAnimating
状态置为true
,然后调用setFrame
方法从第0帧开始展示图片
1 | private void setFrame(int frame, boolean unschedule, boolean animate) { |
这里可以看到,和我们所想的一样,调用了DrawableContainer.selectDrawable
切换当前展示图片,由于我们之前将mAnimating
赋值为了true
,所以会调用scheduleSelf
方法调度展示下一张图片,时间为当前帧持续时间后
1 | public void scheduleSelf(@NonNull Runnable what, long when) { |
scheduleSelf
方法调用了Drawable.Callback.scheduleDrawable
方法,我们去View
里面看实现:
1 | public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { |
实际上两个分支最终都是通过Handler
实现延时调用,而调用的Runnable
对象就是之前scheduleSelf
传入的this
。没错,AnimationDrawable
实现了Runnable
接口:
1 | public void run() { |
可以看到,在一帧持续时间结束后,便会调用nextFrame
方法,计算下一帧的index
,然后调用setFrame
方法切换下一帧,形成一个循环,这样一帧帧的图片便动了起来,形成了帧动画
包装Drawable.Callback
我们从源码层面分析了帧动画是如何运作的,那么怎么监听动画事件相信各位应该都能得出结论了吧?没错,就是重设Drawable
的Callback
当Drawable
被设置到控件中后,控件会将自身作为Drawable.Callback
设置给Drawable
,那么我们只需要重新给Drawable
设置一个Drawable.Callback
,在其中调用View
回调方法的同时,加入自己的监听逻辑即可
1 | val animDrawable = imageView.drawable as AnimationDrawable |
以上的代码便是示例,当满足动画运行到最后一帧,且满足结束状态时,在最后一帧的持续时间后处理结束后需要做的事
当AnimationDrawable
切换Visible
状态为false
时,动画会被暂停,如果在动画结束后触发setVisible(false)
事件,也会触发invalidateDrawable
回调,所以这里需要额外判断一下isVisible
自己包装的Drawable.Callback
一定需要找个东西将它强引用起来,因为Drawable.Callback
是以弱引用的形式被保存在Drawable
内的,很容易被回收,一旦被回收,整个AnimationDrawable
动画就动不起来了
尾声
为了这么简单一个小功能,还得跑到源码里看怎么实现,对此我的感受是:一入安卓深似海,从此头发是路人