Android属性动画基础:你是否真的了解插值器(TimeInterpolator)

  插值器和估值器是我们可以改变动画更新值的两个切入点,通过自定义插值器和估值器,我们可以随意改变动画更新时值的计算方式以满足我们特定的需求。本文简单介绍属性动画插值器(TimeInterpolator)。在读此文前,如果您还不了解属性动画执行流程,建议您先看一下这篇文章,简单了解一下:Android属性动画基础之流程解析

首先,看一下TimeInterpolator源码:

/**
 * A time interpolator defines the rate of change of an animation. This allows animations
 * to have non-linear motion, such as acceleration and deceleration.
 */
public interface TimeInterpolator {

    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}

  通过接口描述,可以知道,通过插值器可以更改动画的变化速率,其实类似于视频播放,我们可以快放也可以慢放,只不过快放和慢放的速率都是线性的。随着执行时间的流逝,动画不断进行更新,即根据动画执行的时间来更新其所操纵的数值或对象,getInterpolation(float input)方法中的input参数就是与动画当前执行周期内执行时间相关的归一化变量,取值范围[0,1],这点在上一篇文章Android属性动画基础之流程解析有所提及,只是碍于篇幅没有详细介绍,这篇文章会对其做较为详尽的解析。getInterpolation(float input)方法所计算出的数值会直接作为时间因子参与动画更新计算。我们先看一下方法api对input的描述,翻译过来大概是:"input参数取值范围[0,1],表示动画当前所处的节点,0代表动画开始,1代表动画结束"。但是,可但是,但可是,这个描述其实是不严谨的,稍后我们分析input参数的数值计算方式就会知道为何这个描述是不严谨的。
  为了搞清楚上述input参数的计算方式,我们需要知道getInterpolation方法何时被触发,不用想,肯定是计算更新之前被触发的,这简直是废话,其实我们首先需要了解属性动画执行流程(请参考Android属性动画基础之流程解析),这里不做过多阐述,直接看相关代码(如对属性动画流程有疑问,:

1   boolean animateBasedOnTime(long currentTime) {
2       boolean done = false;
3       if (mRunning) {
4           final long scaledDuration = getScaledDuration();
5           final float fraction = scaledDuration > 0 ?
6                   (float)(currentTime - mStartTime) / scaledDuration : 1f;
7           final float lastFraction = mOverallFraction;
8           final boolean newIteration = (int) fraction > (int) lastFraction;
9           final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
10                   (mRepeatCount != INFINITE);
11            if (scaledDuration == 0) {
12                // 0 duration animator, ignore the repeat count and skip to the end
13                done = true;
14            } else if (newIteration && !lastIterationFinished) {
15                // Time to repeat
16                if (mListeners != null) {
17                    int numListeners = mListeners.size();
18                    for (int i = 0; i < numListeners; ++i) {
19                        mListeners.get(i).onAnimationRepeat(this);
20                    }
21                }
22            } else if (lastIterationFinished) {
23                done = true;
24            }
25            mOverallFraction = clampFraction(fraction);
26            float currentIterationFraction = getCurrentIterationFraction(mOverallFraction);
27            animateValue(currentIterationFraction);
28        }
29        return done;
30    }

31    private float clampFraction(float fraction) {
32        if (fraction < 0) {
33            fraction = 0;
34        } else if (mRepeatCount != INFINITE) {
35            fraction = Math.min(fraction, mRepeatCount + 1);
36        }
37        return fraction;
38    }

    /**
     * Calculates the fraction of the current iteration, taking into account whether the animation
     * should be played backwards. E.g. When the animation is played backwards in an iteration,
     * the fraction for that iteration will go from 1f to 0f.
     */
39    private float getCurrentIterationFraction(float fraction) {
40        fraction = clampFraction(fraction);
41        int iteration = getCurrentIteration(fraction);
42        float currentFraction = fraction - iteration;
43        return shouldPlayBackward(iteration) ? 1f - currentFraction : currentFraction;
44    }

    /**
     * Calculates the direction of animation playing (i.e. forward or backward), based on 1)
     * whether the entire animation is being reversed, 2) repeat mode applied to the current
     * iteration.
     */
45    private boolean shouldPlayBackward(int iteration) {
46          // 注意此处条件判断
47        if (iteration > 0 && mRepeatMode == REVERSE &&(iteration < (mRepeatCount + 1) || mRepeatCount == INFINITE)) {
48            // if we were seeked to some other iteration in a reversing animator,
49            // figure out the correct direction to start playing based on the iteration
50            if (mReversing) {
51                return (iteration % 2) == 0;
52            } else {
53                return (iteration % 2) != 0;
54            }
55        } else {
56            return mReversing;
57        }
58    }

    /**
     * This method is called with the elapsed fraction of the animation during every
     * animation frame. This function turns the elapsed fraction into an interpolated fraction
     * and then into an animated value (from the evaluator. The function is called mostly during
     * animation updates, but it is also called when the <code>end()</code>
     * function is called, to set the final value on the property.
     *
     * <p>Overrides of this method must call the superclass to perform the calculation
     * of the animated value.</p>
     *
     * @param fraction The elapsed fraction of the animation.
     */
    @CallSuper
      // 动画更新计算方法
59    void animateValue(float fraction) {
60        fraction = mInterpolator.getInterpolation(fraction);
61        mCurrentFraction = fraction;
62        int numValues = mValues.length;
63        for (int i = 0; i < numValues; ++i) {
              // 动画更新计算
64            mValues[i].calculateValue(fraction);
65        }
66        if (mUpdateListeners != null) {
67            int numListeners = mUpdateListeners.size();
68            for (int i = 0; i < numListeners; ++i) {
69                mUpdateListeners.get(i).onAnimationUpdate(this);
70            }
71        }
72    }

  先看一下animateBasedOnTime(long currentTime)方法第26行,再结合动画计算方法animateValue(float fraction),可以知道,插值器getInterpolation(float fraction)方法接收参数就是第26行计算出来的currentIterationFraction ,下面我们就看看该值是如何计算的。根据源码,很明显我们需要先了解第25行的mOverallFraction和fraction,这俩货在Android属性动画基础之流程解析中真的有做过说明,这里再简单说一下。fraction是当前时间currentTime与动画开始时间mStartTime的差值与动画后期的比值,不考虑边界条件的话,其实就是动画执行的整体时间进度(可能大于1哦,因为您可能会重复执行动画)。那么mOverallFraction是啥呢,它是根据fraction做边界处理之后得到的值,也就是考虑边界条件后的动画整体执行时间进度,假设您设置动画重复执行的次数为n,那么mOverallFraction的最大值为n+1。
  接下来就要看第26行了,mOverallFraction作为参数传入getCurrentIterationFraction(float fraction)方法得到currentIterationFraction,currentIterationFraction又作参数传入插值器getInterpolation(float input)方法,看看getCurrentIterationFraction(float fraction)方法。定位到第41行,首先根据整体执行进度计算出动画的迭代次数(已重复执行的次数)iteration,第42行,整体进度减掉已重复执行次数得到当前执行周期内的时间进度currentFraction(其实就是周期归一化而已,取值范围[0,1]),如果您按照插值器getInterpolation(float input)方法api的描述来理解,那么currentFraction就应该是input参数的接收值,然而并不一定是~,因为input参数接收的值是第43行计算出来的,没办法,看一下shouldPlayBackward(int iteration)方法吧。
  先看第47至第54行代码,只解析方法内使用的参数的意义,第47行的条件判断条件为真时,要求迭代次数即已重复执行的次数大于0;mRepeatMode(控制动画第偶数次执行方式:倒序执行或正序执行,就像影片播放一样,是从头至尾还是从尾至头)为REVERSE;已迭代次数小于等于目标重复次数。mReversing也是一个控制上述"影片"播放顺序的东东,它控制的是当前动画是否要在原来执行顺序的基础上做翻转,该参数可通过reverse()方法更改。通过第43行代码及shouldPlayBackward(int iteration)方法源码,可以很明确的将,插值器getInterpolation(float input)方法所接收的参数值未必是当前动画执行周期内真正的时间进度,当您需要倒序执行动画的时候,input = 1-currentFraction = 1 * (1-currentFraction),正序执行时input = currentFraction。但是,该值的的确确是应该参与动画更新计算的时间因子,这点并没有问题。我们可以以影片播放来举例说明,假设影片片长10s,共100帧,设播放的时间为t,那么正序播放的情况下,应该播放第(int) (10 * t)帧,倒序播放的话应该播放 (int) (100 - 10 * t = 10 * (10 - t)),那么现在你再看看,10 * (10 - t) 与上述1 * (1-currentFraction)有什么区别?其实没区别,非要说有区别,也就是周期是否归一化而已,因为这里的10就是影片周期。
  综上所述,传入插值器getInterpolation(float input)方法中的参数值,就是原本应该参与动画更新计算的时间因子,但是就像影片播放一样,我们想要快放或者慢放,怎么办?很明显,将原本的时间因子"篡改一下"就好了,这就是getInterpolation(float input)所做的事情,该方法根据原本真实的时间因子,计算出一个新的时间因子,然后传入animateValue(float fraction)方法参与最终的计算(见第27行)。比影片快放慢放更强大的是,我们可以随意"篡改",非线性的都可以。
  到此为止,我们应该已经了解插值器的用途,下一篇文章将会介绍估值器(TypeEvaluator)

简单示例gif如下


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

推荐阅读更多精彩内容

  • 1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今...
    未聞椛洺阅读 2,580评论 0 9
  • 【Android 动画】 动画分类补间动画(Tween动画)帧动画(Frame 动画)属性动画(Property ...
    Rtia阅读 5,954评论 1 38
  • 一: 传统 View 动画(Tween/Frame) 1.1 Tween 动画 主要有 4 中:缩放、平移、渐变、...
    dfg_fly阅读 663评论 1 2
  • Android中的动画主要有帧动画、补间动画、属性动画(3.0之后出现),此处按照官方将其分为两大类记录。他们之间...
    KwokKwok阅读 305评论 0 1
  • 很多时候,很迷茫,不知道该去追求什么,是去追求空洞的分数,还是追求大学里虚无缥缈的“高管职位”。 很难说什么是错的...
    Amy雪朵儿阅读 225评论 0 0