-
概述
相比入其他两种动画,逐帧动画在原理层面是最简单的,同样可以使用xml文件定义所有帧,然后通过setBackground设置到View的background属性中,另外也可以自己创建AnimationDrawable对象,然后调用addFrame(@NonNull Drawable frame, int duration)方法添加每一帧。
-
AnimationDrawable
不管是通过TypeArray的getDrawable(attr)生成的AnimationDrawable对象还是自己new的,最后都是通过AnimationDrawable的构造方法创建的,也都是通过执行addFrame方法添加帧的,AnimationDrawable内部有一个类型为AnimationState的mAnimationState会保存这些帧的信息:
public void addFrame(@NonNull Drawable frame, int duration) { mAnimationState.addFrame(frame, duration); if (!mRunning) { setFrame(0, true, false); } }
AnimationState的addFrame如下:
public void addFrame(Drawable dr, int dur) { // Do not combine the following. The array index must be evaluated before // the array is accessed because super.addChild(dr) has a side effect on mDurations. int pos = super.addChild(dr); mDurations[pos] = dur; }
里面有一个mDurations保存每一帧的显示时间,super是DrawableContainerState,他的addChild方法如下:
public final int addChild(Drawable dr) { final int pos = mNumChildren; if (pos >= mDrawables.length) { growArray(pos, pos+10); } dr.mutate(); dr.setVisible(false, true); dr.setCallback(mOwner); mDrawables[pos] = dr; mNumChildren++; mChildrenChangingConfigurations |= dr.getChangingConfigurations(); invalidateCache(); mConstantPadding = null; mCheckedPadding = false; mCheckedConstantSize = false; mCheckedConstantState = false; return pos; }
有一个mDrawables的数组存放每一帧的Drawable对象,每一个Drawable都设成初始visible为false,注意这里的顺序可见是先添加先显示。
回到addFrame,这里会调用一次setFrame方法,这是更新当前帧的方法,下面再说它,这里会更新到显示第一个帧。
-
start
所有的帧的数据都准备好了之后就可以调用start方法开启动画了:
@Override public void start() { mAnimating = true; if (!isRunning()) { // Start from 0th frame. setFrame(0, false, mAnimationState.getChildCount() > 1 || !mAnimationState.mOneShot); } }
可以看到,这里还是调用的setFrame,只不过这里传入的最后一个参数是根据AnimationState来决定的,如果帧数大于1或者mOneShot为false(默认)的话就是true:
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]); } }
首先会调用selectDrawable方法设置当前要显示的首个图片:
public boolean selectDrawable(int index) { if (index == mCurIndex) { return false; } final long now = SystemClock.uptimeMillis(); if (DEBUG) android.util.Log.i(TAG, toString() + " from " + mCurIndex + " to " + index + ": exit=" + mDrawableContainerState.mExitFadeDuration + " enter=" + mDrawableContainerState.mEnterFadeDuration); if (mDrawableContainerState.mExitFadeDuration > 0) { if (mLastDrawable != null) { //上一张正在渐变隐藏时直接设置成不可见(不等它自己隐藏了) mLastDrawable.setVisible(false, false); } if (mCurrDrawable != null) { mLastDrawable = mCurrDrawable; mLastIndex = mCurIndex; mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration; } else { mLastDrawable = null; mLastIndex = -1; mExitAnimationEnd = 0; } } else if (mCurrDrawable != null) { //设置当前Drawable不可见 mCurrDrawable.setVisible(false, false); } if (index >= 0 && index < mDrawableContainerState.mNumChildren) { final Drawable d = mDrawableContainerState.getChild(index); mCurrDrawable = d; mCurIndex = index; if (d != null) { if (mDrawableContainerState.mEnterFadeDuration > 0) { mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration; } initializeDrawableForDisplay(d); } } else { mCurrDrawable = null; mCurIndex = -1; } if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) { if (mAnimationRunnable == null) { mAnimationRunnable = new Runnable() { @Override public void run() { animate(true); invalidateSelf(); } }; } else { unscheduleSelf(mAnimationRunnable); } // Compute first frame and schedule next animation. animate(true); } invalidateSelf(); return true; }
首先会把当前显示的mCurrDrawable设置不可见,然后设置当前帧的出场时间mEnterAnimationEnd为当前时间加上渐变显示的时长。initializeDrawableForDisplay方法是设置每张drawable的效果,比如alpha、color filter、tint list等。最后一部分其实就是设置入场和出场动画。
-
设置入场和出场动画
出入场动画都是通过构造函数中调用的setConstantState方法传入的DrawableContainerState中的mEnterFadeDuration和mExitFadeDuration设置的。
看一下animate方法:
void animate(boolean schedule) { mHasAlpha = true; final long now = SystemClock.uptimeMillis(); boolean animating = false; if (mCurrDrawable != null) { if (mEnterAnimationEnd != 0) { if (mEnterAnimationEnd <= now) { mCurrDrawable.setAlpha(mAlpha); mEnterAnimationEnd = 0; } else { int animAlpha = (int)((mEnterAnimationEnd-now)*255) / mDrawableContainerState.mEnterFadeDuration; mCurrDrawable.setAlpha(((255-animAlpha)*mAlpha)/255); animating = true; } } } else { mEnterAnimationEnd = 0; } if (mLastDrawable != null) { if (mExitAnimationEnd != 0) { if (mExitAnimationEnd <= now) { mLastDrawable.setVisible(false, false); mLastDrawable = null; mLastIndex = -1; mExitAnimationEnd = 0; } else { int animAlpha = (int)((mExitAnimationEnd-now)*255) / mDrawableContainerState.mExitFadeDuration; mLastDrawable.setAlpha((animAlpha*mAlpha)/255); animating = true; } } } else { mExitAnimationEnd = 0; } if (schedule && animating) { scheduleSelf(mAnimationRunnable, now + 1000 / 60); } }
可见这里算出透明度变化比率然后通过Drawable的setAlpha方法修改透明度,所以出入场动画其实只是透明度的变化。那么是动画就不可能只变化一次,在最后会调用scheduleSelf方法:
public void scheduleSelf(@NonNull Runnable what, long when) { final Callback callback = getCallback(); if (callback != null) { callback.scheduleDrawable(this, what, when); } }
这里有一个callback,它是在DrawableContainerState的addChild方法中设置的,参数是mOwner,mOwner就是AnimationDrawable本身,它本身并没有继承Drawable.Callback,是它的父类DrawableContainer实现了这个接口,实现的三个方法如下:
@Override public void invalidateDrawable(@NonNull Drawable who) { // This may have been called as the result of a tint changing, in // which case we may need to refresh the cached statefulness or // opacity. if (mDrawableContainerState != null) { mDrawableContainerState.invalidateCache(); } if (who == mCurrDrawable && getCallback() != null) { getCallback().invalidateDrawable(this); } } @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { if (who == mCurrDrawable && getCallback() != null) { getCallback().scheduleDrawable(this, what, when); } } @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { if (who == mCurrDrawable && getCallback() != null) { getCallback().unscheduleDrawable(this, what); } }
这里也需要一个Callback,这个Callback是属于AnimationDrawable的,它在View的setBackground方法中设置的,传入的是this,即View本身实现了Drawable.Callback接口:
/** * Schedules an action on a drawable to occur at a specified time. * * @param who the recipient of the action * @param what the action to run on the drawable * @param when the time at which the action must occur. Uses the * {@link SystemClock#uptimeMillis} timebase. */ @Override 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); } } }
可以看到,这里使用了mChoreographer(负责屏幕刷新之后的回调),即屏幕刷新之后都会执行上面的mAnimationRunnable,也就会重走animate方法,一直循环,等到animate中判断当前时间超过截止时间了,animating字段就不会为true了,也就不会执行scheduleSelf逻辑了,从而渐入渐出动画停止。
上面的渐入渐出动画其实是一个异步动作,因为它会被放在一个队列里执行,安排完渐入渐出动画之后,会执行invalidateSelf方法(其实每次渐入渐出动画方法执行时都会调用一次):
public void invalidateSelf() { final Callback callback = getCallback(); if (callback != null) { callback.invalidateDrawable(this); } }
我们还是要到View中去找这个方法:
/** * Invalidates the specified Drawable. * * @param drawable the drawable to invalidate */ @Override public void invalidateDrawable(@NonNull Drawable drawable) { if (verifyDrawable(drawable)) { final Rect dirty = drawable.getDirtyBounds(); final int scrollX = mScrollX; final int scrollY = mScrollY; invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY); rebuildOutline(); } }
可见这里会调用invalidate方法,所以会重新设置background,也就应用了最新的Drawable。
回到setFrame方法,接着会执行unscheduleSelf方法,这个方法最终也会执行到View的unscheduleDrawable方法:
/** * Cancels a scheduled action on a drawable. * * @param who the recipient of the action * @param what the action to cancel */ @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { if (verifyDrawable(who) && what != null) { if (mAttachInfo != null) { mAttachInfo.mViewRootImpl.mChoreographer.removeCallbacks( Choreographer.CALLBACK_ANIMATION, what, who); } getRunQueue().removeCallbacks(what); } }
这里其实就是把之前的Callback先移除掉,因为接下来会重新调用scheduleSelf设置,这时传入的Runnable对象就是实现了Runnable接口的AnimationDrawable本身,传入的第二个参数就是Runnable的延迟执行时间,可见是当前帧的duration,这里就是控制每一帧显示时长的关键,看一下AnimationDrawable实现的run方法:
@Override public void run() { nextFrame(false); }
这里调用了nextFrame方法:
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); }
可以看到,这里会判断是否需要从头播放还是停止动画从而停在最后一帧。
至此,整个动画执行流程就走完了。
-
xml形式创建过程简述
通过xml形式加载的AnimationDrawable会调用inflate方法:
@Override public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable); super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible); updateStateFromTypedArray(a); updateDensity(r); a.recycle(); inflateChildElements(r, parser, attrs, theme); setFrame(0, true, false); }
inflateChildElements方法如下:
private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { int type; final int innerDepth = parser.getDepth()+1; int depth; while ((type=parser.next()) != XmlPullParser.END_DOCUMENT && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { if (type != XmlPullParser.START_TAG) { continue; } if (depth > innerDepth || !parser.getName().equals("item")) { continue; } final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawableItem); final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1); if (duration < 0) { throw new XmlPullParserException(parser.getPositionDescription() + ": <item> tag requires a 'duration' attribute"); } Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable); a.recycle(); if (dr == null) { while ((type=parser.next()) == XmlPullParser.TEXT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException(parser.getPositionDescription() + ": <item> tag requires a 'drawable' attribute or child tag" + " defining a drawable"); } dr = Drawable.createFromXmlInner(r, parser, attrs, theme); } //这里也是调用addFrame添加帧 mAnimationState.addFrame(dr, duration); if (dr != null) { dr.setCallback(this); } } }
可以看到,和我们手动创建的过程原理是一样的。
-
总结
AnimationDrawable的大体流程就是:
- 首先通过addFrame方法添加帧数据(包括Drawable图片和duration显示时长);
- 然后调用View的setBackground方法绑定View,这个过程中传入的AnimationDrawable对象会调用setCallback方法和View绑定在一起,即AnimationDrawable的mCallback就是View本身,View内部实现了Drawable.Callback接口;
- 调用start方法开启动画;
- start内部调用setFrame方法设置index为0的起始下标处的帧为第一帧;
- setFrame方法首先会调用selectDrawable方法,这个方法负责给当前要显示的Drawable做一些设置,比如修改透明度、染色等,还会异步开启渐入渐出动画(如果设置开启了的话);
- 其次调用unscheduleSelf方法移除掉之前的切换帧的回调;
- 最后调用scheduleSelf设置新的回调,这个回调中会调用nextFrame切换帧;
- 渐入渐出动画Runnable和帧切换Runnable都是通过scheduleSelf这个方法设置的,这个方法的第二个参数表示延迟执行时间,对于渐入渐出动画来说传入的就是渐入渐出的时长,对于帧切换来说传入的就是帧的duration;
- 渐入渐出动画Runnable和帧切换Runnable回调中实质都是重新调用它们的入口方法重走逻辑,前者是animate方法,后者是setFrame方法;
- scheduleSelf最终执行的就是View中实现的Drawable.Callback的scheduleDrawable方法,方法中使用的mAttachInfo.mViewRootImpl.mChoreographer绑定回调接口,而mChoreographer就是和屏幕刷新相关的,即回调接口是和屏幕刷新绑定在一起的,从而达到即时的变化。
网友评论