Stories

Detail Return Return

Android 動畫 ValueAnimator 的使用和那些坑 - Stories Detail

前言

在早期,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,使用它可以讓你只關注自己需要的方法。

Add a new Comments

Some HTML is okay.