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()
我个人习惯把颜色渐变封装成一个扩展函数,这样用起来顺手:
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
}
抛物线运动模拟:让动画更真实
线性插值太死板了。真实世界里的运动,很少是匀速直线。比如抛出去的球,它的轨迹是抛物线,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。效果比线性插值自然太多了。
更复杂的路径:贝塞尔曲线
抛物线还不够?那就上贝塞尔曲线。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
}
知识体系总览
下面这张图总结了 TypeEvaluator 的核心逻辑和本章涉及的知识点:
说白了,TypeEvaluator 就是给你一个机会,让你在动画的每一帧里「插手」数值的计算。你可以在里面写任何数学公式,只要返回正确的数据类型就行。颜色渐变、路径动画、抛物线运动,本质上都是对 fraction 参数的不同处理方式。
嗯,掌握了这个思路,你就能做出各种看起来很「聪明」的动画效果。下次遇到需要复杂运动的场景,别再用 ofFloat 硬凑了,试试自定义 Evaluator,你会发现新世界。