31、属性动画进阶:ValueAnimator、AnimatorSet、插值器与估值器自定义
属性动画,说白了就是让View的某个属性值,在一段时间内从A变成B。你想想看,如果只是让按钮从透明变到不透明,或者从左边滑到右边,用系统自带的就够了。但真实项目里,哪有这么简单?
我记得有一次做电商App的秒杀倒计时,产品要求数字翻转时带弹性效果,而且还要和背景色渐变同步。嗯,这时候如果只用ObjectAnimator,代码会写得非常痛苦。所以今天我们就来聊聊属性动画的进阶玩法——ValueAnimator、AnimatorSet,以及自定义插值器和估值器。
ValueAnimator:最底层的动画引擎
很多人一上来就用ObjectAnimator,其实它只是ValueAnimator的封装。ValueAnimator本身不操作任何View,它只负责计算数值变化。你可以把它理解成一个「数值发生器」。
我个人习惯在需要同时控制多个属性,或者动画逻辑比较复杂时,直接用ValueAnimator。比如下面这个例子:
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
// 这里你可以自己决定怎么用这个value
view.setAlpha(value);
view.setTranslationX(value * 200);
view.setScaleX(0.5f + value * 0.5f);
}
});
animator.start();
你看,一个ValueAnimator就能同时控制透明度、位移和缩放。如果用ObjectAnimator,你得写三个动画对象,还要考虑同步问题。这就是ValueAnimator的优势——灵活。
AnimatorSet:让动画按你的节奏跑
实际项目中,很少有动画是独立运行的。更多时候是「先放大,再旋转,同时改变背景色」。这时候就需要AnimatorSet来编排动画序列。
AnimatorSet支持三种基本编排方式:
- playTogether():多个动画同时播放
- playSequentially():多个动画按顺序播放
- play().with() / before() / after():更灵活的编排
我曾经在做一个引导页动画时,需要让Logo先弹入,然后标题淡入,同时按钮从底部滑入。用AnimatorSet写出来是这样的:
ObjectAnimator logoAnim = ObjectAnimator.ofFloat(logoView, "scaleX", 0f, 1f);
ObjectAnimator titleAnim = ObjectAnimator.ofFloat(titleView, "alpha", 0f, 1f);
ObjectAnimator btnAnim = ObjectAnimator.ofFloat(btnView, "translationY", 200f, 0f);
AnimatorSet set = new AnimatorSet();
set.play(logoAnim).before(titleAnim);
set.play(titleAnim).with(btnAnim);
set.setDuration(500);
set.start();
这里要注意一个坑:play().before() 和 play().after() 的语义是「动画A在动画B之前/之后开始」,而不是「等A结束再开始B」。如果你需要严格的前后顺序,建议用 playSequentially()。
插值器:控制动画的「节奏感」
插值器决定了数值变化的「速度曲线」。系统自带的LinearInterpolator、AccelerateDecelerateInterpolator已经能满足大部分场景。但如果你想要那种「弹一下再停」的效果,就得自己写了。
自定义插值器只需要实现Interpolator接口,重写 getInterpolation(float input) 方法。input是从0到1的线性值,返回值是经过「扭曲」后的进度。
举个例子,我做过一个「果冻弹跳」效果:
public class BounceInterpolator implements Interpolator {
@Override
public float getInterpolation(float t) {
// 模拟弹簧阻尼效果
return (float) (1 - Math.exp(-4 * t) * Math.cos(10 * t));
}
}
这个公式看起来有点数学,但效果很直观——数值会先超过目标值,然后弹回来,再超过,逐渐衰减到目标值。用在按钮点击反馈上,用户会觉得「很有质感」。
估值器:自定义「数值变化路径」
估值器(TypeEvaluator)负责把插值器算出来的进度(0~1)转换成具体的属性值。系统提供了IntEvaluator、FloatEvaluator、ArgbEvaluator。但如果你要动画一个自定义对象,比如PointF或者Rect,就得自己写估值器了。
我记得在做一个地图标记动画时,需要让标记点沿着贝塞尔曲线移动。这时候系统自带的线性估值器就不够用了。我写了一个贝塞尔估值器:
public class BezierEvaluator implements TypeEvaluator<PointF> {
private PointF controlPoint;
public BezierEvaluator(PointF controlPoint) {
this.controlPoint = controlPoint;
}
@Override
public PointF evaluate(float t, PointF start, PointF end) {
// 二阶贝塞尔曲线公式
float x = (1 - t) * (1 - t) * start.x + 2 * t * (1 - t) * controlPoint.x + t * t * end.x;
float y = (1 - t) * (1 - t) * start.y + 2 * t * (1 - t) * controlPoint.y + t * t * end.y;
return new PointF(x, y);
}
}
使用时配合ValueAnimator:
ValueAnimator anim = ValueAnimator.ofObject(
new BezierEvaluator(new PointF(300, 0)),
new PointF(0, 0), new PointF(600, 0)
);
anim.addUpdateListener(animation -> {
PointF point = (PointF) animation.getAnimatedValue();
marker.setTranslationX(point.x);
marker.setTranslationY(point.y);
});
anim.start();
这样标记点就会沿着一条弧线移动,而不是直线。用户体验瞬间提升一个档次。
知识体系总览
下面这张图是我自己总结的属性动画进阶知识结构,你可以对照着梳理思路:
避坑指南与最佳实践
做了这么多年动画,我总结了几条经验,分享给你:
- 不要在动画回调里做耗时操作——onAnimationUpdate每秒回调几十次,里面哪怕只做一次文件读写,都会卡成PPT。
- 记得取消动画——Activity或Fragment销毁时,调用animator.cancel()。我曾经因为忘记取消,导致内存泄漏,排查了半天。
- 插值器和估值器要配合使用——插值器控制节奏,估值器控制路径。两者独立,但共同决定最终效果。
- 用PropertyValuesHolder优化性能——如果你需要同时动画多个属性,用PropertyValuesHolder比多个ObjectAnimator更高效。
属性动画的进阶用法,说白了就是「把控制权拿回自己手里」。系统提供的工具能覆盖80%的场景,但剩下的20%,往往就是决定产品体验好坏的关键。希望今天的内容能帮你把这20%也拿下。
公众号:蓝海资料掘金营,微信deep3321