8、插值器与估值器协同:TimeInterpolator与TypeEvaluator配合原理、自定义贝塞尔曲线插值器

动画的本质是什么?说白了,就是在一段时间内,让某个值从起点变到终点。但这里有个关键问题——怎么变?是匀速变,还是先快后慢,还是像弹簧一样来回弹?

这就引出了我们今天的主角:插值器(TimeInterpolator)估值器(TypeEvaluator)。这两个家伙是Android动画引擎的「左右护法」,一个管节奏,一个管数值。我刚开始做动画时,总觉得它们是一回事,后来踩了坑才明白——分工完全不同

一句话总结:插值器决定「时间进度→动画进度」的映射关系,估值器决定「动画进度→具体数值」的映射关系。

8.1 它们到底怎么配合的?

先看一张图,你就明白了:

插值器与估值器协同工作流程 时间流逝 t 0% → 100% TimeInterpolator f(t) → 动画进度 如:加速、减速、弹跳 TypeEvaluator g(进度) → 具体值 如:颜色、位置、大小 属性值 详细流程: ① 动画引擎每帧回调 onAnimationUpdate(),传入当前时间 t(0~1 归一化) ② 插值器将 t 映射为动画进度 f(t),例如加速插值器:f(t) = t² ③ 估值器接收 f(t),结合起始值 start 和结束值 end,计算当前属性值 ④ 最终值设置到目标对象的属性上,触发 UI 重绘 关键:插值器只输出 0~1 的进度,不关心具体数值类型    估值器只做数值计算,不关心时间节奏

你看,整个流程非常清晰。我当年第一次看源码时,发现ValueAnimator内部就是这么干的——先调插值器,再调估值器,各司其职

8.2 源码层面看配合

咱们直接看ValueAnimator的核心逻辑,其实就几行关键代码:

// ValueAnimator 内部核心逻辑(简化版)
void animateValue(float fraction) {
    // 第一步:插值器处理时间进度
    float interpolatedFraction = mInterpolator.getInterpolation(fraction);
    
    // 第二步:估值器计算具体数值
    mCurrentValue = mEvaluator.evaluate(interpolatedFraction, 
                                         mStartValue, mEndValue);
    
    // 第三步:通知监听器
    onAnimationUpdate(mCurrentValue);
}

嗯,这里要注意:fraction是引擎传进来的时间进度(0~1),但经过插值器后,interpolatedFraction可能就不是线性了。比如你用了AccelerateInterpolator,那interpolatedFraction在前期会小于fraction,后期才追上来。

我的习惯:调试动画卡顿时,我会在插值器里加日志,看 fractioninterpolatedFraction 的差值。如果差值太大,说明动画节奏有问题,容易造成视觉上的「突兀感」。

8.3 系统自带的插值器与估值器

Android 给我们准备了不少现成的工具,我列个表方便你查阅:

类型 类名 效果 适用场景
插值器 LinearInterpolator 匀速 机械运动、进度条
AccelerateInterpolator 先慢后快 物体掉落、弹出
DecelerateInterpolator 先快后慢 物体停止、淡入
BounceInterpolator 末尾弹跳 消息提示、小球落地
估值器 IntEvaluator 整数线性插值 位置、大小
FloatEvaluator 浮点数线性插值 透明度、旋转角度
ArgbEvaluator 颜色平滑过渡 背景色、文字色变化

你想想看,如果只是做简单的位移动画,用LinearInterpolatorFloatEvaluator就够了。但如果你想做那种「卡片飞入时带点弹性效果」,就得自己动手了。

8.4 自定义贝塞尔曲线插值器

说到自定义插值器,我最常用的就是贝塞尔曲线。为什么?因为它的控制点可以精确调节「加速-减速」的节奏,比系统自带的灵活太多。

我在项目中做过一个「下拉刷新」的动画,要求头部先快速弹出一段,然后慢悠悠地归位。用系统插值器怎么调都不对味,最后用三阶贝塞尔曲线搞定了。

先看代码:

class BezierInterpolator(
    private val controlX1: Float = 0.25f,
    private val controlY1: Float = 0.1f,
    private val controlX2: Float = 0.25f,
    private val controlY2: Float = 1.0f
) : TimeInterpolator {

    override fun getInterpolation(input: Float): Float {
        // 使用二分法逼近贝塞尔曲线上的 t 值
        var t = 0.5f
        var low = 0.0f
        var high = 1.0f
        
        // 迭代 10 次,精度足够
        for (i in 0 until 10) {
            val x = cubicBezierX(t, controlX1, controlX2)
            if (x < input) {
                low = t
            } else {
                high = t
            }
            t = (low + high) / 2
        }
        
        // 用逼近的 t 计算 y 值
        return cubicBezierY(t, controlY1, controlY2)
    }

    private fun cubicBezierX(t: Float, c1: Float, c2: Float): Float {
        return 3 * (1 - t) * (1 - t) * t * c1 +
               3 * (1 - t) * t * t * c2 +
               t * t * t
    }

    private fun cubicBezierY(t: Float, c1: Float, c2: Float): Float {
        return 3 * (1 - t) * (1 - t) * t * c1 +
               3 * (1 - t) * t * t * c2 +
               t * t * t
    }
}
我曾经踩过的坑:一开始我直接用 input 作为 t 值去算 y,结果动画完全不对。后来才意识到——贝塞尔曲线的 x 和 y 是参数方程,input 是 x 轴上的进度,必须先反解出 t,再用 t 算 y。这个「反解」步骤不能省!

使用起来也很简单:

// 使用贝塞尔插值器,模拟「先快后慢再快」的效果
val interpolator = BezierInterpolator(
    controlX1 = 0.42f,  // 控制点1 x
    controlY1 = 0.0f,   // 控制点1 y
    controlX2 = 0.58f,  // 控制点2 x
    controlY2 = 1.0f    // 控制点2 y
)

val animator = ValueAnimator.ofFloat(0f, 1f).apply {
    this.interpolator = interpolator
    duration = 300
    addUpdateListener { animator ->
        val value = animator.animatedValue as Float
        // 更新 UI
        view.translationX = value * 200
    }
}

你可能会问:控制点怎么选?我一般用这个经验:

  • 先快后慢:控制点1靠近(0,0),控制点2靠近(1,0) — 比如 (0.25, 0.1, 0.25, 1.0)
  • 先慢后快:控制点1靠近(0,1),控制点2靠近(1,1) — 比如 (0.0, 0.0, 0.58, 1.0)
  • 弹性效果:控制点1的y大于1,或者控制点2的y小于0 — 比如 (0.25, 1.5, 0.75, -0.5)

8.5 自定义估值器:不只是线性

说完了插值器,再聊聊估值器。系统自带的FloatEvaluatorIntEvaluator都是线性插值,但有时候我们需要非线性——比如颜色变化时,想让红色先变、蓝色后变。

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

class SpringEvaluator(private val stiffness: Float = 0.5f) : TypeEvaluator<Float> {
    override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
        // 在标准线性插值基础上,加入弹簧阻尼效果
        val linear = startValue + (endValue - startValue) * fraction
        val spring = Math.sin(fraction * Math.PI * 4).toFloat() * 
                     (1 - fraction) * stiffness * (endValue - startValue)
        return linear + spring
    }
}

这个估值器会在线性插值的基础上叠加一个正弦波衰减,模拟出「弹簧回弹」的效果。配合DecelerateInterpolator使用,效果更自然。

我的建议:自定义估值器时,一定要保证 evaluate(0, start, end) == startevaluate(1, start, end) == end。否则动画结束位置不对,UI 会「跳一下」。

8.6 插值器与估值器的协同调优

最后分享一个实战经验。有一次我做「点赞动画」,要求心形图标先放大再缩小,最后弹跳两下。我试了各种组合:

  1. AccelerateDecelerateInterpolator + FloatEvaluator:太平淡,没有「弹」的感觉
  2. BounceInterpolator + FloatEvaluator:弹得太夸张,像皮球
  3. 自定义贝塞尔插值器 + SpringEvaluator:完美!既有加速减速,又有轻微回弹

说白了,插值器控制「节奏感」估值器控制「数值变化曲线」。两者配合得好,动画才有「灵魂」。我现在的习惯是:先确定插值器(决定动画的「性格」),再微调估值器(决定动画的「细节」)。

嗯,这一章的内容就到这。记住一句话:插值器是「什么时候变」,估值器是「变成什么」。搞懂这个,你就能随心所欲地控制动画了。


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