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的优势——灵活。

我的小技巧: 如果你需要动画的「中间状态」去做一些额外计算(比如根据进度改变文字颜色),用ValueAnimator比ObjectAnimator方便得多。

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()

我曾经踩过的坑: AnimatorSet嵌套AnimatorSet时,如果子Set设置了重复模式(repeatMode),父Set的监听器可能不会按预期触发。解决方案是尽量在顶层Set设置重复,避免嵌套。

插值器:控制动画的「节奏感」

插值器决定了数值变化的「速度曲线」。系统自带的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();

这样标记点就会沿着一条弧线移动,而不是直线。用户体验瞬间提升一个档次。

知识体系总览

下面这张图是我自己总结的属性动画进阶知识结构,你可以对照着梳理思路:

属性动画进阶 ValueAnimator AnimatorSet 插值器 估值器 数值发生器 灵活控制多属性 监听更新回调 编排动画序列 playTogether/Sequentially 灵活依赖关系 控制速度曲线 自定义getInterpolation() 控制数值映射 自定义evaluate()

避坑指南与最佳实践

做了这么多年动画,我总结了几条经验,分享给你:

  1. 不要在动画回调里做耗时操作——onAnimationUpdate每秒回调几十次,里面哪怕只做一次文件读写,都会卡成PPT。
  2. 记得取消动画——Activity或Fragment销毁时,调用animator.cancel()。我曾经因为忘记取消,导致内存泄漏,排查了半天。
  3. 插值器和估值器要配合使用——插值器控制节奏,估值器控制路径。两者独立,但共同决定最终效果。
  4. 用PropertyValuesHolder优化性能——如果你需要同时动画多个属性,用PropertyValuesHolder比多个ObjectAnimator更高效。
一个小建议: 调试自定义插值器时,可以用Log打印出每个时间点的input和返回值,画成折线图,一眼就能看出曲线是否平滑。这比凭感觉调参数靠谱多了。

属性动画的进阶用法,说白了就是「把控制权拿回自己手里」。系统提供的工具能覆盖80%的场景,但剩下的20%,往往就是决定产品体验好坏的关键。希望今天的内容能帮你把这20%也拿下。


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