摘要:本文根据Animator的源码分析其运行原理,包括:属性动画包含哪些基本元素;属性动画的运行流程是怎样的;属性动画是怎么对View起作用的;怎么组合并运行多个属性动画;属性动画的帧率决定者Choreographer
是如何工作的。
文章原创,允许转载,转载请注明出处。
1. 前言
上一篇文章《Android Animation运行原理详解》介绍了插间动画的原理,而Android3.0之后引进了一种动画实现——属性动画,放在以前可能会因为要兼容3.0以前系统而小犹豫下,但现在3.0以上系统已占有率已达97%以上(来自Android Studio统计数据),市场上许多应用甚至已经将4.0作为最低兼容版本了,因此属性动画基本就是标配了,再说就算真的有老古董应用想要兼容3.0以前系统也可以使用开源库NineOldAndroids来享受属性动画带来的快感。这种情况下,理解属性动画的运行原理,分析属性动画相对于插间动画而言有哪些优势就比较有意义了,而且属性动画似乎已经慢慢取代插间动画而成为动画的主流实现方式了。本文主要分享我在学习属性动画的过程中的一些收获,包括:属性动画包含哪些基本元素;属性动画的运行流程是怎样的;属性动画是怎么对View起作用的;怎么组合并运行多个属性动画;属性动画的帧率决定者Choreographer
是如何工作的。
2. 属性动画的基本元素
属性动画跟插间动画一样会包含动画相关的属性,如动画时长、延迟时间、插间器等等,为了后面分析动画运行流程时概念更加明确,这里我摘抄了ValueAnimator
源码中的字段,并做了相应的注解,对Animator有过了解的同学可以直接跳过,不熟悉的同学建议先扫一眼,然后重点关注PropertyValuesHolder
这个属性相关的介绍。
// 初始化函数是否被调用
boolean mInitialized = false;
// 动画时长
private long mDuration = (long)(300 * sDurationScale);
private long mUnscaledDuration = 300;
// 动画延时
private long mStartDelay = 0;
private long mUnscaledStartDelay = 0;
// 动画重复模式及次数
private int mRepeatCount = 0;
private int mRepeatMode = RESTART;
// 插间器,参看http://cogitolearning.co.uk/?p=1078
private TimeInterpolator mInterpolator = sDefaultInterpolator;
// 动画开始运行的时间点
long mStartTime;
// 是否需要在掉帧的时候调整动画开始时间点
boolean mStartTimeCommitted;
// 动画是否反方向运行,当repeatMode=REVERSE是会每个动画周期反转一次
private boolean mPlayingBackwards = false;
// 当前动画在一个动画周期中所处位置
private float mCurrentFraction = 0f;
// 动画是否延时
private boolean mStartedDelay = false;
// 动画完成延时的时间点
private long mDelayStartTime;
// 动画当前所处的状态:STOPPED, RUNNING, SEEKED
int mPlayingState = STOPPED;
// 动画是否被启动
private boolean mStarted = false;
// 动画是否被执行(以动画第一帧被计算为界)
private boolean mRunning = false;
// 回调监听器
private boolean mStartListenersCalled = false; // 确保AnimatorListener.onAnimationStart(Animator)仅被调用一次
ArrayList<AnimatorListener> mListeners = null; // start,end,cancel,repeat回调
ArrayList<AnimatorPauseListener> mPauseListeners = null; // pause, resume回调
ArrayList<AnimatorUpdateListener> mUpdateListeners = null; // value更新回调
以上属性都是动画的基本属性以及运行过程的状态记录,跟插间动画差别不大,而属性动画相对于插间动画来件引入了一些新的概念:可以暂停和恢复、可以调整进度,这些概念的引入,让动画的概念更加饱满起来,让动画有了视频播放的概念,而跟这些新概率相关的属性主要包括:
// 动画是否被暂停
boolean mPaused = false;
// 动画暂停时间点,用于在动画被恢复的时候调整mStartTime以确保动画能优雅地继续运行
private long mPauseTime;
// 动画是否从暂停中被恢复,用于表明动画可以调整mStartTime了
private boolean mResumed = false;
// 动画被设定的进度位置,具体功效见后文对动画流程的分析
float mSeekFraction = -1;
除了上面这些动画属性以外,Animator还包含了一个有别于Animation的属性,那就是PropertyValuesHolder
,PropertyValuesHolder
是用来保存某个属性property
对应的一组值,这些值对应了一个动画周期中的所有关键帧。其实,动画说到底是由动画帧组成的,Animator可以设定并保存整个动画周期中的关键帧,然后根据这些关键帧计算出动画周期中任一时间点对应的动画帧的动画数据,而每一帧的动画数据里都包含了一个时间点属性fraction
以及一个动画值mValue
,从而实现根据当前的时间点计算当前的动画值,然后用这个动画值去更新property
对应的属性,这也是为什么Animator被称为属性动画的原因,因为它的整个动画过程实际上就是不断计算并更新对象的属性:
// 保存property及其values的数组
PropertyValuesHolder[] mValues;
HashMap<String, PropertyValuesHolder> mValuesMap;
在这里必须要详细分析下PropertyValuesHolder
,因为这关系到后面对动画流程的理解。注意了,我要出大招了:
上面这张图详细地描述了PropertyValuesHolder
的组成,我知道大家一般是不愿意看这张图的,但是为什么我还要放在这?因为我都画了,不放怎么能显现出我的工作量- -!但是作为一个正义的程序员,我会用语言描述来拯救你们这些小懒虫,PropertyValuesHolder
由Property
及Keyframes
组成,其中Property
用于描述属性的特征:如属性名以及属性类型,并提供set及get方法用于获取及设定给定Target的对应属性值;Keyframes
由一组关键帧Keyframe
组成,每一个关键帧由fraction及value来定量描述,于是Keyframes
可以根据给定的fraction定位到两个关键帧,这两个关键帧的fraction组成的区间包含给定的fraction,然后根据定位到的两个关键帧以及设定插间器及求值器就可以计算出给定fraction对应的value。于是,PropertyValuesHolder
的整个工作流程也就呼之欲出了,首先通过setObjectValues
等函数来初始化关键帧组mKeyframes
,必要的情况下(如ObjectAnimator
)可以通过setStartValue
及setEndValue
来设定第一帧及最末帧的value,以上工作只是完成了PropertyValuesHolder
的初始化,之后就可以由Animator
在绘制动画帧的时候通过fraction来调用calculateValue
计算该fraction对应的value(实际上是由mKeyframes
的getValue
方法做出最终计算),获得对应的value之后,一方面可以通过getAnimatedValue
提供给Animator
使用,另一方面也可以通过setAnimatedValue
方法直接将该值设定到相应Target中去,这样PropertyValuesHolder
的职责也就完成了。有了PropertyValuesHolder
的鼎力支持之后,动画也就可以开始正常的运转起来了,具体的运转流程又是怎样的咧?且听下节讲解。
3. 属性动画的运行流程
通过上一小节的介绍,我想各位看客应该对动画的组成元素有个基本的了解了,接下来就让我们看看这些基本元素组合在一起之后能诞生怎样的奇妙功效,让我们揭开面纱一睹美人芳颜。
属性动画的运转流程大体可分为三种类型:善始善终型、英年早逝型、命运多舛型,但不管哪种类型首先必须进行以下这些必要的初始化工作:
- 通过
setIntValues
、setFloatValues
、setObjectValues
或setValues
初始化PropertyValuesHolder
数组 - 设定
mDuration
、mStartDelay
、mRepeatCount
、mRepeatMode
、mInterpolator
、各种监听器等动画相关参数
完成动画的初始化工作之后,随着start
的调用,动画正式开始。
(1)善始善终型动画
善始善终型动画描述的是一种无病而终的动画人生,这样的动画一旦start
就沿着既定的路线一直跑到终点,不快不慢、不长不短。
在start
函数中,首先会尝试获取或创建一个AnimationHandler
,这里要是不解释下AnimationHandler
可能就忽悠不下去了,因此我们来看看这是个什么鬼。
protected static ThreadLocal<AnimationHandler> sAnimationHandler =
new ThreadLocal<AnimationHandler>();
根据官方解释以及源码分析可以发现:这家伙就是一个定时任务处理器,根据Choreographer
的脉冲周期性地完成指定的任务,由于它是一个线程安全的静态变量,因此运行在同一线程中的所有Animator共用一个定时任务处理器,这样的好处在于:一方面可以保证Animator中计算某一时刻动画帧是在同一线程中运行的,避免了多线程同步的问题;另一方面,该线程下所有动画共用一个处理器,可以让这些动画有效地进行同步,从而让动画效果更加优雅。至于AnimationHandler
具体用来做哪些任务,我们看看动画怎么蹦跶的就明白了。
成功获取到AnimationHandler
之后,会做如下处理:
mPlayingState = STOPPED; // 当前状态为STOPPED
mStarted = true; // 动画启动标志位置位
mStartedDelay = false; // 动画未完成延时标志位复位
mPaused = false; // 动画暂停标志位复位
若动画未设定mStartDelay
,还会如下额外操作:
mStartTime = currentTime; // 动画起始时间为当前时间
mStartTimeCommitted = true; // 动画起始时间不可调整
animateValue(fraction); // 计算第一帧动画值
mRunning = true; // 动画运行标志位置位
notifyStartListeners(); // 回调AnimatorListener.onAnimationStart
完成所有启动操作之后,会将该动画加入AnimationHandler.mPendingAnimations
这个等待列表,接着就正式开启AnimationHandler
的定时任务:
animationHandler.start()
-> animationHandler.scheduleAnimation()
-> mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, mAnimate, null);
-> animationHandler.doAnimationFrame(mChoreographer.getFrameTime());
BOSS来袭!!!AnimationHandler.doAnimationFrame
就是动画的Studio,负责动画生命周期的处理,用下面这个流程图来简单描述:
注意,这个流程图跟普通的流程图意义稍微有点区别,那就是整个流程不一定是在一帧内同时完成的,一个动画可能需要跨越多帧才能从START走到END,比如:动画可能延迟了3帧才正式开始,然后做了10帧动画才最后结束。流程清楚了之后,我们来分析下流程中涉及的函数具体的逻辑是怎样的,但分析之前有必要先说明下AnimationHandler
中的保存动画的几个列表:
// 保存start被调用且尚未到达第一帧的动画
protected final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>();
// 保存第一帧已到达且mStartDelay不等于0的动画,等待调用Animator.delayedAnimationFrame
protected final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>();
// 保存延时已完成的动画,等待调用Animator.startAnimation
private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>();
// 保存正在运行的动画,该列表中动画mRunning为true,等待调用Animator.doAnimationFrame
protected final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();
// 保存已完成的动画,等待调用Animator.endAnimation
private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>();
了解了这几个列表之后,我们就可以描述AnimationHandler.doAnimationFrame
做了啥了:
- 若
mPendingAnimations
不为空,则遍历其内所有Animator做如下处理:mStartDelay==0
的Animator,调用Animator.startAnimation
,该函数处理如下:- 调用
Animator.initAnimation
,初始化所有PropertyValuesHolder
- 将该Animator放入
AnimationHandler.mAnimations
- 调用
AnimatorListener.onAnimationStart
- 调用
mStartDelay==0
的Animator,放入AnimationHandler.mDelayedAnims
- 若
mDelayedAnims
不为空,则遍历其内所有Animator做如下处理:- 调用
Animator.delayedAnimationFrame
,判断该动画延时是否完成,若延时完成:mStartTime = mDelayStartTime + mStartDelay
,设定动画开始时间mStartTimeCommitted = true
,禁止调整动画开始时间mPlayingState = RUNNING
,设置动画运行状态为RUNNING- 将该Animator放入
AnimationHandler.mReadyAnims
- 调用
- 若
mReadyAnims
不为空,则遍历其内所有Animator做如下处理:- 调用
Animator.startAnimation
- 设置
Animator.mRunning = true
- 调用
- 若
mAnimations
不为空,则遍历其内所有Animator做如下处理:- 调用
Animator.doAnimationFrame
,这个函数是动画的关键处理函数:- 若
mPlayingState == STOPPED
,则初始化mStartTime
为该动画帧时间 - 调用
animationFrame(long currentTime)
,做如下处理:
- 若
- 调用
boolean animationFrame(long currentTime) {
boolean done = false;
switch (mPlayingState) {
case RUNNING:
case SEEKED:
// 计算当前动画帧相对于上一帧动画而言包含的动画周期数
float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
if (mDuration == 0 && mRepeatCount != INFINITE) {
// 若动画周期为0,则可以直接结束动画
mCurrentIteration = mRepeatCount;
if (!mReversing) {
mPlayingBackwards = false;
}
}
if (fraction >= 1f) {
// 动画周期数大于1的情况下,需对动画进行repeat或者结束动画
if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {
// repeat动画
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
// 调用AnimationListener.onAnimationRepeat
mListeners.get(i).onAnimationRepeat(this);
}
}
if (mRepeatMode == REVERSE) {
mPlayingBackwards = !mPlayingBackwards;
}
// 累加动画repeat次数
mCurrentIteration += (int) fraction;
// 调整fraction至[0.0f,1.0f)
fraction = fraction % 1f;
// 调整动画开始时间
mStartTime += mDuration;
} else {
// 结束动画
done = true;
fraction = Math.min(fraction, 1.0f);
}
}
if (mPlayingBackwards) {
fraction = 1f - fraction;
}
// 计算当前动画帧的动画数据
animateValue(fraction);
break;
}
return done;
}
-
-
animateValue(fraction)
函数如下:
-
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
// 通过PropertyValuesHolder计算当前动画帧的动画值
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
// 调用AnimatorUpdateListener.onAnimationUpdate,
// 在回调中可以通过Animator.getAnimatedValue()获取当前动画帧的数据进行最终的动画处理(如调整Target相应的属性值)
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
- 若
mEndingAnims
不为空,则遍历其内所有Animator做如下处理:- 调用
Animator.endAnimation
,该函数主要做扫尾工作:- 把该动画从
AnimationHandler
的所有列表中清除 - 若未调用过
AnimatorListener.onAnimationStart
,则调用 - 调用
AnimatorListener.onAnimationEnd
- 复位动画所有状态:如
mPlayingState = STOPPED
、mRunning=false
、mStarted = false
等等
- 把该动画从
- 调用
- 若
mAnimations
或mDelayedAnims
不为空,则对下一帧进行定时。
通过上面这个描述应该能比较清楚的理解动画的整个流程了,但是这里其实我有一个疑问尚未解惑:在animationFrame(long currentTime)
计算得到fraction
后,当fraction>=1.0f
时会对迭代次数以及动画开始时间进行调整,代码如下:
// 计算fraction
fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
// 调整迭代次数以及动画开始时间
mCurrentIteration += (int) fraction;
fraction = fraction % 1f;
mStartTime += mDuration;
fraction
的计算及调整、mCurrentIteration
的累积并不难理解,但是对mStartTime
的调整就有点诡异了:无论当前跨越多少个动画周期,动画的起始时间只向前调整一个周期,而我查了发现也没有其他地方会在下一帧动画运算前对mStartTime
再做调整;那么在下一帧动画中计算fraction
时,若上一帧动画跨越的动画周期数大于等于2,则这一次计算得到的fraction
会比正常跨越周期数多,这意味着动画实际的运行时长会比理论上的时长短,当然这仅仅发生在当mDuration < 0.5 * frameInterval
时,其中frameInterval为两帧动画的间隔时长。尽管对于通常仅为16ms的frameInterval
来说,这种情形应该算是小概率事件,但是Android为何要如此处理,我想应该是有一个比较合理的理由,只是我目前尚未理解,希望了解缘由的小伙伴能指点一二。
(2)英年早逝型动画
不是每一个动画都能正常走完自己的动画人生,有些动画可能需要在必要的时候为完成某个绚烂的效果而英勇牺牲。这样的情况通常有两种:cancel
和end
。接下我们就一次分析下这两种情形:
1) Animator.cancel
cancel
只会处理那些正在运行或者等待开始运行的动画,具体的处理逻辑是这样的:
- 若未调用过
AnimatorListener.onAnimationStart
,则调用 - 调用
AnimatorListener.onAnimationCancel
- 调用
Animator.endAnimation
:- 把该动画从
AnimationHandler
的所有列表中清除 - 调用
AnimatorListener.onAnimationEnd
- 复位动画所有状态:如
mPlayingState = STOPPED
、mRunning=false
、mStarted = false
等等
- 把该动画从
从上面的逻辑不难发现,cancel
对回调的处理是比较完整的,但是cancel
被调用之后,动画的动画值会停留在当前帧而不会继续进行计算。
2) Animator.end
end
相对于cancel
来说有两个区别:一个是会处理所有动画;另一个是会计算最末一帧动画值。其具体的处理逻辑如下所示:
- 若动画尚未开始:调用
Animatior.startAnimation
让动画处于正常运行状态 - 计算最后一帧动画的动画值:
animateValue(mPlayingBackwards ? 0f : 1f)
- 结束动画:调用
endAnimation
这两种处理都会导致动画的非正常结束,需要注意的是cancel
会保留当前的动画值,而end
会计算最末帧的动画值。
(3)命运多舛型动画
之前提到过,属性动画相对于插间动画而言更多的体现出了一种播放视频的感觉:可以暂停和恢复、可以调整进度,这样的动画人生是走走停停的,有时候一不小心还会倒退,过得并不那么一帆风顺,是多舛的命运。我们来一一分析一下:
1) pause
当pause
被调用的时候,仅在动画已开始(mStarted==true
)且当前为非暂停状态时才进行以下处理:
- 置位:
mPaused = true
- 调用
AnimatorPauseListener.onAnimationPause
- 复位
mResumed = false
- 清空暂停时间:
mPauseTime = -1
做完这些处理之后,就静静地等风来(等下一帧动画的到来),当风来了之后,delayedAnimationFrame
或doAnimationFrame
被调用,此时若仍然处于暂停状态,就会做如下截击:
if (mPaused) {
if (mPauseTime < 0) {
mPauseTime = frameTime;
}
return false;
}
这样就阻止了动画的正常运行,并记录下来动画暂停的时间,确保恢复之后能让动画调整到暂停之前的动画点正常运行,具体怎么起作用就要看resume这小子的了。
2) resume
当resume
被调用的时候,仅当当前是暂停状态时,会做如下处理:
- 置位:
mResumed = true
- 复位:
mPaused = false
- 调用
AnimatorPauseListener.onAnimationResume
当风再次来临,delayedAnimationFrame
或doAnimationFrame
被调用,此时若处于恢复状态(mResume==true
),就会做如下补偿处理:
// delayedAnimationFrame的处理
if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
mDelayStartTime += (currentTime - mPauseTime);
}
}
// doAnimationFrame的处理
if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
mStartTime += (frameTime - mPauseTime);
mStartTimeCommitted = false;
}
}
这样就让暂停的时间从动画的运行过程中消除,就好像从来没暂停过一样,不带走一片云彩。
3) seek
我想每个人看电影的时候,或多或少会有这样的经历:看到一个特别唯美或者特别感动的画面,总忍不住想N次回放;又或看到一个无聊或烂俗的桥段,总吐槽着直接跳过。而对动画进行seek
就类似于你拖动电影的进度条,这样可以让动画从动画过程中的任意一个时刻开始运行,seek
是通过setCurrentPlayTime(long playTime)
或者setCurrentFraction(float fraction)
来实现的,实际上最终的逻辑都在setCurrentFraction
里面,这家伙主要干了下面这几件事:
- 根据fraction计算并更新
mPlayingBackwards
、mCurrentIteration
、mStartTime
等动画关键元素 - 调用
animateValue(fraction)
计算该fraction对应的动画值 - 若动画为非运行状态(mPlayingState != RUNNING),则设定
mPlayingState = SEEKED
做完这些事,动画的运行状态就被强行调整了,当下一帧动画来临时,则会从强行设定的这个动画时间点继续按正常的动画流程继续运行下去,然而,在这里有一个不可忽视的小细节,那就是当用seek
向前调整的时候会导致mStartTime
先于frameTime
,这样假设还是用mStartTime去调用animationFrame
就会导致animationFrame
中计算得到的fraction为负值,因此细心的程序员们在调用animationFrame
之前做了这样的处理:Math.max(frameTime, mStartTime)
,这样整个世界就清静了,永远不会出现负值。
到这为止,动画的三大运转流程就讲完了,其中有些个处理地比较漂亮的细节可能由于篇幅的问题这里并未提及,希望有心的小伙伴可以再去看看源码,我想会有不少的收获的。
4. 属性动画与View的结合
前面长篇大论了半天,根本没有具体讲到属性动画是怎么在View上起作用的,但其实细致看下来的小伙伴应该很容易推测出怎么用属性动画去实现View的变换:那就是根据计算出来的动画值去修改View的属性,如alpha、x、y、scaleX、scaleY、translationX、translationY等等,这样当View重绘时就会产生作用,随着View连续不断地被重绘,你的眼中就会产生绚烂多彩的动画了。这就说完了,是不是感觉这一节也太精简了,相对于前一节来讲简直让人难以接受。不要慌,我这么滥情的人是舍不得你们难过的,所以我要把ObjectAnimator
这个最常用的家伙祭出来拯救你们受伤的小心脏。
ObjectAnimator
是可以在动画帧计算完成之后直接对Target
属性进行修改的属性动画类型,相对于ValueAnimator
来说更加省心省力,为了造福广大Androider,ObjectAnimator
默默做了不少工作:
- 提供
setTarget
接口,用于设定动画过程中属性修改的主体,值得注意的是,若在动画已启动的情况下修改Taget会导致当前动画被cancel
,然后等待下一次被start
initAnimation
会通过调用所有PropertyValuesHolder
的setupSetterAndGetter
方法实现对Property
的set及get方法的初始化,以方便后续对Target
对应属性值的修改- 通过
setupStartValue
及setupEndValues
对各PropertyValuesHolder
种的首末帧数据的动画值进行初始化 - 新增一个
mAutoCancel
属性,当mAutoCancel==true
时,在start
的过程中会清除AnimationHandler
中对同一Target
及该Target
同一属性进行处理的其他动画 - 在动画的整个过程中,若发现
Target
不再有效(动画这种保存的是Target
的弱引用),则cancel
该动画 - 最后,在
animationValue
函数中,调用PropertyValuesHolder.setAnimatedValue
对Target
的属性进行修改。
以上,就是ObjectAnimator
背着ValueAnimator
额外做的各种“勾当”,顺带再补充个小细节,在Animator中保存PropertyValuesHolder
的是一个数组,而在函数animationValue
中会遍历处理所有的PropertyValuesHolder
,因此一个动画实现多个属性的同时修改是一件非常容易的事。虽然知道了怎么实现一个动画中修改多个属性,但是怎么实现多个动画的组合运行还尚未可知,我们在下一节里揭秘所有内幕。
5. 属性动画的组合运行
举个例子来说明组合运行多个属性动画的意思:我想在平移一个View的同时改变这个View的透明度,平移完成之后我需要放大整个View。看过《Android Animation运行原理详解》这篇文章的同学应该知道插间动画是可以实现组合运行多个动画的,但是其实现上只支持“在某个动画的同时做另外一个动画”这种“playTogether”的模式。然后到了属性动画,我们会惊喜的发现组合动画AnimatorSet
除了可以实现“playTogether”模式(下文中“with”模式与此同义)之外,还支持“before”及“after”模式,这家伙简直是吃了窜天猴了——想上天啊,但是不得不说,我喜欢23333~~
为了支持这些组合模式,AnimatorSet
下了血本,引入了Dependency
以及Node
这两个数据结构,其中Dependency
表示一条依赖规则:
private static class Dependency {
// 规则类型
public int rule;
static final int WITH = 0;
static final int AFTER = 1;
// 规则绑定的主体
public Node node;
}
而Node
用于在AnimatorSet
表征其包含的Animator
,其数据结构如下:
private static class Node implements Cloneable {
// 对应的动画
public Animator animation;
// 该Node包含的规则列表
public ArrayList<Dependency> dependencies = null;
// dependencies的副本,应用在动画运行过程,避免损坏原始数据
public ArrayList<Dependency> tmpDependencies = null;
// 该节点依赖的节点
public ArrayList<Node> nodeDependencies = null;
// 依赖该节点的节点
public ArrayList<Node> nodeDependents = null;
// 该节点对应的动画是否完成
public boolean done = false;
}
而在AnimatorSet
中是这样来存储Node
:
// 利用一个list及map来冗余地存储该AnimatorSet包含的所有Node
private ArrayMap<Animator, Node> mNodeMap = new ArrayMap<Animator, Node>();
private ArrayList<Node> mNodes = new ArrayList<Node>();
// 用于存储按动画运行顺序排好序的所有Node
private ArrayList<Node> mSortedNodes = new ArrayList<Node>();
有了这两个利器之后,我们再来讨论三种组合模式的实现就变得简单了,这三种组合模式都是通过一个叫Builder
的家伙来创建的,可以通过AnimatiorSet.play(Animator)
来创建Builder
,这也说明每一个Builder
都会有一个规则主体Animator,而用这个Builder
创建的规则都是以这个主体Animator为基准的,这也意味着该Builder
下多条规则之间是没有直接必然的关联的,但是规则之间可能会因为主体Animator而产生间接的关系,这个时候应该举个例子来说明下这段抽象的描述,但是举例之前必须先分析三种组合模式的具体实现:
// 根据Animator获取或创建Node
Node node = mNodeMap.get(anim);
if (node == null) {
node = new Node(anim);
mNodeMap.put(anim, node);
mNodes.add(node);
}
// with模式实现
Dependency dependency = new Dependency(mCurrentNode, Dependency.WITH);
node.addDependency(dependency);
// before模式实现
Dependency dependency = new Dependency(mCurrentNode, Dependency.AFTER);
node.addDependency(dependency);
// after模式实现
Dependency dependency = new Dependency(node, Dependency.AFTER);
mCurrentNode.addDependency(dependency);
从实现中可以看出在创建规则的时候实际上我们定义并记录了Node之间的相互关系,同时我们发现由于在Dependency
并未定义“before”类型的规则,因此“before”模式实际是用“after”模式来间接实现的。分析完这三种组合模式的具体实现之后,就可以继续前面的举例了:
AnimatorSet s = new AnimatorSet();
// 下面代码产生的规则并不能确定anim2与anim3的先后关系
s.play(anim1).before(anim2).before(anim3);
// 下面代码产生的规则可间接确定anim2与anim3的先后关系
s.play(anim1).before(anim2).after(anim3);
// 下面代码产生的规则可完全确定anim1、anim2、anim3之间的先后关系
s.play(anim1).before(anim2);
s.play(anim2).before(anim3);
这回应该把之前那段抽象的描述解释清楚了,但是另外一个悬念不知道各位有没有发现:我们在创建规则的时候只是记录了Node
之间的相互关系,但是这种相互关系具体是怎么起作用的尚未可知,真相就蕴藏在AnimatorSet
对其包含的动画的调度过程中,说曹操曹操到,下面我们就来分析AnimatorSet
是怎么管理这么多动画小朋友的,要理清楚其中奥妙,还不得不提到两个特殊的监听器:
1) DependencyListener implements AnimatorListener
DependencyListener
用于具化依赖规则:
public void onAnimationEnd(Animator animation) {
if (mRule == Dependency.AFTER) {
startIfReady(animation);
}
}
public void onAnimationStart(Animator animation) {
if (mRule == Dependency.WITH) {
startIfReady(animation);
}
}
当动画开始或结束时,会分析以动画对应Node
(设为NodeA)为依赖的Node
(设为NodeB),若将NodeA从NodeB的tmpDependencies
中移除之后tmpDependencies
不在包含其他Node
则说明NodeB的启动条件已满足。
2) AnimatorSetListener implements AnimatorListener
AnimatorSetListener
对于整个AnimatorSet
来说仅有一个实例,该实例会被设定到所有被包含的Animator
中去,用于管理AnimatorSet
的回调,如:仅当所有Animator
均结束之后,才调用AnimatorSet
监听器的onAnimationEnd
;确保cancel
时对每一Animator
仅调用一次onAnimationCancel
。
了解了这两个监听器之后,我们就可以以AnimatorSet.start
为切入点一气呵成地理解AnimatorSet
管理所有Animator
的逻辑,当AnimatorSet.start
函数被调用时AnimatorSet
被正式激活:
- 根据
AnimatorSet
参数初始化包含的Animator
:- 禁用所有
Animator
的异步模式 - 若
mDuration >= 0
,则将该mDuration
设定至所有Animator
- 若
mInterpolator != null
,则将该mInterpolator
设定至所有Animator
- 禁用所有
-
调用
sortNodes
函数根据Node
之间的依赖规则确定Node
中动画触发的先后顺序,存储在mSortedNodes
中,具体排序算法如下(这一段引用了源码中的伪代码注释):- All nodes without dependencies become ‘roots’
- while roots list is not null
- for each root r
- add r to sorted list
- remove r as a dependency from any other node
- any nodes with no dependencies are added to the roots list
- for each root r
- 分析
mSortedNodes
dependencies
为空的Node
,作为可直接启动的Node
放入nodesToStart
中- 对于不可直接启动的
Node
,针对其每一条依赖规则创建一个DependencyListener
加入其监听器列表 - 将
AnimatorSetListener
加入所有Node的监听器列表
- 根据该
AnimatorSet
是否需要延时分别处理:- 需要延时:创建一个辅助延时Animator,设定其
mDuration
为AnimatorSet
的延时时长,并在延时Animator结束之后启动nodesToStart
中的所有动画 - 无需延时:直接启动
nodesToStart
中的所有动画
- 需要延时:创建一个辅助延时Animator,设定其
- 调用
AnimatorSet
中设定的监听器的onAnimationStart
- 若
AnimatorSet
不包含任何Animator(即mNodes为空)且无需延时,则直接结束该AnimatorSet
,并调用AnimatorSet
中设定的监听器的onAnimationEnd
上面这一段话请在理解了上文中提到的两个特殊监听器之后再阅读,这样你才能更加清楚的理解为什么在start
函数中这样处理完了之后就可以实现根据Node
之间既定的依赖关系有序的完成所有动画。按常理,应该继续分析下AnimatorSet
其他的一些函数,如:cancel()
、end()
、pause()
、resume()
甚至setTarget(Object target)
,但是由于这些函数原则上只是将调用传递至其包含的Animator
,至于一些小的处理细节也并没有太多值得分析的,因此就留待各位自行探索啦。
整个Animator
模块的分析到这,其实已经算比较完整了,而且码了这么多字已经开始产生逆反心里,但是大纲一开始就立好了,如果这时候放弃总觉得有点蛇尾,所以我今天就死磕自己一回,把属性动画的帧率决定者Choreographer
撸完。
6. 属性动画编舞者
Choreographer
的中文翻译是“编舞者”,我觉得还是很形象的,所以这一节的标题就直接直译了。大家对Choreographer
可能比较陌生,甚至有可能忘了这家伙在哪出现过,所以我先来帮大家回忆下:AnimationHandler
中触发定时任务的代码是这样的:
private void scheduleAnimation() {
if (!mAnimationScheduled) {
// 关键在这 →_→
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, mAnimate, null);
mAnimationScheduled = true;
}
}
这下应该回忆起来了吧!是的,Choreographer
就是任务分发的核心,它决定了动画中帧与帧之间的间隔时长,用人话说就是决定了动画的流畅度。
Choreographer
是线程安全的,其构造函数如下:
private Choreographer(Looper looper) {
// 初始化Handler,用于在创建线程中分发事件
// 事件通常包括MSG_DO_FRAME、MSG_DO_SCHEDULE_VSYNC、MSG_DO_SCHEDULE_CALLBACK三类
mLooper = looper;
mHandler = new FrameHandler(looper);
// 初始化vsync脉冲接收器
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
// 初始化上一帧时间点
mLastFrameTimeNanos = Long.MIN_VALUE;
// 根据屏幕刷新频率计算帧间隔
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
// 创建事件队列
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
这里有两个关键概念:一个是VSYNC
。关于VSYNC
这个概念,可参考VSYNC的生成这篇文章,我们这里可以简单地把他理解成屏幕刷新时的同步信号,而FrameDisplayEventReceiver
则是在收到同步信号时处理一些事件(在Choreographer
中会向FrameHandler
发送一个以FrameDisplayEventReceiver
为callback
的消息,当回调回来的时候调用Choreographer.doFrame
);另一个是CallbackQueue
,这是一个事件队列,目前包含CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT四种类型的事件队列,这个队列是按照事件触发事件排序的优先级队列,以action
+token
作为组合键来判定两个事件是否相等,而通常action
分为Runnable
及FrameCallback
两种,分别由Choreographer.postCallback
和Choreographer.postFrameCallback
向Choreographer
进行委派。
这些概念讲清楚之后,我们就跟着Animator中的调用mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, mAnimate, null)
来感受一番传说中的编舞者,postCallback
最终会调用postCallbackDelayedInternal
来执行具体的逻辑:
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
这段代码首先将事件按callbackType添加至相应的事件队列,然后在指定的时间点(如无延时则直接触发,有延时则通过向FrameHandler
发送MSG_DO_SCHEDULE_CALLBACK
消息来进行延时分发)触发scheduleFrameLocked
。(这里厚颜无耻地打个小广告,在向FrameHandler
发送消息的时候,将消息设置成了异步消息,关于什么是异步消息,参看我之前分享Handler机制的文章Android Handler运行机制Java层源码分析中的分享)
当scheduleFrameLocked
被调用时,做了如下处理:
- 若使用
VSYNC
,则调用scheduleVsyncLocked
等待VSYNC
信号,如上文所述,VSYNC
信号到来时会通过向FrameHandler
发送以FrameDisplayEventReceiver
为callback
的消息来触发doFrame
- 不使用VSYNC,通过向
FrameHandler
发送MSG_DO_FRAME
消息来触发doFrame
,注意这个消息带有延时,而延时的时长为上一帧的时间点加上帧延时sFrameDelay
(默认为10ms)
所以不管是否通过哪种途径,最终的归属都是doFrame(long frameTimeNanos, int frame)
,这里主要干了两件事:
- 调整
frameTimeNanos
:当当前时间与frameTimeNanos
之差大于或等于帧间隔mFrameIntervalNanos
时,调整frameTimeNanos
确保当前时间与frameTimeNanos
之差小于mFrameIntervalNanos
- 调用
doCallbacks(int callbackType, long frameTimeNanos)
依次处理mCallbackQueue
中满足条件的事件,事件队列的处理顺序为CALLBACK_INPUT -> CALLBACK_ANIMATION -> CALLBACK_TRAVERSAL -> CALLBACK_COMMIT
所以事件最终是在doCallbacks(int callbackType, long frameTimeNanos)
中被处理掉的,抛开细节,doCallbacks
就是从callbackType
对应的mCallbackQueue
取出处理事件在frameTimeNanos
的事件,然后调用事件对应action
,实现事件的处理。
Choreographer
对事件的分发处理流程大致就如上所述,整体上跟Handler的感觉挺像,只是因为跟系统帧频率挂在一起而有了不少的特殊性,甚至View的traversal也是通过它进行分发的,建议有兴趣的同学可以去寻根溯源下。
7. 后记
分析Animator的代码对于作为程序员的我来说其实并不难,但是码出这么些字来其实还是有点费劲了,从中午一直干到晚上,差不多八九个小时,但说实话用文字来描述这些东西的时候,会逼迫自己去把之前看代码时忽略的一些小细节也品味了一遍,写完之后会有一种畅通感,就像被打通了任督二脉一样,对Animator的把握变得更加系统和具象。当然,希望这篇分享有给你们带来一些启发,也建议各位多用文字把学到的东西系统地分享出来,相信我,亲测这绝对是件利人利己的事。