7、TypeEvaluator自定义:ArgbEvaluator颜色渐变、自定义Evaluator实现路径动画、抛物线运动模拟

说实话,TypeEvaluator 这东西,在 Android 动画里是个容易被忽略的角色。很多人用 ValueAnimator 时,只关心 ofInt 和 ofFloat,觉得够用了。但一旦遇到颜色渐变、路径运动这些场景,你就发现默认的 Evaluator 不够用了。

我最早接触 TypeEvaluator,是在做一个登录页面的背景色渐变效果。当时想从蓝色平滑过渡到紫色,结果直接用 ofInt 传了两个颜色值,出来的效果...嗯,怎么说呢,颜色像抽风一样乱跳。后来才意识到,颜色值不能当普通整数处理,得用 ArgbEvaluator。

ArgbEvaluator:颜色渐变的正确姿势

先说说为什么直接用 ofInt 不行。颜色值 #FF0000FF(蓝色)和 #FF800080(紫色),它们的 ARGB 通道是分开的。如果你直接做整数插值,R、G、B 通道会混在一起算,结果就是中间出现一堆脏兮兮的颜色。

ArgbEvaluator 做的事情很简单:把颜色拆成 A、R、G、B 四个通道,分别做线性插值,再拼回去。这样颜色过渡就平滑了。

// 基础用法
val animator = ValueAnimator.ofObject(
    ArgbEvaluator(),
    0xFF0000FF.toInt(),  // 蓝色
    0xFF800080.toInt()   // 紫色
)
animator.duration = 2000
animator.addUpdateListener { animation ->
    val color = animation.animatedValue as Int
    view.setBackgroundColor(color)
}
animator.start()
小技巧:ArgbEvaluator 是 Android 官方提供的,但它的实现其实很简单。你完全可以用它做三色甚至四色渐变,只需要在 Animator 里加多个中间值,配合 Keyframe 使用。

我个人习惯把颜色渐变封装成一个扩展函数,这样用起来顺手:

fun View.animateColor(
    fromColor: Int,
    toColor: Int,
    duration: Long = 1000
) {
    ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor).apply {
        this.duration = duration
        addUpdateListener { animator ->
            setBackgroundColor(animator.animatedValue as Int)
        }
        start()
    }
}

自定义 Evaluator:路径动画的核心

ArgbEvaluator 是官方写好的,但很多时候你需要自己写 Evaluator。比如让一个 View 沿着曲线运动,而不是直线。

ValueAnimator 默认的 ofFloat 只能产生一维数值变化。你想让 View 同时改变 x 和 y,就得用 ofObject,传一个 PointF 或者自己定义的数据类。

我曾在项目中做过一个「飘落花瓣」的效果。花瓣不能直直地掉下来,得有左右摇摆的轨迹。这时候就得自定义 Evaluator。

// 定义一个路径点
data class PathPoint(
    val x: Float,
    val y: Float
)

// 自定义 Evaluator
class PathEvaluator : TypeEvaluator<PathPoint> {
    override fun evaluate(
        fraction: Float,
        startValue: PathPoint,
        endValue: PathPoint
    ): PathPoint {
        // 线性插值
        val x = startValue.x + (endValue.x - startValue.x) * fraction
        val y = startValue.y + (endValue.y - startValue.y) * fraction
        return PathPoint(x, y)
    }
}

// 使用
val animator = ValueAnimator.ofObject(
    PathEvaluator(),
    PathPoint(0f, 0f),
    PathPoint(500f, 800f)
)
animator.addUpdateListener { animation ->
    val point = animation.animatedValue as PathPoint
    view.translationX = point.x
    view.translationY = point.y
}
注意:自定义 Evaluator 时,fraction 参数是 0 到 1 之间的浮点数,代表动画的进度。你可以在 evaluate 方法里做任何数学变换,但一定要保证 startValue 和 endValue 不为 null,否则会崩溃。我曾经在 Evaluator 里忘记判空,结果在低端机型上闪退了一波。

抛物线运动模拟:让动画更真实

线性插值太死板了。真实世界里的运动,很少是匀速直线。比如抛出去的球,它的轨迹是抛物线,x 方向匀速,y 方向有重力加速度。

要实现抛物线,关键是在 Evaluator 里对 x 和 y 用不同的插值策略:

class ParabolaEvaluator(
    private val gravity: Float = 9.8f  // 重力加速度
) : TypeEvaluator<PathPoint> {
    override fun evaluate(
        fraction: Float,
        startValue: PathPoint,
        endValue: PathPoint
    ): PathPoint {
        // x 方向匀速
        val x = startValue.x + (endValue.x - startValue.x) * fraction
        
        // y 方向匀加速(模拟重力)
        val totalY = endValue.y - startValue.y
        val y = startValue.y + totalY * fraction * fraction
        
        return PathPoint(x, y)
    }
}

为什么 y 方向用 fraction * fraction?你想想看,自由落体的位移和时间是平方关系。fraction 代表时间进度,平方之后,y 的变化在前期慢、后期快,看起来就像被重力拉着往下坠。

我做过一个「投篮」的小动画,篮球从球员手里飞到篮筐,用的就是这种抛物线 Evaluator。效果比线性插值自然太多了。

核心思路:自定义 Evaluator 的本质,就是控制每个维度的「变化节奏」。x 和 y 可以用不同的插值公式,甚至可以用贝塞尔曲线、正弦波等。你只需要在 evaluate 方法里写数学公式就行。

更复杂的路径:贝塞尔曲线

抛物线还不够?那就上贝塞尔曲线。Android 的 Path 类自带贝塞尔计算,但要在 Evaluator 里用,得自己算。

class BezierEvaluator(
    private val controlX: Float,
    private val controlY: Float
) : TypeEvaluator<PathPoint> {
    override fun evaluate(
        fraction: Float,
        startValue: PathPoint,
        endValue: PathPoint
    ): PathPoint {
        val oneMinusT = 1 - fraction
        
        // 二阶贝塞尔曲线公式
        val x = oneMinusT * oneMinusT * startValue.x +
                2 * oneMinusT * fraction * controlX +
                fraction * fraction * endValue.x
        
        val y = oneMinusT * oneMinusT * startValue.y +
                2 * oneMinusT * fraction * controlY +
                fraction * fraction * endValue.y
        
        return PathPoint(x, y)
    }
}

用贝塞尔曲线,你可以做出各种弧线、S 形轨迹。控制点决定了曲线的弯曲方向和程度。我习惯把控制点放在起点和终点连线的中垂线上,这样曲线看起来比较自然。

Evaluator 的性能注意事项

Evaluator 的 evaluate 方法在动画的每一帧都会被调用。如果你的动画是 60fps,持续 2 秒,那这个方法会被调用 120 次。所以里面不要做耗时操作。

我曾经犯过一个错误:在 Evaluator 里 new 对象。每次 evaluate 都 new 一个 PathPoint,导致内存抖动,动画卡顿。后来改成复用对象才解决。

// 不好的写法:每次创建新对象
override fun evaluate(...): PathPoint {
    return PathPoint(x, y)  // 每次 new
}

// 好的写法:复用对象
private val temp = PathPoint(0f, 0f)
override fun evaluate(...): PathPoint {
    temp.x = x
    temp.y = y
    return temp
}
经验之谈:如果你的 Evaluator 需要大量计算,可以考虑把计算结果缓存起来。比如贝塞尔曲线的控制点坐标,可以在构造 Evaluator 时算好,不要在 evaluate 里重复算。

知识体系总览

下面这张图总结了 TypeEvaluator 的核心逻辑和本章涉及的知识点:

TypeEvaluator 自定义知识体系 TypeEvaluator ArgbEvaluator 自定义路径 Evaluator 抛物线运动 颜色通道拆分 线性插值 PointF 数据类 贝塞尔曲线 重力加速度模拟 平方时间插值 核心:在 evaluate 方法中控制每个维度的变化节奏 避免在 evaluate 中创建新对象,注意性能优化

说白了,TypeEvaluator 就是给你一个机会,让你在动画的每一帧里「插手」数值的计算。你可以在里面写任何数学公式,只要返回正确的数据类型就行。颜色渐变、路径动画、抛物线运动,本质上都是对 fraction 参数的不同处理方式。

嗯,掌握了这个思路,你就能做出各种看起来很「聪明」的动画效果。下次遇到需要复杂运动的场景,别再用 ofFloat 硬凑了,试试自定义 Evaluator,你会发现新世界。