Android 動畫之屬性動畫

被被Android程式設計之路發表於2019-05-07

簡介

Android 開發中,總是需要一些動畫來優化使用者的互動體驗,提高使用者滿意度。因此,Google 為我們提供了一些用於處理動畫效果的動畫框架。Android 的動畫框架分為兩類:

  • 傳統動畫(Animation):通過系統不斷呼叫onDraw方法重繪介面,來達到動畫的效果。
  • 屬性動畫(Animator):通過操縱一個屬性的get/set方法,真實地改變目標的某些屬性。

傳統動畫框架的侷限性

既然有了傳統動畫框架,Google 為什麼還要創造一個屬性動畫框架呢?

我們下面舉個例子來說明一下傳統動畫的侷限性。

在佈局中加入一個 ImageView 和一個 Button,點選 ImageView 後彈出一個 Toast,點選 Button 後使 ImageView 展現一個向右平移的動畫效果。

下面是使用傳統動畫實現的程式碼:

TranslateAnimation animation = new TranslateAnimation(0,200,0,0);    // 平移動畫x軸移動200,y軸不動
animation.setDuration(1000);    // 動畫時長
animation.setFillAfter(true);   // 使動畫結束後停留在結束的位置
mIvPicture.startAnimation(animation);
複製程式碼

執行後,ImageView 確實進行了我們預期的平移的效果。可是當我們嘗試點選 ImageView 當前的位置時,卻沒有 Toast 彈出。我們再嘗試去點選 ImageView 開始動畫前的位置,卻成功彈出了 Toast。

這就是傳統動畫很大的侷限性:

  • 它僅僅是重繪了控制元件,改變了其顯示的位置。但真正事件響應的位置,卻並沒有發生改變。因此傳統動畫不適合做具有互動的動畫效果。僅僅能做一些顯示的動畫效果
  • 傳統動畫是不斷通過 onDraw() 方法重繪介面,必然會十分耗費GPU資源。
  • 傳統動畫所支援的動畫型別少,僅有旋轉、縮放、位移、透明度這四種動畫效果。雖然通過組合可以實現豐富的效果,但相比直接通過改變屬性來實現的屬性動畫來說,還是有很大的侷限性的。

因此,Google 為我們提供了一套全新的屬性動畫框架,來讓我們實現更豐富的動畫效果。

ObjectAnimator

ObjectAnimator 是屬性動畫中,最簡單也最常用的一個物件。

實現 Animation 框架的功能

平移

前文提到的使 ImageVIew 向右平移 200 畫素的動畫效果,使用屬性動畫只需要很簡單的幾句程式碼即可實現:

ObjectAnimator.ofFloat(mIvPicture,"translationX",0F,200F)
        .setDuration(1000)
        .start();
複製程式碼

我們來分析一下這一句程式碼。我們呼叫了ofFloat程式碼,並傳入三個引數。

​ 第一個引數是動畫需要操縱的目標,在這裡是我們的 ImageView。

​ 第二個引數是所需要操縱的目標所具備的屬性名稱。

​ 第三個引數是這個動畫變化的取值範圍。

最後設定一下它的動畫的屬性,便可以 start 了。

這次我們再次點選 ImageView 目前的位置,成功地彈出了 Toast。這證實了屬性動畫是通過改變物體的屬性來達到動畫效果的理論。

當我們需要改變 y 座標時,只需要把 "translationX" 變為 "translationY" 即可。

其實 ,只要Google對一個物件的某個屬性提供了get和set方法,我們就可以使用這個屬性來實現動畫效果

其實我們還能用 X Y 兩個屬性實現之前的動畫效果,那麼物件屬性中 X 的 Y 與 translationX translationY 有什麼區別呢?

  • translationX translationY指的是物體的偏移量,而X Y則表示它最終到達的絕對位置。

旋轉

旋轉屬性使用的是 "rotation" 屬性,後面的變換範圍的單位是角度。

比如想讓 ImageView 旋轉90度,只需要

ObjectAnimator.ofFloat(mIvPicture,"rotation",0F,90F)
        .setDuration(1000)
        .start();
複製程式碼

其他

其實屬性動畫能操縱的屬性,只要具有 set、get 方法,都可以進行操縱。如 scaleX、scaleY 等等...

插值器

Android 為我們內建了插值器,使我們的動畫更為自然。比如可以讓我們的平移動畫像物體的重力加速度由快到慢的 Accelerate 等等

Android中內建了七種插值器,分別是

  • Accelerate
  • Decelerate
  • Accelerate/Decelerate
  • Anticipate
  • Overshoot
  • Anticipate/Overshoot
  • Bounce

要應用插值器,可以呼叫 ObjectAnimator 的 setInterpolator 方法, new 出對應的插值器作為引數(xxxInterpolator)。比如下面這段程式碼:

animator.setInterpolator(new AccelerateInterpolator());
複製程式碼

通過插值器,我們可以讓動畫的效果更佳自然。

多種屬性動畫同時作用

當我們把幾種動畫按順序寫下時,執行程式,會發現效果是三種屬性動畫的疊加。由此可以發現,屬性動畫在呼叫 start 方法後,實際上是一個非同步的過程。因此我們就可以看到三個屬性動畫同時作用的效果。通過這樣的方法,其實就可以實現多種屬性動畫同時作用的效果:

ObjectAnimator.ofFloat(mIvPicture,"translationX",0F,200F).setDuration(1000).start();
ObjectAnimator.ofFloat(mIvPicture,"rotationX",0F,360F).setDuration(1000).start();
ObjectAnimator.ofFloat(mIvPicture,"translationY",0F,200F).setDuration(1000).start();
複製程式碼

其實 Google 為我們提供了更好的方法,來實現這樣的效果。

我們可以使用 PropertyValuesHolder 來實現。其建構函式僅僅比 ObjectAnimator 少了一個作用物件引數。之後通過ObjectAnimator 的 ofPropertyValuesHolder 方法,傳入作用物件以及要同時作用的 PropertyValuesHolder 即可執行。可以看到下面的程式碼示例:

PropertyValuesHolder p1 = PropertyValuesHolder.ofFloat("translationX",0F,200F);
PropertyValuesHolder p2 = PropertyValuesHolder.ofFloat("rotationX",0F,360F);
PropertyValuesHolder p3 = PropertyValuesHolder.ofFloat("translationY",0F,200F);
ObjectAnimator.ofPropertyValuesHolder(mIvPicture,p1,p2,p3).setDuration(1000).start();
複製程式碼

執行後可以發現,與之前的效果是相同的。

那既然兩種方法效果一樣,這樣相比之前有什麼好處麼?

  • 其實 Google 在 PropertyValuesHolder 內部進行了一些優化,使得我們使用多個屬性動畫時更加有效率,節省系統資源。

AnimatorSet 屬性集合

playTogether 方法

我們其實還可以通過 AnimatorSet,來實現同樣的效果。這裡我們呼叫了 set 的 playTogether 方法,使得這些方法同時執行:

AnimatorSet set = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(mIvPicture, "translationX", 0F, 200F);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(mIvPicture, "rotationX", 0F, 360F);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(mIvPicture, "translationY", 0F, 200F);
set.playTogether(animator1,animator2,animator3);
set.setDuration(1000);
set.start();
複製程式碼

playSequentially方法

除了 playTogether 方法外,AnimatorSet 還提供了 playSequentially 方法,它可以使得動畫按順序執行。具體順序取決於呼叫時的引數順序。

AnimatorSet set = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(mIvPicture, "translationX", 0F, 200F);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(mIvPicture, "rotationX", 0F, 360F);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(mIvPicture, "translationY", 0F, 200F);
set.playSequentially(animator1,animator2,animator3);
set.setDuration(1000);
set.start();
複製程式碼

play 與 with、after 方法

我們除了可以用上述方法來讓動畫按順序執行外,也可以通過 AnimatorSet 的 play、with、after、before 等方法相組合來控制動畫播放關係。

例如如下的程式碼就可以實現先平移,再旋轉的效果

set.play(animator1).with(animator3);
set.play(animator2).after(animator1);
複製程式碼

動畫監聽事件

通過下面的程式碼,我們可以實現按鈕按下後漸隱的效果。

mBtnPress.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        ObjectAnimator animator = ObjectAnimator.ofFloat(mBtnPress,"alpha",1F,0F);
        animator.setDuration(1000);
        animator.start();
    }
});
複製程式碼

但如果我們想要在動畫播放完成後再執行一些操作的話,又該如何實現呢?

  • 我們可以使用 ObjectAnimator 的 addListener方法,傳入一個AnimatorListener,為動畫設定監聽事件。

AnimatorListener

一個AnimatorListener,需要實現四個方法,分別是:

  • onAnimationStart
  • onAnimationEnd
  • onAnimationCancel
  • onAnimationRepeat

它們的回撥時機我們根據字面意思便可以理解。大部分時候,我們需要實現的是onAnimationEnd方法。

animator.addListener(new AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
    }
    @Override
    public void onAnimationEnd(Animator animation) {
        Toast.makeText(MainActivity.this,"Animation End",Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onAnimationCancel(Animator animation) {
    }
    @Override
    public void onAnimationRepeat(Animator animation) {
    }
});
複製程式碼

AnimatorListenerAdapter

如果每次監聽都需要實現這麼多方法,未免太麻煩了一點。因此 Android 為我們提供了另一種方法來新增動畫的監聽事件:在新增 AnimatorListener 的時候,傳入 AnimatorListenerAdapter 即可。這樣我們就只需要實現自己需要的方法即可。

animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        Toast.makeText(MainActivity.this,"Animation End",Toast.LENGTH_SHORT).show();
    }
});
複製程式碼

ValueAnimator

簡介

ValueAnimator 本身不作用於任何一個屬性,也不提供任何一種動畫。它就是一個數值發生器,可以產生想要的各種數值。Android 系統為它提供了很多計算數值的方法,如 int、float 等等。我們也可以自己實現計算數值的方法。其實,在屬性動畫中,如何產生每一步的動畫效果,都是通過 ValueAnimator 計算出來的。

比如我們要實現一個從 0-100 的位移動畫。隨著動畫時間的持續,它產生的值也會從 0-100 遞增。通過這個 ValueAnimator 產生的值,再進行屬性的設定即可。

那麼 ValueAnimator 究竟是如何產生這些值的呢?

  • 首先 ValueAnimator會根據會根據動畫已進行的時間與它持續的總時間的比值,產生一個0-1的時間因子。有了這樣的時間因子,經過相應的變換,就可以根據初始值和最終值來生成中間的相應值。同時,通過插值器的使用,我們還可以進一步控制每一個時間因子產生值的變化速率。如果我們使用的是線性插值器,那麼它生成值的時候就會呈一個線性變化。如果我們使用一個加速度插值器,那麼它生成值時便會呈一個二次曲線,增長率越來越快。

由於 ValueAnimator 不作用於任何一個屬性,也不提供任何一種動畫。因此並沒有 ObjectAnimator 使用得廣泛。

實際上,ObjectAnimator 就是基於 ValueAnimator 進行的一次封裝。我們可以檢視 ObjectAnimator 的原始碼,會發現它繼承自 ValueAnimator,是它的一個子類。正是 ValueAnimator 產生的變化值,才使得 ObjectAnimator 可以將它應用於各個屬性。

使用方法

我們可以通過 ValueAnimator 的 ofXXX 產生一個 XXX 型別的值(如ofInt),然後為 ValueAnimator 新增一個更新的回撥事件。在回撥事件中,通過引數 animation 的 getAnimationValue() 方法,來獲取對應的 value。有了這個值,我們就可以實現我們所有想要的動畫效果。

比如此處就通過 ValueAnimator 實現了一個計時器的動畫效果。

ValueAnimator animator = ValueAnimator.ofInt(0,100);
animator.setDuration(5000);
animator.addUpdateListener(new AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        Integer value = (Integer) animation.getAnimatedValue();
        mButton.setText(""+value);
    }
});
animator.start();
複製程式碼

自定義數值生成器

前面提到,ValueAnimator 可以建立自定義的數值生成器,做法就是呼叫 ValueAnimator 的 ofObject 方法,建立一個 TypeEvaluator 作為引數。之後我們可以通過重寫 TypeEvaluator 的 evaluate 方法,來按照自己的規則返回具體的值。

ValueAnimator animator = ValueAnimator.ofObject(new TypeEvaluator() {
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        //計算
        return null;    //返回值
    }
});
複製程式碼

我們來看一下 evaluate 方法的幾個引數

  • float fraction:前面提到的時間因子
  • Object startValue:起始值
  • Object endValue:結束值

通過這三個值,我們就可以經過計算產生所有我們想要的值。

其實,通過 TypeEvaluator,我們不光能產生普通的資料,還能結合泛型,我們還能定義更加複雜的資料:

我們可以在建立 TypeEvaluator 時指定具體型別,來達到更豐富的效果。比如這裡就用到了一個名為 PointF 的資料型別:

ValueAnimator animator = ValueAnimator.ofObject(new TypeEvaluator<PointF>() {
    @Override
    public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
        //計算
        return null;    //返回值
    }
});
複製程式碼

相關文章