当数学遇上动画(1)

当数学遇上动画:讲述ValueAnimatorTypeEvaluatorTimeInterpolator之间的恩恩怨怨(1)

其实关于ValueAnimator的内部工作原理大家也都清楚,本文只是选择从数学函数的角度来解析这个原理,方便理解。看完了本节之后我们就更加清楚如何借助TypeEvaluatorTimeInterpolator来帮助我们实现动画等知识。

本系列文章共有三篇,第一篇通过源码解析ValueAnimator类,第二篇通过实例解析TimeInterpolatorTypeEvaluator,第三篇分析常见动画背后的缓动函数,最后引出一个新的Android动画开发的辅助库Yava

1 Android动画基础知识

(1)狭义而言,动画一般就是指某个View组件的某个或者某些属性值在一段时间内不断变化的过程,这个变化过程往往有个起始值、结束值和一系列的中间值,ValueAnimator就是用来反映这个属性值变化过程的重要类,所以本文的介绍主要是以分析ValueAnimator为主。
(2)如果将属性值的变化过程看做一个数学函数的话,从动画效果上来看它是连续的,但实际上它还是离散的,因为它实际上也就是通过插入中间值(简称插值)从而"一帧一帧"完成动画的,那每一帧在哪里取,取多少呢?这也就是ValueAnimator类主要完成的作用。

那到底ValueAnimator是怎么控制属性值的变化过程的呢?答案是借助TimeInterpolatorTypeEvaluator来帮忙!TimeInterpolator用来控制在哪里取,而TypeEvaluator用来控制取多少。(注:取多少个点进行插值是不确定的,例如动画持续时间1s,可能取60,也可能取54、57或者58个中间点进行插值)

先说本小节结论,每一个ValueAnimator其实就是一个的TimeInterpolator和一个TypeEvaluator的结合体。从数学的角度来看,ValueAnimator就是由TimeInterpolatorTypeEvaluator这两个简单函数组合而成的一个复合函数。用图来表述如下:

img

你也可以将TimeInterpolatorTypeEvaluator看作是工厂流水线上的两个小员工,那么ValueAnimator就是车间主管啦。TimeInterpolator这个小员工面对的是产品的半成品,他负责控制半成品输出到下一个生产线的速度。而下一个生产线上的小员工TypeEvaluator的任务就是打磨半成品得到成品,最后将成品输出。

2 结合源码解释函数形式

(1)假设TimeInterpolator是函数x=f(t),t表示动画已经完成的时间比率(例如动画的总时长是10s,已经过了4s了,那么t=0.4),所以t的取值范围是[0,1],0表示动画开始,1表示动画结束。该函数的返回值指的是动画实际插值的时间点,一般是0到1之间,但是也可以小于0("下冲")或者大于1("上冲")。
该函数的作用是把当前时间进度映射成另一个值,这样动画参照的时间由此被"篡改",动画的速度由此被改变。 (后面还有详细介绍)

参考接口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.
     */
    float getInterpolation(float input);
}

(2)假设TypeEvaluator是函数y=g(x,a,b),x就是前面函数f(t)篡改之后的插值的时间点,a、b分别表示属性动画的起始值和结束值。
该函数的作用是通过起始值、结束值以及插值时间点来计算在该时间点的属性值应该是多少。

参考接口TypeEvaluator的定义:

/**
 * Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators
 * allow developers to create animations on arbitrary property types, by allowing them to supply
 * custom evaluators for types that are not automatically understood and used by the animation
 * system.
 */
public interface TypeEvaluator<T> {

    /**
     * This function returns the result of linearly interpolating the start and end values, with
     * <code>fraction</code> representing the proportion between the start and end values. The
     * calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
     * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
     * and <code>t</code> is <code>fraction</code>.
     */
    public T evaluate(float fraction, T startValue, T endValue);
}

(3)假设TimeInterpolatorTypeEvaluator是上面两个简单函数,那么ValueAnimator也是一个函数,它其实就是表示TimeInterpolator的函数x=f(t)和表示TypeEvaluator的函数y=g(x,a,b)结合而成的复合函数F=g(f(t),a,b)

参考ValueAnimatoranimateValue方法的定义:

/**
 * 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.
 */
void animateValue(float fraction) {
    fraction = mInterpolator.getInterpolation(fraction); //TimeInterpolator 函数
    mCurrentFraction = fraction;
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
        mValues[i].calculateValue(fraction); //TypeEvaluator 函数
    }
    if (mUpdateListeners != null) { // 通知监听器
        int numListeners = mUpdateListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            mUpdateListeners.get(i).onAnimationUpdate(this);
        }
    }
}

3 通俗解析各个击破

3.1 关于ValueAnimator

(0)ValueAnimator就是一个的TypeEvaluator和一个TimeInterpolator的结合体,所以该类有两个方法分别用来设置动画的TypeEvaluatorTimeInterpolator
(1)setInterpolator方法可以不调用,默认是加速减速插值器AccelerateDecelerateInterpolator,但是如果调用且传入的参数为null的话,那么就会被设置成线性插值器LinearInterpolator (暂时不清楚为什么要这样做)
(2)setEvaluator方法也可以不调用,默认会根据属性值的类型设置一个IntEvaluator或者FloatEvaluator。后面会讲到这类TypeEvaluator可以看作是线性估值器"LinearTypeEvaluator"并没有这个说法,因故加上引号)。

参考ValueAnimator的部分源码:

//默认的TimeInterpolator是AccelerateDecelerateInterpolator
// The time interpolator to be used if none is set on the animation
private static final TimeInterpolator sDefaultInterpolator = new AccelerateDecelerateInterpolator();

/**
* The time interpolator used in calculating the elapsed fraction of this animation. The
* interpolator determines whether the animation runs with linear or non-linear motion,
* such as acceleration and deceleration. The default value is
* {@link android.view.animation.AccelerateDecelerateInterpolator}
*
* @param value the interpolator to be used by this animation. A value of <code>null</code>
* will result in linear interpolation.
*/
@Override
public void setInterpolator(TimeInterpolator value) {
   if (value != null) {
       mInterpolator = value;
   } else {
       // 如果传入的TimeInterpolator是null的话就设置为LinearInterpolator
       mInterpolator = new LinearInterpolator();
   }
}

/**
 * The type evaluator to be used when calculating the animated values of this animation.
 * The system will automatically assign a float or int evaluator based on the type
 * of <code>startValue</code> and <code>endValue</code> in the constructor. But if these values
 * are not one of these primitive types, or if different evaluation is desired (such as is
 * necessary with int values that represent colors), a custom evaluator needs to be assigned.
 * For example, when running an animation on color values, the {@link ArgbEvaluator}
 * should be used to get correct RGB color interpolation.
 *
 * <p>If this ValueAnimator has only one set of values being animated between, this evaluator
 * will be used for that set. If there are several sets of values being animated, which is
 * the case if PropertyValuesHolder objects were set on the ValueAnimator, then the evaluator
 * is assigned just to the first PropertyValuesHolder object.</p>
 *
 * @param value the evaluator to be used this animation
 */
public void setEvaluator(TypeEvaluator value) {
    if (value != null && mValues != null && mValues.length > 0) {
        mValues[0].setEvaluator(value);
    }
}

(4)调用ValueAnimatorofInt方法时发生了什么
ValueAnimatorofInt方法是创建动画常用的方法,该方法会调用ValueAnimatorsetIntValues,其中调用了PropertyValuesHoldersetIntValues方法,里面又调用了KeyframeSetofInt方法用来得到动画的帧集合,该方法的实现如下:

//根据提供的数字序列得到动画的核心帧集合
public static KeyframeSet ofInt(int... values) {
    int numKeyframes = values.length;//有多少个数字就有多少帧
    IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)];//至少有2帧
    if (numKeyframes == 1) {//如果只传入一个数字,那么该数字就是结束帧的值
        keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);
        keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);
    } else {//如果传入多个数字,那么可以将整个动画时间间隔均分,每个数字按顺序在每个时间比率上占据一个属性值
        keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]);
        for (int i = 1; i < numKeyframes; ++i) {
            keyframes[i] = (IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);
        }
    }
    return new IntKeyframeSet(keyframes);
}

KeyframeofInt方法的签名为Keyframe ofInt(float fraction, int value):前者就是动画已经完成的时间比率,后者是该帧的属性值,它表示在这个特定的时间比率对应的时刻,函数曲线会经过或者非常接近这个属性值(可能是没有经过,而只是很接近很接近,毕竟是曲线拟合嘛)。

上面得到的帧只是动画的几个核心帧,肯定不是动画的全部帧,那中间的其他帧是怎么计算的呢?
这个问题我们可以看下KeyframeSetgetValue方法,方法传入的参数就是动画的时间比率,返回值就是此帧的属性值。

/**
 * Gets the animated value, given the elapsed fraction of the animation (interpolated by the
 * animation's interpolator) and the evaluator used to calculate in-between values. This
 * function maps the input fraction to the appropriate keyframe interval and a fraction
 * between them and returns the interpolated value. Note that the input fraction may fall
 * outside the [0-1] bounds, if the animation's interpolator made that happen (e.g., a
 * spring interpolation that might send the fraction past 1.0). We handle this situation by
 * just using the two keyframes at the appropriate end when the value is outside those bounds.
 */
public Object getValue(float fraction) {
    // Special-case optimization for the common case of only two keyframes
    if (mNumKeyframes == 2) {//1.处理只有2帧的情况
        if (mInterpolator != null) {
            //先调用TimeInterpolator函数
            fraction = mInterpolator.getInterpolation(fraction);
        }
        //再调用TypeEvaluator函数
        return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(), mLastKeyframe.getValue());
    }
    //2.处理上冲和下冲的情况
    if (fraction <= 0f) {
        final Keyframe nextKeyframe = mKeyframes.get(1);
        final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        final float prevFraction = mFirstKeyframe.getFraction();
        float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction);
        return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(), nextKeyframe.getValue());
    } else if (fraction >= 1f) {
        final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2);
        final TimeInterpolator interpolator = mLastKeyframe.getInterpolator();
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        final float prevFraction = prevKeyframe.getFraction();
        float intervalFraction = (fraction - prevFraction) / (mLastKeyframe.getFraction() - prevFraction);
        return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), mLastKeyframe.getValue());
    }
    //3.处理正常的多帧的情况
    Keyframe prevKeyframe = mFirstKeyframe;
    //首先要遍历前面计算出的主要KeyFrame集合,看当前的fraction是处在哪个区间的
    for (int i = 1; i < mNumKeyframes; ++i) {
        Keyframe nextKeyframe = mKeyframes.get(i);
        if (fraction < nextKeyframe.getFraction()) {
            final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
            final float prevFraction = prevKeyframe.getFraction();
            //将当前的fraction折算成在这个区间内的时间比率,这个计算有意思吧
            float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction);
            // Apply interpolator on the proportional duration.
            if (interpolator != null) {
                //先调用TimeInterpolator函数
                intervalFraction = interpolator.getInterpolation(intervalFraction);
            }
            //再调用TypeEvaluator函数
            return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), nextKeyframe.getValue());
        }
        prevKeyframe = nextKeyframe;
    }
    // shouldn't reach here
    return mLastKeyframe.getValue();
}

举个简单的例子,下图中的ValueAnimatorTimeInterpolatorLinearInterpolator,它的TypeEvaluatorIntEvaluator,初始化的时候给定了5个数字,那么核心帧集合中有5帧,此时我们要求当fraction=0.4的时刻的value是多少。

img

3.2 关于TimeInterpolator

我们都知道时间是一秒一秒过去的,也就是线性的,匀速前进的。如果属性值从起始值到结束值是匀速变化的话,那么整个动画看起来就是慢慢地均匀地变化着。但是,如果我们想让动画变得很快或者变得很慢怎么办?答案是我们可以通过“篡改时间”来完成这个任务!这正是TimeInterpolator类的工作,它实际上就是一条函数曲线。

举个栗子!如下图所示,x轴表示时间的比率,y轴表示属性值。在不考虑TypeEvaluator的计算的情况下,假设属性值是从0变化到1,默认情况下线性插值器就和曲线y=x一样,在时间t的位置上的值为f(t)=t,当t=0.5的时刻传给TypeEvaluator的是t=0.5的时刻的值0.5。但是,当我们将TimeInterpolator设置为函数y=x2或者y=x(0.5)时,动画的效果就截然不同啦。在t=0.5的时刻,y=x^2=0.25 < 0.5,表示它将时间推迟了,传给TypeEvaluator的是0.25时刻的值0.25;而y=x^(0.5)=0.71 > 0.5,表示它将时间提前了,传给TypeEvaluator的是0.71时刻的值0.71

此外,仔细观察曲线的斜率不难发现,曲线y=x2的斜率在不断增加,说明变化越来越快,作用在View组件上就是刚开始挺慢的,然后不断加速的效果;而曲线y=x(0.5)的斜率在不断减小,说明变化越来越慢,作用在View组件上就是刚开始挺快的,然后不断减速的效果。

img

推荐在cubic-bezier.com网站中简单绘制和上面两个曲线类似形状的曲线,然后选择线性曲线作为参考,查看方块的运动变化情况。

Android的动画框架中已经给我们提供了不少实现了TimeInterpolator的插值器,包括AccelerateDecelerateInterpolator, AccelerateInterpolator, AnticipateInterpolator, AnticipateOvershootInterpolator, BounceInterpolator, CycleInterpolator, DecelerateInterpolator, LinearInterpolator, OvershootInterpolator, PathInterpolator。

基本上每个插值器其实就对应一条曲线,例如加速减速插值器AccelerateDecelerateInterpolator对应的曲线如下,斜率是先增加后减小。

img

那除了Android系统自带的这些,还有哪些常见的TimeInterpolator呢?

不妨看看这个项目EaseInterpolator,作者实现了30种常见动画的TimeInterpolator,每个TimeInterpolator的曲线形状大致如下图所示 (图片截自easings.net,项目EaseInterpolator实现了其中的30个效果)
这个项目中的Ease[XXX]Interpolator都实现了Interpolator接口,而Interpolator接口继承自TimeInterpolator接口。

img

3.3 关于TypeEvaluator

TypeEvaluator实际上也是一条函数曲线,它的输入是TimeInterpolator传进来的被“篡改”了的时间比率,还有动画的起始值和结束值信息,输出就是动画当前应该更新的属性值。假设TimeInterpolatorLinearInterpolatorf(t)=t),也就是说时间比率不被“篡改”的话,那么ValueAnimator对应的函数其实就简化成了TypeEvaluator函数(F=g(x,a,b)=g(f(t),a,b)=g(t,a,b)),即动画实际上只由TypeEvaluator来控制。

Android系统动画框架中提供了几个TypeEvaluator,例如IntEvaluator、FloatEvaluator、ArgbEvaluator、PointEvaluator、PathEvaluator等

IntEvaluatorevaluate方法的实现:(这类TypeEvaluator就是下一节提到的线性估值器"LinearTypeEvaluator"

public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
    int startInt = startValue;
    return (int)(startInt + fraction * (endValue - startInt));
}

那除了Android系统自带的这些,还有哪些常见的TypeEvaluator呢?

这个时候不妨看看@代码家的经典项目AnimationEasingFunctions,里面实现了28种常见动画的TypeEvaluator,每个TypeEvaluator的曲线形状和上面的截自easings.net的图片一样,代码家实现的动画效果也正是参考自那个项目的效果而想出来的。
项目中的[XXX]Ease[YYY]都继承自BaseEasingMethod,而BaseEasingMethod实现了TypeEvaluator接口。

看到这里的话,机智的你肯定发现了,为什么那些TimeInterpolatorTypeEvaluator的函数曲线形状一样一样的,到底TimeInterpolatorTypeEvaluator是啥关系啊?在实现动画上它们又有啥区别呢?

请继续看下一节

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

推荐阅读更多精彩内容

  • 1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今...
    未聞椛洺阅读 2,688评论 0 10
  • 当数学遇上动画:讲述ValueAnimator、TypeEvaluator和TimeInterpolator之间的...
    javayhu阅读 409评论 0 1
  • 转载一篇高质量博文,原地址请戳这里转载下来方便今后查看。1 背景不能只分析源码呀,分析的同时也要整理归纳基础知识,...
    Elder阅读 1,936评论 0 24
  • 如果搜索一下“大数据”关键字,可以找到无数的趋势、概念文章,然而切合实际工作的内容极少,本文聊一下两点日常困难和思...
    scvhuang阅读 1,281评论 2 6
  • 我给一个博士朋友看电脑里的老房子照片,她在大学里头教数学。幻灯片播过了一轮,几十座辽南地区青砖黑瓦木窗的老房子一一...
    塔林其其格阅读 1,410评论 0 3