Android Animator运行原理详解

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的属性,那就是PropertyValuesHolderPropertyValuesHolder是用来保存某个属性property对应的一组值,这些值对应了一个动画周期中的所有关键帧。其实,动画说到底是由动画帧组成的,Animator可以设定并保存整个动画周期中的关键帧,然后根据这些关键帧计算出动画周期中任一时间点对应的动画帧的动画数据,而每一帧的动画数据里都包含了一个时间点属性fraction以及一个动画值mValue,从而实现根据当前的时间点计算当前的动画值,然后用这个动画值去更新property对应的属性,这也是为什么Animator被称为属性动画的原因,因为它的整个动画过程实际上就是不断计算并更新对象的属性:

// 保存property及其values的数组
PropertyValuesHolder[] mValues;
 HashMap<String, PropertyValuesHolder> mValuesMap;

在这里必须要详细分析下PropertyValuesHolder,因为这关系到后面对动画流程的理解。注意了,我要出大招了:

PropertyValuesHolder 类图

上面这张图详细地描述了PropertyValuesHolder的组成,我知道大家一般是不愿意看这张图的,但是为什么我还要放在这?因为我都画了,不放怎么能显现出我的工作量- -!但是作为一个正义的程序员,我会用语言描述来拯救你们这些小懒虫,PropertyValuesHolderPropertyKeyframes组成,其中Property用于描述属性的特征:如属性名以及属性类型,并提供set及get方法用于获取及设定给定Target的对应属性值;Keyframes由一组关键帧Keyframe组成,每一个关键帧由fraction及value来定量描述,于是Keyframes可以根据给定的fraction定位到两个关键帧,这两个关键帧的fraction组成的区间包含给定的fraction,然后根据定位到的两个关键帧以及设定插间器及求值器就可以计算出给定fraction对应的value。于是,PropertyValuesHolder的整个工作流程也就呼之欲出了,首先通过setObjectValues等函数来初始化关键帧组mKeyframes,必要的情况下(如ObjectAnimator)可以通过setStartValuesetEndValue来设定第一帧及最末帧的value,以上工作只是完成了PropertyValuesHolder的初始化,之后就可以由Animator在绘制动画帧的时候通过fraction来调用calculateValue计算该fraction对应的value(实际上是由mKeyframesgetValue方法做出最终计算),获得对应的value之后,一方面可以通过getAnimatedValue提供给Animator使用,另一方面也可以通过setAnimatedValue方法直接将该值设定到相应Target中去,这样PropertyValuesHolder的职责也就完成了。有了PropertyValuesHolder的鼎力支持之后,动画也就可以开始正常的运转起来了,具体的运转流程又是怎样的咧?且听下节讲解。

3. 属性动画的运行流程

通过上一小节的介绍,我想各位看客应该对动画的组成元素有个基本的了解了,接下来就让我们看看这些基本元素组合在一起之后能诞生怎样的奇妙功效,让我们揭开面纱一睹美人芳颜。
属性动画的运转流程大体可分为三种类型:善始善终型、英年早逝型、命运多舛型,但不管哪种类型首先必须进行以下这些必要的初始化工作:

  • 通过setIntValuessetFloatValuessetObjectValuessetValues初始化PropertyValuesHolder数组
  • 设定mDurationmStartDelaymRepeatCountmRepeatModemInterpolator、各种监听器等动画相关参数

完成动画的初始化工作之后,随着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,负责动画生命周期的处理,用下面这个流程图来简单描述:

AnimationHandler 流程图

注意,这个流程图跟普通的流程图意义稍微有点区别,那就是整个流程不一定是在一帧内同时完成的,一个动画可能需要跨越多帧才能从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)`函数如下:
        ```java
        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 = STOPPEDmRunning=falsemStarted = false等等
  • mAnimationsmDelayedAnims不为空,则对下一帧进行定时。

通过上面这个描述应该能比较清楚的理解动画的整个流程了,但是这里其实我有一个疑问尚未解惑:在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)英年早逝型动画

不是每一个动画都能正常走完自己的动画人生,有些动画可能需要在必要的时候为完成某个绚烂的效果而英勇牺牲。这样的情况通常有两种:cancelend。接下我们就一次分析下这两种情形:
1) Animator.cancel
cancel只会处理那些正在运行或者等待开始运行的动画,具体的处理逻辑是这样的:

  • 若未调用过AnimatorListener.onAnimationStart,则调用
  • 调用AnimatorListener.onAnimationCancel
  • 调用Animator.endAnimation
    • 把该动画从AnimationHandler的所有列表中清除
    • 调用AnimatorListener.onAnimationEnd
    • 复位动画所有状态:如mPlayingState = STOPPEDmRunning=falsemStarted = 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

做完这些处理之后,就静静地等风来(等下一帧动画的到来),当风来了之后,delayedAnimationFramedoAnimationFrame被调用,此时若仍然处于暂停状态,就会做如下截击:

if (mPaused) {
    if (mPauseTime < 0) {
        mPauseTime = frameTime;
    }
    return false;
}

这样就阻止了动画的正常运行,并记录下来动画暂停的时间,确保恢复之后能让动画调整到暂停之前的动画点正常运行,具体怎么起作用就要看resume这小子的了。

2) resume
resume被调用的时候,仅当当前是暂停状态时,会做如下处理:

  • 置位:mResumed = true
  • 复位:mPaused = false
  • 调用AnimatorPauseListener.onAnimationResume

当风再次来临,delayedAnimationFramedoAnimationFrame被调用,此时若处于恢复状态(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计算并更新mPlayingBackwardsmCurrentIterationmStartTime等动画关键元素
  • 调用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会通过调用所有PropertyValuesHoldersetupSetterAndGetter方法实现对Property的set及get方法的初始化,以方便后续对Target对应属性值的修改
  • 通过setupStartValuesetupEndValues对各PropertyValuesHolder种的首末帧数据的动画值进行初始化
  • 新增一个mAutoCancel属性,当mAutoCancel==true时,在start的过程中会清除AnimationHandler中对同一Target及该Target同一属性进行处理的其他动画
  • 在动画的整个过程中,若发现Target不再有效(动画这种保存的是Target的弱引用),则cancel该动画
  • 最后,在animationValue函数中,调用PropertyValuesHolder.setAnimatedValueTarget的属性进行修改。

以上,就是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
  • 分析mSortedNodes

    • dependencies为空的Node,作为可直接启动的Node放入nodesToStart
    • 对于不可直接启动的Node,针对其每一条依赖规则创建一个DependencyListener加入其监听器列表
    • AnimatorSetListener加入所有Node的监听器列表
  • 根据该AnimatorSet是否需要延时分别处理:

    • 需要延时:创建一个辅助延时Animator,设定其mDurationAnimatorSet的延时时长,并在延时Animator结束之后启动nodesToStart中的所有动画
    • 无需延时:直接启动nodesToStart中的所有动画
  • 调用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发送一个以FrameDisplayEventReceivercallback的消息,当回调回来的时候调用Choreographer.doFrame);另一个是CallbackQueue,这是一个事件队列,目前包含CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT四种类型的事件队列,这个队列是按照事件触发事件排序的优先级队列,以action+token作为组合键来判定两个事件是否相等,而通常action分为RunnableFrameCallback两种,分别由Choreographer.postCallbackChoreographer.postFrameCallbackChoreographer进行委派。
这些概念讲清楚之后,我们就跟着Animator中的调用mChoreographer.postCallback来感受一番传说中的编舞者,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发送以FrameDisplayEventReceivercallback的消息来触发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的把握变得更加系统和具象。当然,希望这篇分享有给你们带来一些启发,也建议各位多用文字把学到的东西系统地分享出来,相信我,亲测这绝对是件利人利己的事。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • Animation Animation类是所有动画(scale、alpha、translate、rotate)的基...
    四月一号阅读 1,868评论 0 10
  • 1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今...
    未聞椛洺阅读 2,579评论 0 9
  • Android框架提供了两种类型的动画:View Animation(也称视图动画)和Property Anima...
    RxCode阅读 1,581评论 1 5
  • 提到拆书,就必定要提到赵周老师,要提到拆书帮。 赵周老师写了一本名为《这样读书就够了》的书籍,书中提到了“拆书”、...
    墨竹_sunshine阅读 534评论 4 12
  • 如何做好淘宝SEO?不管新手还是老手做店铺时,都会面临死款这种尴尬的局面,那么该如何才能挽救店铺的死款呢?怎么让它...
    b6f19acf7f09阅读 335评论 0 0