头图

前言

在早期,Android 提供了逐帧动画(Frame Animation)和补间动画(Tween Animation)两种动画方式,这两种动画方式能够满足大部分基础动画需求。然而,随着开发需求的不断变化,Android 于 3.0 版本推出了属性动画,成为更强大、更灵活的动画框架,并沿用至今。

在进行属性动画的开发中, ObjectAnimator 是最常用到的类,它可以直接对任意对象的任意属性进行动画操作。不过虽说 ObjectAnimator 更加常用,但是它其实是继承自 ValueAnimator 的,底层的动画实现机制也是基于 ValueAnimator 来完成的,因此 ValueAnimator 仍然是整个属性动画中最核心的一个类。

所以这篇文章就来讲讲 ValueAnimator 这个类的使用,以及它有哪些需要注意的坑。

ValueAnimator 基础用法

属性动画的运行机制是不断地对值进行修改来实现的,而这个值从开始到结束之间如何修改就是由 ValueAnimator 这个类来计算的。我们只需要将初始值和结束值提供给 ValueAnimator,并且告诉它动画运行的时长,ValueAnimator 就会自动帮我们完成从初始值到结束值的修改。除此之外,ValueAnimator 还可以设置动画的重复次数,重复时的播放模式,以及各种监听等。

虽然 ValueAnimator 是一个非常重要的类,但是它的使用却一点都不复杂。下面是一个 ValueAnimator 的简单例子:

ValueAnimator valueAnimator = ValueAnimator.ofFloat(1F); 
valueAnimator.setDuration(500);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        final float currentValue = (float) animation.getAnimatedValue();
        Log.d("ValueAnimation", "ValueAnimator.onAnimationUpdate currentValue : "+currentValue);
    }
});
valueAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
        Log.d("ValueAnimation", "ValueAnimator.onAnimationStart");
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        Log.d("ValueAnimation", "ValueAnimator.onAnimationEnd");
    }

    @Override
    public void onAnimationCancel(Animator animation) {
        Log.d("ValueAnimation", "ValueAnimator.onAnimationCancel");
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
        Log.d("ValueAnimation", "ValueAnimator.onAnimationRepeat");
    }
});
valueAnimator.start();

这个例子使用 ValueAnimator 创建了一个值从 0F 到 1F 之间的变化(ValueAnimator.ofFloat(1F)),
变化的总时长是 500 毫秒(setDuration(500)),
添加了值变化时的监听(addUpdateListener(AnimatorUpdateListener listener)),
添加了动画状态的监听(addListener(AnimatorListener listener)),
最后使用 start() 方法让这个动画运行起来。

在这些监听里,现在都是打印了 log,方便查看整个动画运行的过程。如下:

2024-12-07 18:35:12.621  D  ValueAnimator.onAnimationStart
2024-12-07 18:35:12.621  D  ValueAnimator.onAnimationUpdate currentValue : 0.0
2024-12-07 18:35:12.674  D  ValueAnimator.onAnimationUpdate currentValue : 0.0
2024-12-07 18:35:13.064  D  ValueAnimator.onAnimationUpdate currentValue : 0.8729706
...
2024-12-07 18:35:13.140  D  ValueAnimator.onAnimationUpdate currentValue : 0.9892905
2024-12-07 18:35:13.188  D  ValueAnimator.onAnimationUpdate currentValue : 1.0
2024-12-07 18:35:13.192  D  ValueAnimator.onAnimationEnd

以上是 ValueAnimator 的基本用法,不过大家有没有发现,onAnimationUpdate 在初始值 0F 时,被回调了两次。这就是我们要说的 ValueAnimator 的第一个个坑。

坑一:onAnimationUpdate 在初始值时被回调两次

从上面的 log 能看出确实发现了这个问题,那我们换一下,看看其他类型的 ValueAnimator 是不是也有这个问题:

ValueAnimator valueAnimator = ValueAnimator.ofInt(50);

打印出来的 log 如下:

2024-12-07 19:40:56.132  D  ValueAnimator.onAnimationStart
2024-12-07 19:40:56.133  D  ValueAnimator.onAnimationUpdate currentValue : 0
2024-12-07 19:40:56.174  D  ValueAnimator.onAnimationUpdate currentValue : 0
2024-12-07 19:40:56.345  D  ValueAnimator.onAnimationUpdate currentValue : 12
2024-12-07 19:40:56.356  D  ValueAnimator.onAnimationUpdate currentValue : 14
2024-12-07 19:40:56.368  D  ValueAnimator.onAnimationUpdate currentValue : 17
......
2024-12-07 19:40:56.652  D  ValueAnimator.onAnimationUpdate currentValue : 49
2024-12-07 19:40:56.668  D  ValueAnimator.onAnimationUpdate currentValue : 50
2024-12-07 19:40:56.669  D  ValueAnimator.onAnimationEnd

可以看到 onAnimationUpdate 方法在 0 这个初始值上确实回调了两次。也就意味着 ValueAnimator 确实存在此问题,在编码时需要注意这个坑。

这里我们不继续深入为什么会有这个坑,只考虑如何避免这个坑带来的问题。

有些程序员会在初始值时做一些动画的准备工作,创建必要的对象,读取动画资源,为变量赋初始值等操作:

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        final int currentValue = (int) animation.getAnimatedValue();
        if(currentValue == 0) {
            //动画的初始化工作,资源准备
        }
        //动画逻辑
        Log.d("ValueAnimation", "ValueAnimator.onAnimationUpdate currentValue : "+currentValue);
    }
});

在没有这个坑的情况下,这是没问题的操作。但在这个坑的情况下,onAnimationUpdate 会在初始值调用两次,也就意味着会进行两此动画的初始化工作,这就会造成逻辑错误了。

所以,一般情况下,在添加 ValueAnimator.AnimatorUpdateListener 时,我们一般这么添加:

valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    
    private int lastValue = -1 ;
    
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        final int currentValue = (int) animation.getAnimatedValue();
        if(lastValue == currentValue) {
            return;
        }
        lastValue = currentValue;
        
        if(currentValue == 0) {
            //动画的初始化工作,资源准备
        }
        //动画逻辑
        Log.d("ValueAnimation", "ValueAnimator.onAnimationUpdate currentValue : "+currentValue);
    }
});

这么做有两个好处:

  1. 避免了 onAnimationUpdate 在初始值调用两次的坑带来的各种问题;
  2. 避免了 onAnimationUpdate 回调频率过高带来的性能问题,只在值发生变化时进行必要的操作;

在看完了 ValueAnimator 的基础用法和它的第一个坑之后,我们再来看看它的其他的用法和另外的坑。

ValueAnimator 设置重复播放

最前面说到,ValueAnimator 可以设置动画循环播放,这里涉及到的方法有两个:

  1. ValueAnimator.setRepeatCount(int value);设置重复播放的次数
  2. ValueAnimator.setRepeatMode(@RepeatMode int value);设置动画重复播放的模式

其中 setRepeatCount 可以设置 0 表示不重复(这也是默认值,也就是只播放一次,无重复),也可以设置任意大于0 的值表示动画重复的次数,还能设置为 ValueAnimator.INFINITE 表示动画将无限循环播放;
setRepeatMode(@RepeatMode int value) 方法可以传入的参数为 ValueAnimator.RESTART 表示播放到结束值后,跳转到初始值再不断更新到结束值,ValueAnimator.REVERSE 则表示播放到结束值之后,将逆转播放顺序,下次播放就从结束值不断更新到开始值,然后不断逆转。但是无论是设置什么值,也只有在重复播放时才会看到效果。

我们可以将代码做如下修改:

ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 50);
valueAnimator.setDuration(500);
valueAnimator.setRepeatCount(2);
valueAnimator.setRepeatMode(ValueAnimator.RESTART);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        final int currentValue = (int) animation.getAnimatedValue();
        Log.d("ValueAnimation", "ValueAnimator.onAnimationUpdate currentValue : "+currentValue);
    }
});
valueAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
        Log.d("ValueAnimation", "ValueAnimator.onAnimationStart");
    }
    @Override
    public void onAnimationEnd(Animator animation) {
        Log.d("ValueAnimation", "ValueAnimator.onAnimationEnd");
    }
    @Override
    public void onAnimationCancel(Animator animation) {
        Log.d("ValueAnimation", "ValueAnimator.onAnimationCancel");
    }
    @Override
    public void onAnimationRepeat(Animator animation) {
        Log.d("ValueAnimation", "ValueAnimator.onAnimationRepeat");
    }
});
valueAnimator.start();

将上述代码运行后,能得到如下的 log:

2024-12-07 21:49:25.634  D  ValueAnimator.onAnimationStart
2024-12-07 21:49:25.634  D  ValueAnimator.onAnimationUpdate currentValue : 1
2024-12-07 21:49:26.005  D  ValueAnimator.onAnimationUpdate currentValue : 1
2024-12-07 21:49:26.022  D  ValueAnimator.onAnimationUpdate currentValue : 12
......    1 --> 50
2024-12-07 21:49:26.176  D  ValueAnimator.onAnimationUpdate currentValue : 47
2024-12-07 21:49:26.196  D  ValueAnimator.onAnimationUpdate currentValue : 49
2024-12-07 21:49:26.236  D  ValueAnimator.onAnimationRepeat
2024-12-07 21:49:26.237  D  ValueAnimator.onAnimationUpdate currentValue : 50
2024-12-07 21:49:26.257  D  ValueAnimator.onAnimationUpdate currentValue : 1
2024-12-07 21:49:26.313  D  ValueAnimator.onAnimationUpdate currentValue : 1
2024-12-07 21:49:26.313  D  ValueAnimator.onAnimationUpdate currentValue : 2
......    1 --> 50
2024-12-07 21:49:26.682  D  ValueAnimator.onAnimationUpdate currentValue : 48
2024-12-07 21:49:26.705  D  ValueAnimator.onAnimationUpdate currentValue : 49
2024-12-07 21:49:26.735  D  ValueAnimator.onAnimationRepeat
2024-12-07 21:49:26.783  D  ValueAnimator.onAnimationUpdate currentValue : 50
2024-12-07 21:49:26.799  D  ValueAnimator.onAnimationUpdate currentValue : 1
2024-12-07 21:49:26.799  D  ValueAnimator.onAnimationUpdate currentValue : 1
2024-12-07 21:49:26.799  D  ValueAnimator.onAnimationUpdate currentValue : 2
......    1 --> 50
2024-12-07 21:49:27.199  D  ValueAnimator.onAnimationUpdate currentValue : 49
2024-12-07 21:49:27.232  D  ValueAnimator.onAnimationUpdate currentValue : 50
2024-12-07 21:49:27.233  D  ValueAnimator.onAnimationEnd

通过 log 能看到因为设置了重复次数为 2,因此动画总共运行了 3 次,又因为设置了重复的模式为 RESTART,所以值的变化从 1 --> 50 重复 3 次。可见这两个方法都是可以正常调用的。

但是眼尖的同学应该在这些 log 中发现了坑。

坑二:onAnimationRepeat 的调用时机不太对

在 Log 中,我们能看出来,第一个坑仍然存在,而且我们发现了新的坑,那就是 onAnimationRepeat 这个回调的调用时机不太对。

在常人的理解中,起码在我的理解中,当我看到这个方法,我会认为这个方法应该在两次动画播放之间进行回调,即:

  1. onAnimationUpdate 0 --> 50
  2. onAnimationRepeat
  3. onAnimationUpdate 0 --> 50

然而,实际情况却不是这样:onAnimationRepeat 会在每一次动画的最后一帧之前被调用

这种奇怪的调用时机我不理解,也确实大受震撼。一般情况下,在需要指定动画重复次数的场景下,也需要获取当前是第几次播放,以便在 onAnimationUpdate 有播放不同动画的可能性。但是这种坑的存在,使得处理这种问题只能在 onAnimationUpdate 回调中做特殊处理,这个坑也似乎使 onAnimationRepeat 这个回调丧失了意义。但也有可能 Google 觉得这么设计才是对的。

作为开发者,在使用 ValueAnimator 的时候,一定要注意这个坑,在各自的场景下尽量避免回调时机带来的逻辑问题。

但是你以为这两个坑就完了么?不,还有,但介绍三个坑之前,我们来介绍属性动画中常用的类:AnimationSet

AnimatorSet 组合多个动画

AnimationSet 是 Android 中用于组合多个动画的容器类。通过 AnimationSet,你可以将多个独立的动画组合成一个整体,使得它们可以同时播放或是顺序播放,从而创建更复杂的动画效果。

AnimationSetAnimation 的子类,其本身也是一种动画,因此你可以将一个 AnimationSet 放到另一个 AnimationSet 中。其常用方法与 ValueAnimator 类似,但是有两个方法需要注意:

  1. AnimationSet.playSequentially(Animator... items) 将传入的动画按照顺序播放
  2. AnimationSet.playTogether(Animator... items) 将传入的动画一起播放

当然你也可以通过 AnimatorSet.Builder 的四个方法做更方便的设置。
无论是调用 playSequentially 还是 playTogetherAnimatorSet 都需要调用 start() 方法才能开启动画,也可以为其设置与 ValueAnimator 相同的监听。

我们看下面的代码:

ValueAnimator valueAnimator_1 = ValueAnimator.ofInt(1, 50);
ValueAnimator valueAnimator_2 = ValueAnimator.ofInt(100, 150);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playSequentially(valueAnimator_1,valueAnimator_2);
animatorSet.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
        Log.d("ValueAnimation", "animatorSet.onAnimationStart");
    }
    @Override
    public void onAnimationEnd(Animator animation) {
        Log.d("ValueAnimation", "animatorSet.onAnimationEnd");
    }
    @Override
    public void onAnimationCancel(Animator animation) {
        Log.d("ValueAnimation", "animatorSet.onAnimationCancel");
    }
    @Override
    public void onAnimationRepeat(Animator animation) {
        Log.d("ValueAnimation", "animatorSet.onAnimationRepeat");
    }
});
animatorSet.start();

这段代码简单演示了 AnimatorSet 的使用,使用它来播放两个 ValueAnimator,一个是从 0 到 50,一个是从 100 到 150。

我们再看一下 log:

2024-12-07 23:26:52.249  D  valueAnimator_1.onAnimationStart
2024-12-07 23:26:52.250  D  valueAnimator_1.onAnimationUpdate currentValue : 1
2024-12-07 23:26:52.250  D  valueAnimator_1.onAnimationUpdate currentValue : 1
2024-12-07 23:26:52.250  D  animatorSet.onAnimationStart
2024-12-07 23:26:52.317  D  valueAnimator_1.onAnimationUpdate currentValue : 1
2024-12-07 23:26:52.567  D  valueAnimator_1.onAnimationUpdate currentValue : 25
......    1 --> 50
2024-12-07 23:26:52.820  D  valueAnimator_1.onAnimationUpdate currentValue : 49
2024-12-07 23:26:52.840  D  valueAnimator_1.onAnimationUpdate currentValue : 50
2024-12-07 23:26:52.840  D  valueAnimator_1.onAnimationEnd
2024-12-07 23:26:52.841  D  valueAnimator_2.onAnimationStart
2024-12-07 23:26:52.841  D  valueAnimator_2.onAnimationUpdate currentValue : 100
2024-12-07 23:26:52.846  D  valueAnimator_2.onAnimationUpdate currentValue : 100
2024-12-07 23:26:52.846  D  valueAnimator_2.onAnimationUpdate currentValue : 100
2024-12-07 23:26:52.873  D  valueAnimator_2.onAnimationUpdate currentValue : 101
......    100 --> 150
2024-12-07 23:26:53.300  D  valueAnimator_2.onAnimationUpdate currentValue : 149
2024-12-07 23:26:53.319  D  valueAnimator_2.onAnimationUpdate currentValue : 150
2024-12-07 23:26:53.320  D  valueAnimator_2.onAnimationEnd
2024-12-07 23:26:53.320  D  animatorSet.onAnimationEnd

能看到两个 ValueAnimator 都按照顺序播放了,并且 AnimatorSet 的回调都被调用了。使用 AnimatorSet 就是这么简单。
不过咱们现在把目光放到 log 的最前面的一部分,就会发现第三个坑所在了。

坑三:ValueAnimator 先于 AnimatorSetonAnimationStart 方法执行

理论上来讲,当使用 AnimatorSet 包装了两个 ValueAnimator 之后,应该是 AnimatorSetonAnimationStart 方法先执行,然后开始两个 ValueAnimator 的播放。但实际上,会将第一个动画先进行调用,而且会回调 onAnimationUpdate

2024-12-07 23:26:52.249  D  valueAnimator_1.onAnimationStart
2024-12-07 23:26:52.250  D  valueAnimator_1.onAnimationUpdate currentValue : 1
2024-12-07 23:26:52.250  D  valueAnimator_1.onAnimationUpdate currentValue : 1
2024-12-07 23:26:52.250  D  animatorSet.onAnimationStart
......

一般来说,大家在使用动画时,都需要在 onAnimationStart 中进行一些初始化,创建一些对象,而这个坑的存在直接使得在 AnimatorSetonAnimationStart 方法中进行必要的初始化这一操作变得不可能。

那么怎么避免这个问题呢?只能将初始化操作放到各个 ValueAnimatoronAnimationStart 方法中了。

简单总结一下,就是 ValueAnimator.onAnimationRepeatAnimatorSet.onAnimationStart 这两个回调不可靠,别使用就行了。

另外,大家在设置 AnimatorListener 的时候,总是要覆写一些可能用不上的方法,这里推荐 AnimatorListenerAdapter,使用它可以让你只关注自己需要的方法。


李斯维
868 声望6 粉丝

这个家伙很懒,总是喜欢宅在家里,以至于这里什么都没写...