Android Animation动画详解

原载于个人博客http://hjhjw1991.github.io, 转载请注明出处. 没错, 我不生产文章, 我只是自己文章的搬运工

说来惭愧, 接触Android这么久可UI和Animation一直是涉足未深的盲点, 今天就来解决Animation这个盲点.

前言

本篇应该会涉及很多demo图片. 在Android开发中经常会碰到各种各样的动画, 有的是为了辅助实现功能, 例如进度条动画和Loading界面, 有的是为了帮助用户理解功能逻辑, 提升用户体验, 例如切入切出动画, 滑动效果, 而有的是纯粹为了看起来酷炫, 比如各种游戏特效. 一图胜千言, 一个动画有时候能起到的作用是用Toast所达不到的, 如果能用好动画, 则无论是开发过程还是成品都能够得到质的提升(没有动画的世界是多么乏味!).

众所周知, Android对界面的绘制速度是有要求的, 正常屏幕刷新频率是60Hz, 也就是60 frame/s, 故每一帧需要在1000ms/60即约16ms内完成, 否则就会造成能够感知到的掉帧, 俗称卡顿, 所以我们在做动画的时候也一定要考虑性能问题, 由动画引起严重卡顿就得不偿失了, 为了Android早就被黑得体无完肤的卡顿问题, 我们还要多多磨练自己的技术.

先抛出几个优秀的动画效果: [由于简书上传图片失败, 这里直接贴出链接]
https://camo.githubusercontent.com/c41223966bdfed2260dbbabbcbae648e5db542c6/687474703a2f2f7777332e73696e61696d672e636e2f6d773639302f3631306463303334677731656a37356d69327737376732306333306a623471722e676966

https://raw.githubusercontent.com/hujiaweibujidao/FabDialogMorph/master/fabdialog.gif

https://github.com/hujiaweibujidao/wava/raw/master/wava.gif

https://github.com/hujiaweibujidao/yava/raw/master/yava.gif

https://github.com/yanbober/SlideLayout/raw/master/art/demo.gif

感谢工匠若水 代码家潇涧
特别感谢代码家的个人感悟博文, 让我知道用心运营一个产品也许没有想象中那样乏味, 我之所以不去做只是不愿意面对可能的失败, 是完全的不自信.

如果不满足于使用动画库, 想像他们一样为开源世界做出贡献, 那么了解代码背后的代码就十分必要. 以上开源库为我们提供了一个很好的切入点, 读者可以挑选一个感兴趣的效果深入探究.
当然也可以跟我一起(在Android动画方面, 我是一个完全的入门者), 跟这篇文章一起, 从底层, 从原理一步一步地走上来.
本文基于Android6.0源码, 从问题出发, 打算用一篇长文解决所有问题, 所以会在小节标题尽量提示本节内容.

入门

不同于理论研究, 程序开发的一切功能都是为实际需求而生的, 我们先来看需求:
最简单的情况, 我想让一段文本横向缓缓移动, 就像跑马灯一样的效果.
再进一步, 一段文本或者图片我希望把它放大和缩小, 但是不能是那种一点没有过渡的一步到位.
抑或我想让一个百变怪的图像变形成皮卡丘.
...
所有这些都是动画要解决的问题. 光凭直觉我们也能分辨出动画是有类别之分的, 比如形变/位移/镜像, 显然动画可以跟数学描述紧密结合. 可以这么说, 任何动画, 只要我们在有限维空间能够找到表达式来表达映射关系, 就可以实现, 这是动画背后坚实的数学基础.

先不扯数学(后面再扯), 来看看一个简单的位移动画, 我们在最上层怎么实现. 最先想到的当然是去找官方文档(以及顺手创建一个示例工程). 在Android官方文档中, 把动画分为两个系统: 属性动画(property anima)和视图动画(view anima). 为了备忘和无网时回顾, 我这里对官方文档做一个简单翻译, 建议大家看原文.

Android系统框架提供以上两种动画系统, 都可以用, 但是一般来说属性动画比较好用, 因为它更灵活功能也更多. 除了这两种动画以外, 还有一种 Drawable Anima(有叫帧动画Frame Anima的, 我不翻译), 它允许你载入一堆drawable资源, 并一帧一帧挨个显示他们.

我们分别来看看这三种动画都是什么东西, 怎么用, 然后来看看我们要实现的效果怎么实现.
以下内容主要参考官方文档.

View Anima / Tween Anima

视图动画(也叫补间动画)允许在一个视图容器里执行一系列简单变换, 它只能用于视图. 顾名思义, 它是以整个视图为对象, 将帧与帧"之间"的过渡给"补齐"的动画. 它通过动画的起点/终点/尺寸/旋转以及其他常见方面来计算动画. 我们可以用一系列的动画指令(anima instruction)来定义补间动画, XML或者Android代码都可以. 官方推荐XML方式定义, 如果要以代码方式定义, 官方建议参考 android.view.animation.AnimationSet及其他的android.view.animation.Animation子类.

本质上, 补间动画就是给出两个关键帧, 通过一些方法计算中间过程, 并按顺序展示中间过程. 补间动画有AlphaAnimation, RotateAnimation, ScaleAnimation, TranslateAnimation这几种, 看名字大概也知道对应的效果了.
我们来看看怎么用. 先看XML官方示例:

<!-- 以下代码用于拉伸和旋转一个View对象 -->
<set android:shareInterpolator="false">
    <scale
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:fromXScale="1.0"
        android:toXScale="1.4"
        android:fromYScale="1.0"
        android:toYScale="0.6"
        android:pivotX="50%"
        android:pivotY="50%"
        android:fillAfter="false"
        android:duration="700" />
    <set android:interpolator="@android:anim/decelerate_interpolator">
        <scale
           android:fromXScale="1.4"
           android:toXScale="0.0"
           android:fromYScale="0.6"
           android:toYScale="0.0"
           android:pivotX="50%"
           android:pivotY="50%"
           android:startOffset="700"
           android:duration="400"
           android:fillBefore="false" />
        <rotate
           android:fromDegrees="0"
           android:toDegrees="-45"
           android:toYScale="0.0"
           android:pivotX="50%"
           android:pivotY="50%"
           android:startOffset="700"
           android:duration="400" />
    </set>
</set>

看标签名不难理解每个动画元素都做了什么以及相互之间的关系.
XML中除了指定关键帧的属性以外, 有一个重要的属性android:interpolator, 该属性绑定一个Interpolator(有译"插值器"), 由它来控制动画转换过程如何完成, 例如AccelerateInterpolator就指示以越来越快的速度来展示动画. 每个Interpolator类都可以被配置在XML中.
特别要指出的是android:pivotX这类属性, 它的值指定的是一个百分比, 但是有带"%"符号和不带符号, 其含义是不同的: 带"%"符号, 表示相对于View自身, 而不带则表示相对于View的父元素.
假设以上XML以hyperspace_jump.xml的名字存在res/anim/目录下, 在代码中可以这样使用:

ImageView spaceshipImage = (ImageView) findViewById(R.id.spaceshipImage); // 要应用动画的View对象
Animation hyperspaceJumpAnimation = AnimationUtils.loadAnimation(this, R.anim.hyperspace_jump); // 使用AnimationUtils载入并实例化动画
spaceshipImage.startAnimation(hyperspaceJumpAnimation); // startAnimation是View提供的方法, 你也可以用View.setAnimation()指定一个已通过Animation.setStartTime()设定了开始时间的定时动画

我们再来看代码如何定义一个动画:

private TranslateAnimation mTranslateAnimation; // 一个位移动画
//这四个参数含义分别是当前View x起点坐标、x终点坐标、y起点坐标、y终点坐标
mTranslateAnimation = new TranslateAnimation(0, 200, 0, 0);
//动画持续时间
mTranslateAnimation.setDuration(2000);
//重复次数
mTranslateAnimation.setRepeatCount(1);
//动画执行模式
mTranslateAnimation.setRepeatMode(Animation.REVERSE);
... // 后面就跟上文XML方式动画实例化之后一样了

四种补间动画的定义和使用都大同小异, 而且可以设置监听器Animation.AnimationListener(), 这里就不赘述.
需要注意的是两点: 1. 动画播放完成后, View会怎么样; 2. 动画指定的边界和View及View的父容器指定的边界发生冲突时, 例如动画放大到超出View父容器的边界, 会怎样.

Drawable Anima/Frame Anima

这种动画我不知道怎么翻译, 主要是Drawable不好翻译. 正如前文所述, 这种动画就是把一个个Drawable或者说Frame按顺序播放, 跟放电影一样. 它的定义和使用也特别简单:

private AnimationDrawable mAnimationDrawable; // 定义帧动画对象
mAnimationDrawable = new AnimationDrawable();
// 添加帧及播放时间
mAnimationDrawable.addFrame(getResources().getDrawable(R.drawable.australia), 500);
mAnimationDrawable.addFrame(getResources().getDrawable(R.drawable.austria), 500);
mAnimationDrawable.addFrame(getResources().getDrawable(R.drawable.china), 500);
mAnimationDrawable.start(); // 开始动画

以下是XML的定义

<!-- 等效XML -->
<animation-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">

    <item android:drawable="@drawable/australia" android:duration="500"/>
    <item android:drawable="@drawable/austria" android:duration="500"/>
    <item android:drawable="@drawable/china" android:duration="500"/>
</animation-list>
<!-- XML中设置 -->
<ImageView
        android:id="@+id/frame_iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@anim/frame"/>
// java中调用
private ImageView mFrameIv;
private AnimationDrawable mAnimationDrawable;
mFrameIv = (ImageView) findViewById(R.id.frame_iv);
mAnimationDrawable = (AnimationDrawable) mFrameIv.getBackground();
mAnimationDrawable.start();

这种动画本身就是在播放Drawable, 所以不能通过setAnimation或者类似的方式应用在别的对象上, 只能以普通图像的方式使用, 例如设置为背景图, 有点类似于gif.

Property Anima

属性动画可以说是系统提供的最强大的动画框架了, 它可以改变任意对象的属性, 只要对象符合一定条件. 难怪官网说它几乎可以动画化一切(allows you to animate almost anything). 它是Android 3.0 (API 11)新加入的成员.
起先, 补间动画和帧动画似乎足够, 然而补间动画只能操作View, 而且不会改边View的属性, 帧动画又太过简单, 要实现复杂的动画效果需要大量的帧, 他们俩似乎并不足以撑起Android的动画天下, 于是谷歌增加了属性动画.

利用属性动画, 你可以指定对象所具有的某项属性随时间的变化情况, 比如, 你可以指定某个对象在300ms内从位置A移动到位置B, 其间分别位于位置AB1,AB2,AB3.
在属性动画中, 你可以指定动画持续时间(Duration), 属性值与执行时间的函数关系(Time interpolation), 重复模式和重复次数, 动画集合, 帧刷新频率等.

Property Anima有两个类可以使用: ValueAnimator, ObjectAnimator, 他们的共同祖先都是Animator.
以ObjectAnimator为例, 一个对象的属性动画可以这样定义:

private ObjectAnimator mObjectAnimator;
//设置不透明度从1到0变化
mObjectAnimator = ObjectAnimator
                .ofFloat(mObjectAnimatorIv, "alpha", 1, 0) // 以 mObjectAnimatorIv对象的float型alpha属性为动画对象, 值从1变到0. "alpha"必须是对象具有getter/setter的属性的名字, 否则无效果
                .setDuration(1000); // 持续时间1000ms
//设置插值器,先加速后减速
mObjectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
//动画重复执行一次
mObjectAnimator.setRepeatCount(1);
//设置执行模式
mObjectAnimator.setRepeatMode(ValueAnimator.REVERSE);
//设置在插值器所指定的每个动画更新时点的监听器
mObjectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        //获取当前的动画值
        float cVal = (Float) animation.getAnimatedValue();
        //根据动画值缩放当前View
        mObjectAnimatorIv.setScaleX(cVal);
        mObjectAnimatorIv.setScaleY(cVal);
    }
});

上面代码的效果是让对象的alpha属性值按一定规律从1变到0, 即逐渐透明, 并通过监听器让对象同时缩小. 其中该目标属性alpha每个时点的具体值是通过TypeEvaluator根据插值器和初末值计算得到的. 不设置TypeEvaluator的情况下Property Animation会有一个默认的TypeEvaluator. 你可以自定义TypeEvaluator.

ValueAnimator与ObjectAnimator类似, 只是它不能直接指定目标属性和更新属性值, 需要在监听器中去设置.
[图片上传失败...(image-816eb0-1517478468814)]

属性动画也可以通过XML来定义, 这里不再贴代码了.

AnimationSet可以将多个动画组合到一起, 还可以设置集合中动画的播放顺序, 这在实现复杂的动画效果时很有用.

至此, 对属性动画的使用就只缺了解其XML属性集合/API这样繁琐而简单的操作.

扩展阅读: KeyframePropertyValuesHolder

解决问题

回顾一下本节开头提出的需求:

  • 想让一段文本横向缓缓移动, 就像跑马灯一样的效果.
  • 一段文本或者图片我希望把它放大和缩小, 但是不能是那种一点没有过渡的一步到位.
  • 想让一个百变怪的图像变形成皮卡丘.

对以上问题我们应该可以给出解决方案了: 1. 位移动画TranslateAnimation; 2. ScaleAnimation; 3. DrawableAnimation.

以上几种动画效果, 我做了一个演示图片:

animation_demo

其他动画

我们还可以为Layout和Activity指定使用的动画资源, 例如Layout可以定义LayoutAnimation, 它类似AnimationSet. Activity可以调用overridePendingTransition指定进入和退出时使用的动画资源.
自从Lollipop之后, Android加入了很多酷炫的动画效果, 例如水滴, 波纹等, 这一部分还有待扩展了解.
看到这里, 想必读者看见上面那些开源动画中"XXXAnimator"这样的名字, 心里也会对它背后的实现有所猜测了. 事实上打开源码一看, 果然如此, 这些开源项目终于看起来没那么神秘了. 我有一个大胆的想法...

深入源码

咱们忽略简单的补间动画和帧动画, 直奔属性动画. 属性动画这么神奇, 它的内部是怎么实现的呢?
再看上文的示例代码:

mObjectAnimator = ObjectAnimator
                .ofFloat(mObjectAnimatorIv, "alpha", 1, 0)
                .setDuration(1000);
                .setInterpolator(new AccelerateDecelerateInterpolator());
                .setRepeatCount(1);
                .setRepeatMode(ValueAnimator.REVERSE);
                .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                        @Override
                        public void onAnimationUpdate(ValueAnimator animation) {
                                float cVal = (Float) animation.getAnimatedValue();
                                mObjectAnimatorIv.setScaleX(cVal);
                                mObjectAnimatorIv.setScaleY(cVal);
                        }
                })
                .start();

这段代码做了什么事情很好理解. 我们考虑一下, 如果是让我们来实现这个ObjectAnimator类, 怎么做?
首先利用建造者模式设置各种属性/插值器/估值器/监听器. 在start()之后, 开启一个计时器线程, 每过一段特定的时间(该时间由插值器计算出), 就让插值器计算当前目标属性应该是什么值, 计算好后通过反射把值赋给目标属性, 然后检查生命周期并调用回调函数. 大致上应该就是这么个思路, 相信源码即便会复杂些, 基本原则也一样.

分步解析ObjectAnimator

源码是android.animation.ObjectAnimator.java我们就不贴大段代码了, 仅贴关键内容.

首先是ofFloat, 用前两个参数实例化ObjectAnimator并用第三个参数调用setFloatValues(), 最后返回实例化的ObjectAnimator. setFloatValues时, 如果mValues为空, 实际是调用PropertyValuesHolder.ofFloat()来得到值. 看PropertyValuesHolder源码得知它用来保存动画期间关键帧及各帧的值, 这好理解, 刚开始的时候初末帧就是关键帧, 初末值就是对应帧的值, 有了这两个再加上插值器就可以计算有多少个帧, 当然目前还只有两个.
一句话总结就是, ofFloat记录了要实施动画的对象及属性, 其中属性, 属性类型及关键帧集合都被封装到一个PropertyValuesHolder对象中, ObjectAnimatormValues数组保存这个对象.

接着是setDuration(),setRepeatCount(),setRepeatMode()我觉得这三个应该差不多就一起看了. 果不其然, 里面直接调用的它的父类ValueAnimator的同名方法, 保存对应的值, 没什么好说的.

接着是setInterpolator. 调用的父类ValueAnimator的函数保存插值器, 默认是LinearInterpolator, 没什么好说的.
我们这里没有设置估值器, 但也一并看了吧. setEvaluator同样调用的父类方法, 往mValues数组第一个元素设置估值器, 调用PropertyValuesHolder.setEvaluator, 保存了估值器.

接着是addUpdateListener, 同样是父类方法, 添加监听器到成员变量ArrayList<> mUpdateListeners中, 类型是AnimatorUpdateListener. 可以料想后面某处会遍历这个列表, 逐个回调.

最后是start. 深呼吸一下, 这个方法显然是重头戏.
贴上完整代码以示尊重:

    // in ObjectAnimator
    @Override
    public void start() {
        // See if any of the current active/pending animators need to be canceled
        AnimationHandler handler = sAnimationHandler.get(); // AnimationHandler是ValueAnimator的隐藏私有静态内部类, 5.0曾一度实现Runnable, 后来改用Runnable匿名内部类, 稍后我们会看它的源码. 在4.0版本它不是隐藏的, 并且是继承自Handler类, 这解释了它的名字. sAnimationHandler类型为 ThreadLocal<AnimationHandler>
        if (handler != null) {
            int numAnims = handler.mAnimations.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
            numAnims = handler.mPendingAnimations.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
            numAnims = handler.mDelayedAnims.size();
            for (int i = numAnims - 1; i >= 0; i--) {
                if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
                    ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
                    if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                        anim.cancel();
                    }
                }
            }
        } // 以上用于判断队列中是否有需要取消的动画, 以下是真正的执行函数
        ... // debug code
        super.start();
    }

    // in ValueAnimator
    @Override
    public void start() {
        start(false); // ObjectAnimator调用start()时, 均是以正向方式播放动画
    }

    /**
     * Start the animation playing. This version of start() takes a boolean flag that indicates
     * whether the animation should play in reverse. The flag is usually false, but may be set
     * to true if called from the reverse() method.
     *
     * <p>The animation started by calling this method will be run on the thread that called
     * this method. This thread should have a Looper on it (a runtime exception will be thrown if
     * this is not the case). Also, if the animation will animate
     * properties of objects in the view hierarchy, then the calling thread should be the UI
     * thread for that view hierarchy.</p>
     *
     * @param playBackwards Whether the ValueAnimator should start playing in reverse.
     */
    private void start(boolean playBackwards) {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        mPlayingBackwards = playBackwards;
        // 倒放时更新mSeekFraction
        if (playBackwards && mSeekFraction != -1) {
            if (mSeekFraction == 0 && mCurrentIteration == 0) {
                // special case: reversing from seek-to-0 should act as if not seeked at all
                mSeekFraction = 0;
            } else if (mRepeatCount == INFINITE) {
                mSeekFraction = 1 - (mSeekFraction % 1);
            } else {
                mSeekFraction = 1 + mRepeatCount - (mCurrentIteration + mSeekFraction);
            }
            mCurrentIteration = (int) mSeekFraction;
            mSeekFraction = mSeekFraction % 1;
        }
        // 判断当前播放方向
        if (mCurrentIteration > 0 && mRepeatMode == REVERSE &&
                (mCurrentIteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
            // if we were seeked to some other iteration in a reversing animator,
            // figure out the correct direction to start playing based on the iteration
            if (playBackwards) {
                mPlayingBackwards = (mCurrentIteration % 2) == 0;
            } else {
                mPlayingBackwards = (mCurrentIteration % 2) != 0;
            }
        }
        // 更新播放状态
        int prevPlayingState = mPlayingState; // 保存上一个状态
        mPlayingState = STOPPED;
        mStarted = true;
        mStartedDelay = false;
        mPaused = false;
        updateScaledDuration(); // in case the scale factor has changed since creation time
        AnimationHandler animationHandler = getOrCreateAnimationHandler();
        animationHandler.mPendingAnimations.add(this); // 当前动画添加进AnimationHandler等待队列
        if (mStartDelay == 0) { // 未设置时默认mStartDelay为0
            // This sets the initial value of the animation, prior to actually starting it running
            if (prevPlayingState != SEEKED) {
                setCurrentPlayTime(0); // 见下文解释
            }
            mPlayingState = STOPPED;
            mRunning = true;
            notifyStartListeners(); // 通知监听器, 回调AnimatorListener.onAnimationStart方法. 注意, 早于动画真正开始
        }
        animationHandler.start(); // 最终调用AnimationHandler.start()
    }

以上有一些地方需要解释一下: mCurrentIteration指当前是第几次播放动画; getOrCreateAnimationHandler顾名思义是尝试从ThreadLocal中获取Handler, 为空的话就创建一个; setCurrentPlayTime()顾名思义是设置当前播放时间, 该时间必须在0到持续时间之间. 其内部调用setCurrentFraction(fraction)设置当前播放的片段, 比如fraction==1.3就表示当前在播放第2次的0.3这部分. 在上述代码中此时fraction==0. 内部最后再调用animateValue将片段动画化, 这个函数稍后我们还会看到. 该函数首先调用fraction = mInterpolator.getInterpolation(fraction)使用插值器获取计算后的当前fraction值, 然后遍历mValues调用calculateValue(fraction)(内部使用mAnimatedValue = mKeyframeSet.getValue(fraction)获取各帧的当前属性值, 估值器就是在这个getValue函数里调用的), 显然这就是在计算此时的属性值了, 紧接着通过PropertyValuesHolder.setAnimatedValue(Object target)以反射的方式把值写回目标属性. 再遍历mUpdateListeners调用onAnimationUpdate(this).

最后来看AnimationHandler.start().

// in ValueAnimator
    final boolean doAnimationFrame(long frameTime) {
        // 更新动画状态
        if (mPlayingState == STOPPED) {
            mPlayingState = RUNNING;
            if (mSeekFraction < 0) {
                mStartTime = frameTime;
            } else {
                long seekTime = (long) (mDuration * mSeekFraction);
                mStartTime = frameTime - seekTime;
                mSeekFraction = -1;
            }
            mStartTimeCommitted = false; // allow start time to be compensated for jank
        }
        if (mPaused) {
            if (mPauseTime < 0) {
                mPauseTime = frameTime;
            }
            return false;
        } else if (mResumed) {
            mResumed = false;
            if (mPauseTime > 0) {
                // Offset by the duration that the animation was paused
                mStartTime += (frameTime - mPauseTime);
                mStartTimeCommitted = false; // allow start time to be compensated for jank
            }
        }
        // The frame time might be before the start time during the first frame of
        // an animation.  The "current time" must always be on or after the start
        // time to avoid animating frames at negative time intervals.  In practice, this
        // is very rare and only happens when seeking backwards.
        final long currentTime = Math.max(frameTime, mStartTime);
        return animationFrame(currentTime); // 真正的动画处理函数
    }

    boolean animationFrame(long currentTime) { // 真正的动画处理函数
        boolean done = false;
        switch (mPlayingState) {
        case RUNNING:
        case SEEKED:
            // 计算当前fraction
            float fraction = mDuration > 0 ? (float)(currentTime - mStartTime) / mDuration : 1f;
            if (mDuration == 0 && mRepeatCount != INFINITE) {
                // Skip to the end
                mCurrentIteration = mRepeatCount;
                if (!mReversing) {
                    mPlayingBackwards = false;
                }
            }
            if (fraction >= 1f) {
                if (mCurrentIteration < mRepeatCount || mRepeatCount == INFINITE) {
                    // Time to repeat
                    if (mListeners != null) {
                        int numListeners = mListeners.size();
                        for (int i = 0; i < numListeners; ++i) {
                            mListeners.get(i).onAnimationRepeat(this); // 回调监听器的onAnimationRepeat()
                        }
                    }
                    if (mRepeatMode == REVERSE) {
                        mPlayingBackwards = !mPlayingBackwards;
                    }
                    mCurrentIteration += (int) fraction;
                    fraction = fraction % 1f;
                    mStartTime += mDuration;
                    // Note: We do not need to update the value of mStartTimeCommitted here
                    // since we just added a duration offset.
                } else {
                    done = true;
                    fraction = Math.min(fraction, 1.0f);
                }
            }
            if (mPlayingBackwards) {
                fraction = 1f - fraction;
            }
            
            // 根据当前fraction将目标属性值动画化, 该函数的分析见上一段落.
            animateValue(fraction);
            break;
        }

        return done;
    }
    /**
     * This custom, static handler handles the timing pulse that is shared by
     * all active animations. This approach ensures that the setting of animation
     * values will happen on the UI thread and that all animations will share
     * the same times for calculating their values, which makes synchronizing
     * animations possible.
     *
     * The handler uses the Choreographer for executing periodic callbacks.
     *
     * @hide
     */
    @SuppressWarnings("unchecked")
    protected static class AnimationHandler {
        // The per-thread list of all active animations
        /** @hide */
        protected final ArrayList<ValueAnimator> mAnimations = new ArrayList<ValueAnimator>();

        // Used in doAnimationFrame() to avoid concurrent modifications of mAnimations
        private final ArrayList<ValueAnimator> mTmpAnimations = new ArrayList<ValueAnimator>();

        // The per-thread set of animations to be started on the next animation frame
        /** @hide */
        protected final ArrayList<ValueAnimator> mPendingAnimations = new ArrayList<ValueAnimator>();

        /**
         * Internal per-thread collections used to avoid set collisions as animations start and end
         * while being processed.
         * @hide
         */
        protected final ArrayList<ValueAnimator> mDelayedAnims = new ArrayList<ValueAnimator>();
        private final ArrayList<ValueAnimator> mEndingAnims = new ArrayList<ValueAnimator>();
        private final ArrayList<ValueAnimator> mReadyAnims = new ArrayList<ValueAnimator>();

        private final Choreographer mChoreographer;
        private boolean mAnimationScheduled;
        private long mLastFrameTime;

        private AnimationHandler() {
            mChoreographer = Choreographer.getInstance();
        }

        /**
         * Start animating on the next frame.
         */
        public void start() {
            scheduleAnimation(); // 入口函数
        }

        void doAnimationFrame(long frameTime) {
            mLastFrameTime = frameTime;

            // mPendingAnimations holds any animations that have requested to be started
            // We're going to clear mPendingAnimations, but starting animation may
            // cause more to be added to the pending list (for example, if one animation
            // starting triggers another starting). So we loop until mPendingAnimations
            // is empty.
            while (mPendingAnimations.size() > 0) { // 是一个宽度优先遍历动画队列
                ArrayList<ValueAnimator> pendingCopy =
                        (ArrayList<ValueAnimator>) mPendingAnimations.clone(); // 拷贝队列
                mPendingAnimations.clear(); // 清空原队列
                int count = pendingCopy.size();
                for (int i = 0; i < count; ++i) {
                    ValueAnimator anim = pendingCopy.get(i);
                    // If the animation has a startDelay, place it on the delayed list
                    if (anim.mStartDelay == 0) {
                        anim.startAnimation(this); // 调用ValueAnimator.startAnimation开始动画, 可能会导致向原等待队列添加动画
                    } else {
                        mDelayedAnims.add(anim); // 添加到延迟动画队列
                    }
                }
            }

            // Next, process animations currently sitting on the delayed queue, adding
            // them to the active animations if they are ready
            int numDelayedAnims = mDelayedAnims.size();
            for (int i = 0; i < numDelayedAnims; ++i) { // 遍历延迟动画队列, 将已到达时点的动画添加进就绪动画队列(注意没有从延迟队列移除)
                ValueAnimator anim = mDelayedAnims.get(i);
                if (anim.delayedAnimationFrame(frameTime)) {
                    mReadyAnims.add(anim);
                }
            }
            int numReadyAnims = mReadyAnims.size();
            if (numReadyAnims > 0) { // 遍历就绪动画队列, 调用ValueAnimator.startAnimation开始动画, 并从延迟动画队列中移除
                for (int i = 0; i < numReadyAnims; ++i) {
                    ValueAnimator anim = mReadyAnims.get(i);
                    anim.startAnimation(this);
                    anim.mRunning = true;
                    mDelayedAnims.remove(anim);
                }
                mReadyAnims.clear();
            }

            // Now process all active animations. The return value from animationFrame()
            // tells the handler whether it should now be ended
            int numAnims = mAnimations.size();
            for (int i = 0; i < numAnims; ++i) {
                mTmpAnimations.add(mAnimations.get(i));
            }
            for (int i = 0; i < numAnims; ++i) { // 遍历动画队列, 调用ValueAnimator.doAnimationFrame, 如动画已结束则添加进结束动画队列
                ValueAnimator anim = mTmpAnimations.get(i);
                if (mAnimations.contains(anim) && anim.doAnimationFrame(frameTime)) {
                    mEndingAnims.add(anim);
                }
            }
            mTmpAnimations.clear();
            if (mEndingAnims.size() > 0) { // 遍历结束动画队列, 回调endAnimation, 然后清空队列.
                for (int i = 0; i < mEndingAnims.size(); ++i) {
                    mEndingAnims.get(i).endAnimation(this);
                }
                mEndingAnims.clear();
            }

            // Schedule final commit for the frame.
            mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, mCommit, null); // 发送mCommit回调. mCommit的定义见下面

            // If there are still active or delayed animations, schedule a future call to
            // onAnimate to process the next frame of the animations.
            // 为还没有处理完的动画和延迟动画安排新一次的处理
            if (!mAnimations.isEmpty() || !mDelayedAnims.isEmpty()) {
                scheduleAnimation();
            }
        }

        void commitAnimationFrame(long frameTime) { // 结束本帧
            final long adjustment = frameTime - mLastFrameTime;
            final int numAnims = mAnimations.size();
            for (int i = 0; i < numAnims; ++i) {
                mAnimations.get(i).commitAnimationFrame(adjustment);
            }
        }

        private void scheduleAnimation() {
            if (!mAnimationScheduled) {
                mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, mAnimate, null);
                mAnimationScheduled = true;
            }
        }

        // Called by the Choreographer.
        final Runnable mAnimate = new Runnable() { // 匿名内部类, 调用真正的动画函数
            @Override
            public void run() {
                mAnimationScheduled = false;
                doAnimationFrame(mChoreographer.getFrameTime()); // 这里调用的是AnimationHandler.doAnimationFrame
            }
        };

        // Called by the Choreographer.
        final Runnable mCommit = new Runnable() { // 匿名内部类, 结束本帧动画后调用
            @Override
            public void run() {
                commitAnimationFrame(mChoreographer.getFrameTime());
            }
        };
    }

调用路径: start -> scheduleAnimation -> Choreographer.postCallback(Choreographer.CALLBACK_ANIMATION, mAnimate, null) -> run -> doAnimationFrame, 中间大概能猜到是一些消息发送和处理机制, 暂时我们按下不看, 细看doAnimationFrame. 传入的参数是mChoreographer.getFrameTime(), 也就是当前帧时间. 在该方法中, 依次处理了 立即执行动画/延迟动画/就绪动画/正在处理的动画/结束动画 这种情况的动画或动画列表, 最后对于还没处理完的动画则发起新的scheduleAnimation调用, 开始新一轮循环.

到此, 整个动画的执行过程就清清楚楚了. 回想一下, 是不是跟我们预先设想的其实差不多?

总结一下, ObjectAnimator动画的工作流程大致如下:

  1. 通过ofXXX实例化一个ObjectAnimator对象, 设置它的目标/属性名/mValues: PropertyValuesHolder[], 设置持续时间和插值器/估值器. PropertyValuesHolder用于保存动画过程每一帧的信息.
  2. 在调用start()开始动画之后, 首先更新当前动画状态, 根据当前执行时间/总持续时间, 得到当前执行的比例(fraction), 把这个fraction交给插值器得到新的fraction, 再给估值器, 计算目标属性的当前值, 并通过反射把这个值赋给目标属性.
  3. 完成赋值之后, 调用AnimationHandler.start开始更新动画. 利用Choreographer发送任务, 在执行任务时对于立即执行的动画, 调用ValueAnimator.doAnimationFrame完成真正的动画操作, 对于延迟动画/就绪动画/已结束动画则更新它们的状态. 如此反复直到所有动画处理完毕.

动画的灵魂: 数学

这里我们讲解动画另一方面重要的内容: 数学.

诚然, 即便我们对数学不是那么精通, 也能够在知道效果的情况下反推出实现效果需要的数学运算, 这应该也是大多数工程师的第一反应. 然而在数学或者说算法基础扎实的情况下, 我们往往能够很快找到更快更好的实现方式, 也能更容易去理解别人代码中的本质和嗅到可以优化的地方. 有时候一些复杂的动画效果, 我们也可以通过数学方法将之分解为简单的部分从而让问题迎刃而解.
(为什么我们这里没有说"算法"? 因为动画是非常具体的形象, 它需要的往往不止"算法", 还要借用"几何"甚至"数论"的思想和工具, 而这些都可以算在数学的范畴内.)

为了让模拟真实世界的动画效果显得更真实, 我们需要知道真实世界中的运动规律. 众多数学家和物理学家, 已经为我们总结出了很多宏观世界的运动规律, 我们这些麻瓜只要活学活用就好.

举个例子, 弹跳效果. 不考虑介质阻力的情况下, 弹跳轨迹与初速度/质量/落点高度/重力加速度等相关, 所以我们如果要让动画中的弹球像真实世界一样弹跳, 就需要定义它的各项属性按照真实世界中弹跳的过程来变化, 而这个变化规律就需要数学描述.
于是我们只要描述各个属性值的时间函数, 就可以知道在任一时点各个属性的值. 这个描述是不是很耳熟? 没错, 这就是插值器Interpolator做的事.
事实上, 插值器也可以看做是对"时间"这一维度做了映射, 前面说过在不设置TypeEvaluator的情况下, 就会直接按照比例分配属性的值了, 相当于是一个"线性映射": x_new = x + x_total_delta * (time_now - time_start) / time_duration. 这么个函数别看简单, 其实是TypeEvaluator(Interpolator())两个函数的复合函数呢.
回到弹跳效果. 我们可以定义一个BounceInterpolator和一个BounceEvaluator, 分别完成对弹球过程中各个时点和各时点下的位移值的描述, 这样就能够实现弹球动画了. 事实上, Android源码中已经为我们预定义了弹跳插值器, 诶, 名字还就叫BounceInterpolator, 感兴趣的同学可以打开源码看看它里面定义的函数.

插值器和估值器, 两个函数复合成一个动画函数, 这就是属性动画的数学本质. 以我们对数学的理解, 当然知道在某些情况下, 这个复合函数可以退化成一个简单函数, 什么情况呢? 插值器或估值器其中一个是完全线性映射, 也就是说, 什么都不做, 喂什么吐什么. 在简单函数时, 从效果上来说, 无论我们用插值器还是估值器, 都可以将描述函数转化为动画效果, 他们两个是等价的. 只是从逻辑上我还是希望大家能将这两种概念分清楚, 这样有助于理解源码中所定义的那些插值器和估值器.

关于从数学角度理解动画, 以及更多的跟动画有关的函数, 潇湘同学的博客有详细阐述.

注意

使用动画时要特别注意动画的生命周期处理, 播放完成的动画应及时clear, 否则容易引起OOM/内存泄漏/视图显示和事件响应不按预计的方式进行等问题.

参考

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