4、View动画之插值器与估值器:系统插值器详解、自定义插值器、TypeEvaluator的使用

说到View动画,有两个核心概念你绕不开——插值器和估值器。很多新手容易把它们搞混,其实说白了:插值器决定动画变化的速率,估值器决定动画变化的具体数值。我刚开始学的时候也迷糊过一阵,后来在项目里踩了几个坑,才算真正搞明白。

插值器是什么?

插值器(Interpolator),它的任务很简单:根据时间流逝的百分比,计算出当前属性值改变的百分比

举个例子。假设动画从0到1,持续1000ms。如果不用插值器,默认是线性匀速——每过100ms,值增加0.1。但如果你用了加速插值器,前200ms可能只走到0.1,后200ms却冲到0.5。这就是插值器在起作用。

我个人习惯把插值器理解为「节奏控制器」。它不关心你最终要变成什么值,只关心「时间进度」和「数值进度」之间的映射关系。

核心公式:
插值器输入:时间百分比 t (0~1)
插值器输出:数值百分比 f(t) (0~1)
估值器拿到 f(t) 后,再算出具体数值

系统自带的插值器

Android系统给我们准备了不少现成的插值器。我整理了一张表,方便你对照着看:

插值器类名 XML资源ID 效果描述
LinearInterpolator @android:anim/linear_interpolator 匀速运动,最基础
AccelerateInterpolator @android:anim/accelerate_interpolator 先慢后快,像起步加速
DecelerateInterpolator @android:anim/decelerate_interpolator 先快后慢,像刹车减速
AccelerateDecelerateInterpolator @android:anim/accelerate_decelerate_interpolator 两头慢中间快,像过山车
BounceInterpolator @android:anim/bounce_interpolator 终点处弹跳几下
OvershootInterpolator @android:anim/overshoot_interpolator 冲过终点再回弹
AnticipateInterpolator @android:anim/anticipate_interpolator 先后退再前进
AnticipateOvershootInterpolator @android:anim/anticipate_overshoot_interpolator 先后退,再冲过终点回弹
CycleInterpolator @android:anim/cycle_interpolator 正弦曲线循环运动

这些插值器怎么用?在XML动画文件里直接指定就行:

<scale
    android:fromXScale="1.0"
    android:toXScale="1.5"
    android:fromYScale="1.0"
    android:toYScale="1.5"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="1000"
    android:interpolator="@android:anim/bounce_interpolator" />

代码里设置也简单:

Animation anim = AnimationUtils.loadAnimation(context, R.anim.scale_anim);
anim.setInterpolator(new BounceInterpolator());
view.startAnimation(anim);
小技巧: 我个人习惯在代码里设置插值器,因为可以动态切换。比如用户点击按钮时用BounceInterpolator,滑动时用DecelerateInterpolator,灵活很多。

自定义插值器

系统提供的插值器虽然多,但总有不够用的时候。我记得有个项目要做「弹簧松手回弹」的效果,系统自带的都不太像。这时候就得自己动手了。

自定义插值器只需要实现 Interpolator 接口,重写 getInterpolation(float input) 方法。input是时间进度(0~1),返回值是数值进度(0~1)。

来看一个我实际用过的「弹性插值器」:

public class SpringInterpolator implements Interpolator {
    private float tension;

    public SpringInterpolator(float tension) {
        this.tension = tension;
    }

    @Override
    public float getInterpolation(float input) {
        // 模拟弹簧阻尼振荡
        return (float) (1 - Math.exp(-tension * input) * 
               Math.cos(10 * input * Math.PI));
    }
}

这个插值器的效果是:物体到达终点后会轻微振荡几次,然后稳定下来。tension值越大,振荡衰减越快。

注意: 自定义插值器的返回值可以超出0~1范围。比如OvershootInterpolator就会返回大于1的值。但你要确保估值器能处理这种情况,否则动画会出问题。我曾经就因为这个踩过坑——估值器里没做边界判断,结果动画跑到一半卡住了。

估值器:TypeEvaluator

插值器算出了数值进度,接下来估值器就要把这个进度转换成具体的属性值。

比如一个View从x=0移动到x=100,插值器告诉你现在走了60%,估值器就要算出具体数值:0 + (100-0) * 60% = 60。

系统自带的估值器有:

  • IntEvaluator:计算int类型值
  • FloatEvaluator:计算float类型值
  • ArgbEvaluator:计算颜色值(ARGB)
  • RectEvaluator:计算矩形区域

但如果你要动画的对象不是这些基本类型,就得自定义估值器了。

自定义TypeEvaluator

我记得有个需求:让一个点沿着贝塞尔曲线运动。系统没有现成的估值器,只能自己写。

自定义估值器需要实现 TypeEvaluator<T> 接口,重写 evaluate(float fraction, T startValue, T endValue) 方法。

来看一个自定义Point估值器的例子:

public class PointEvaluator implements TypeEvaluator<Point> {
    @Override
    public Point evaluate(float fraction, Point startValue, Point endValue) {
        float x = startValue.x + (endValue.x - startValue.x) * fraction;
        float y = startValue.y + (endValue.y - startValue.y) * fraction;
        return new Point((int) x, (int) y);
    }
}

使用的时候:

ObjectAnimator anim = ObjectAnimator.ofObject(
    view,
    "position",
    new PointEvaluator(),
    new Point(0, 0),
    new Point(300, 500)
);
anim.setDuration(2000);
anim.start();
提示: 估值器的fraction参数,其实就是插值器算出来的数值进度。所以如果你用了自定义插值器,估值器里拿到的fraction可能不是线性变化的。这一点要心里有数。

插值器与估值器的协作流程

为了让你更直观地理解它们的关系,我画了一张流程图:

插值器与估值器协作流程 动画开始 时间 t = 0 插值器 输入: 时间进度 t 输出: 数值进度 f(t) 估值器 输入: f(t), 起始值, 结束值 输出: 当前属性值 更新View属性 View重绘,显示新位置 循环直到 t=1 每一帧动画都会重复这个流程,直到时间进度 t 达到 1.0

流程其实很简单:

  1. 动画引擎计算当前时间进度 t(0~1)
  2. 把 t 交给插值器,得到数值进度 f(t)
  3. 把 f(t) 和起始值、结束值交给估值器,算出具体属性值
  4. 更新View的属性,触发重绘
  5. 重复以上步骤,直到动画结束

实际项目中的选择建议

说了这么多,到底什么时候用哪个?我根据经验总结了几条:

  • 普通UI动效:AccelerateDecelerateInterpolator 最自然,大部分场景都适用
  • 列表条目入场:DecelerateInterpolator 配合一点延迟,效果很顺滑
  • 按钮点击反馈:OvershootInterpolator 或 BounceInterpolator 能增加趣味性
  • 物理模拟:必须自定义插值器,系统提供的都不够真实
  • 颜色渐变:用 ArgbEvaluator,别自己写颜色插值,容易出问题
避坑指南: 我曾经在RecyclerView的Item动画里用了CycleInterpolator,结果列表滑动时卡成PPT。后来发现CycleInterpolator会让动画来回振荡,频繁触发重绘。如果你的动画需要循环,记得控制好周期数和振幅,别让View来回跑太多次。

嗯,插值器和估值器这部分内容就这些。理解它们的工作原理,你就能掌控View动画的每一个细节。后面讲到属性动画和Compose动画时,你会发现这些概念依然在底层发挥作用——只是换了个更优雅的写法而已。


公众号:蓝海资料掘金营,微信 deep3321