1. 程式人生 > >Android三種動畫之(三)屬性動畫

Android三種動畫之(三)屬性動畫

 分享自Carson_Ho的簡書,這篇寫的很詳細,我就不浪費時間寫了

目錄

目錄


1. 屬性動畫出現的原因

  • 屬性動畫(Property Animation)是在 Android 3.0API 11)後才提供的一種全新動畫模式
  • 那麼為什麼要提供屬性動畫(Property Animation)?

1.1 背景

實現動畫效果在Android開發中非常常見,因此Android系統一開始就提供了兩種實現動畫的方式:

  • 逐幀動畫(Frame Animation
  • 補間動畫( Tweened animation

1.2 問題

逐幀動畫 & 補間動畫存在一定的缺點:

a. 作用物件侷限:View

即補間動畫 只能夠作用在檢視View上,即只可以對一個ButtonTextView、甚至是LinearLayout、或者其它繼承自View的元件進行動畫操作,但無法對非View的物件進行動畫操作

  1. 有些情況下的動畫效果只是檢視的某個屬性 & 物件而不是整個檢視;
  2. 如,現需要實現檢視的顏色動態變化,那麼就需要操作檢視的顏色屬性從而實現動畫效果,而不是針對整個檢視進行動畫操作

b. 沒有改變View的屬性,只是改變視覺效果

  • 補間動畫只是改變了View
    的視覺效果,而不會真正去改變View的屬性。
  • 如,將螢幕左上角的按鈕 通過補間動畫 移動到螢幕的右下角
  • 點選當前按鈕位置(螢幕右下角)是沒有效果的,因為實際上按鈕還是停留在螢幕左上角,補間動畫只是將這個按鈕繪製到螢幕右下角,改變了視覺效果而已。

c. 動畫效果單一

  • 補間動畫只能實現平移、旋轉、縮放 & 透明度這些簡單的動畫需求
  • 一旦遇到相對複雜的動畫效果,即超出了上述4種動畫效果,那麼補間動畫則無法實現。

即在功能 & 可擴充套件性有較大侷限性

1.3 問題

  • 為了解決補間動畫的缺陷,在 Android 3.0
    (API 11)開始,系統提供了一種全新的動畫模式:屬性動畫(Property Animation
  • 下面,我將全面介紹屬性動畫(Property Animation

2. 簡介

  • 作用物件:任意 Java 物件

不再侷限於 檢視View物件

  • 實現的動畫效果:可自定義各種動畫效果

不再侷限於4種基本變換:平移、旋轉、縮放 & 透明度


3. 特點

  • 作用物件進行了擴充套件:不只是View物件,甚至沒物件也可以
  • 動畫效果:不只是4種基本變換,還有其他動畫效果
  • 作用領域:API11後引入的

4. 工作原理

  • 在一定時間間隔內,通過不斷對值進行改變,並不斷將該值賦給物件的屬性,從而實現該物件在該屬性上的動畫效果

可以是任意物件的任意屬性

  • 具體的工作原理邏輯如下:

工作原理

  • 從上述工作原理可以看出屬性動畫有兩個非常重要的類:ValueAnimator 類 & ObjectAnimator
  • 其實屬性動畫的使用基本都是依靠這兩個類
  • 所以,在下面介紹屬性動畫的具體使用時,我會著重介紹這兩個類。

5. 具體使用

5.1 ValueAnimator類

  • 定義:屬性動畫機制中 最核心的一個類
  • 實現動畫的原理:通過不斷控制 值 的變化,再不斷 手動 賦給物件的屬性,從而實現動畫效果。如圖下:

工作原理

從上面原理可以看出:ValueAnimator類中有3個重要方法:

  1. ValueAnimator.ofInt(int values)
  2. ValueAnimator.ofFloat(float values)
  3. ValueAnimator.ofObject(int values)

下面我將逐一介紹。

5.1.1 ValueAnimator.ofInt(int values)

  • 作用:將初始值 以整型數值的形式 過渡到結束值

即估值器是整型估值器 - IntEvaluator

  • 工作原理:

工作原理

  • 具體使用:

特別說明:

  1. 因為ValueAnimator本質只是一種值的操作機制,所以下面的介紹先是展示如何改變一個值的過程(下面的例項主要講解:如何將一個值從0平滑地過渡到3)
  2. 至於如何實現動畫,是需要開發者手動將這些 值 賦給 物件的屬性值。關於這部分在下節會進行說明。

操作值的方式 分為 XML 設定 / Java 程式碼設定
設定方式1:Java程式碼設定

實際開發中,建議使用Java程式碼實現屬性動畫:因為很多時候屬性的起始值是無法提前確定的(無法使用XML設定),這就需要在Java程式碼裡動態獲取。

// 步驟1:設定動畫屬性的初始值 & 結束值
ValueAnimator anim = ValueAnimator.ofInt(0, 3);
        // ofInt()作用有兩個
        // 1. 建立動畫例項
        // 2. 將傳入的多個Int引數進行平滑過渡:此處傳入0和1,表示將值從0平滑過渡到1
        // 如果傳入了3個Int引數 a,b,c ,則是先從a平滑過渡到b,再從b平滑過渡到C,以此類推
        // ValueAnimator.ofInt()內建了整型估值器,直接採用預設的.不需要設定,即預設設定瞭如何從初始值 過渡到 結束值
        // 關於自定義插值器我將在下節進行講解
        // 下面看看ofInt()的原始碼分析 ->>關注1
        
// 步驟2:設定動畫的播放各種屬性
        anim.setDuration(500);
        // 設定動畫執行的時長
        
        anim.setStartDelay(500);
        // 設定動畫延遲播放時間

        anim.setRepeatCount(0);
        // 設定動畫重複播放次數 = 重放次數+1
        // 動畫播放次數 = infinite時,動畫無限重複
        
        anim.setRepeatMode(ValueAnimator.RESTART);
        // 設定重複播放動畫模式
        // ValueAnimator.RESTART(預設):正序重放
        // ValueAnimator.REVERSE:倒序回放
     
// 步驟3:將改變的值手動賦值給物件的屬性值:通過動畫的更新監聽器
        // 設定 值的更新監聽器
        // 即:值每次改變、變化一次,該方法就會被呼叫一次
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                int currentValue = (Integer) animation.getAnimatedValue();
                // 獲得改變後的值
                
                System.out.println(currentValue);
                // 輸出改變後的值

        // 步驟4:將改變後的值賦給物件的屬性值,下面會詳細說明
                View.setproperty(currentValue);

       // 步驟5:重新整理檢視,即重新繪製,從而實現動畫效果
                View.requestLayout();
                
                
            }
        });

        anim.start();
        // 啟動動畫
    }

// 關注1:ofInt()原始碼分析
    public static ValueAnimator ofInt(int... values) {
        // 允許傳入一個或多個Int引數
        // 1. 輸入一個的情況(如a):從0過渡到a;
        // 2. 輸入多個的情況(如a,b,c):先從a平滑過渡到b,再從b平滑過渡到C
        
        ValueAnimator anim = new ValueAnimator();
        // 建立動畫物件
        anim.setIntValues(values);
        // 將傳入的值賦值給動畫物件
        return anim;
    }

效果圖

值 從初始值 過度到 結束值 的過程如下:

 

效果圖

設定方法2:在XML 程式碼中設定
具備重用性,即將通用的動畫寫到XML裡,可在各個介面中去重用它

  • 步驟1:在路徑 res/animator的資料夾裡建立相應的動畫 .xml檔案

此處設定為res/animator/set_animation.xml

  • 步驟2:設定動畫引數

set_animation.xml

// ValueAnimator採用<animator>  標籤
<animator xmlns:android="http://schemas.android.com/apk/res/android"  
    android:valueFrom="0"   // 初始值
    android:valueTo="100"  // 結束值
    android:valueType="intType" // 變化值型別 :floatType & intType

    android:duration="3000" // 動畫持續時間(ms),必須設定,動畫才有效果
    android:startOffset ="1000" // 動畫延遲開始時間(ms)
    android:fillBefore = “true” // 動畫播放完後,檢視是否會停留在動畫開始的狀態,預設為true
    android:fillAfter = “false” // 動畫播放完後,檢視是否會停留在動畫結束的狀態,優先於fillBefore值,預設為false
    android:fillEnabled= “true” // 是否應用fillBefore值,對fillAfter值無影響,預設為true
    android:repeatMode= “restart” // 選擇重複播放動畫模式,restart代表正序重放,reverse代表倒序回放,預設為restart|
    android:repeatCount = “0” // 重放次數(所以動畫的播放次數=重放次數+1),為infinite時無限重複
    android:interpolator = @[package:]anim/interpolator_resource // 插值器,即影響動畫的播放速度,下面會詳細講

/>  
  • 步驟3:在Java程式碼中啟動動畫
Animator animator = AnimatorInflater.loadAnimator(context, R.animator.set_animation);  
// 載入XML動畫

animator.setTarget(view);  
// 設定動畫物件

animator.start();  
// 啟動動畫

效果圖

效果同第一種方式是一樣的。

例項說明

  • 下面,我將結合 手動賦值給物件屬性 這一步驟,從而實現一個完整的動畫效果
  • 實現的動畫效果:按鈕的寬度從 150px 放大到 500px
Button mButton = (Button) findViewById(R.id.Button);
        // 建立動畫作用物件:此處以Button為例

// 步驟1:設定屬性數值的初始值 & 結束值
        ValueAnimator valueAnimator = ValueAnimator.ofInt(mButton.getLayoutParams().width, 500);
        // 初始值 = 當前按鈕的寬度,此處在xml檔案中設定為150
        // 結束值 = 500
        // ValueAnimator.ofInt()內建了整型估值器,直接採用預設的.不需要設定
        // 即預設設定瞭如何從初始值150 過渡到 結束值500

// 步驟2:設定動畫的播放各種屬性
        valueAnimator.setDuration(2000);
        // 設定動畫執行時長:1s

// 步驟3:將屬性數值手動賦值給物件的屬性:此處是將 值 賦給 按鈕的寬度
        // 設定更新監聽器:即數值每次變化更新都會呼叫該方法
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animator) {

                int currentValue = (Integer) animator.getAnimatedValue();
                // 獲得每次變化後的屬性值
                System.out.println(currentValue);
                // 輸出每次變化後的屬性值進行檢視

                mButton.getLayoutParams().width = currentValue;
                // 每次值變化時,將值手動賦值給物件的屬性
                // 即將每次變化後的值 賦 給按鈕的寬度,這樣就實現了按鈕寬度屬性的動態變化

// 步驟4:重新整理檢視,即重新繪製,從而實現動畫效果
                mButton.requestLayout();
                
            }
        });

        valueAnimator.start();
        // 啟動動畫

    }

效果圖

效果圖

5.1.2 ValueAnimator.oFloat(float values)

  • 作用:將初始值 以浮點型數值的形式 過渡到結束值

  • 工作原理:

工作原理

  • 具體使用:分為 XML 設定 / Java 程式碼設定

設定方法1:在 Java 程式碼中設定

ValueAnimator anim = ValueAnimator.ofFloat(0, 3);  
// 其他使用類似ValueAnimator.ofInt(int values),此處不作過多描述

設定方法2:在XML 程式碼中設定

  • 步驟1:在路徑 res/animator的資料夾裡建立相應的動畫.xml檔案

此處設定為res/animator/set_animation.xml

  • 步驟2:設定動畫引數

set_animation.xml

// ValueAnimator採用<animator>  標籤
<animator xmlns:android="http://schemas.android.com/apk/res/android"  
    // 設定屬性同上
    android:valueFrom="0"  
    android:valueTo="100"  
    android:valueType="intType"/>  
  • 步驟3:在Java程式碼中啟動動畫
Animator animator = AnimatorInflater.loadAnimator(context, R.animator.set_animation);  
// 載入XML動畫

animator.setTarget(view);  
// 設定動畫物件

animator.start();  
// 啟動動畫

效果圖

效果圖

從上面可以看出,ValueAnimator.ofInt()ValueAnimator.oFloat()僅僅只是在估值器上的區別:(即如何從初始值 過渡 到結束值)

  • ValueAnimator.oFloat()採用預設的浮點型估值器 (FloatEvaluator)
  • ValueAnimator.ofInt()採用預設的整型估值器(IntEvaluator

在使用上完全沒有區別,此處對ValueAnimator.oFloat()的使用就不作過多描述。


5.1.3 ValueAnimator.ofObject()

  • 作用:將初始值 以物件的形式 過渡到結束值

即通過操作 物件 實現動畫效果

  • 工作原理:

工作原理

  • 具體使用:
// 建立初始動畫時的物件  & 結束動畫時的物件
myObject object1 = new myObject();  
myObject object2 = new myObject();  

ValueAnimator anim = ValueAnimator.ofObject(new myObjectEvaluator(), object1, object2);  
// 建立動畫物件 & 設定引數
// 引數說明
// 引數1:自定義的估值器物件(TypeEvaluator 型別引數) - 下面會詳細介紹
// 引數2:初始動畫的物件
// 引數3:結束動畫的物件
anim.setDuration(5000);  
anim.start(); 

在繼續講解ValueAnimator.ofObject()的使用前,我先講一下估值器(TypeEvaluator

估值器(TypeEvaluator) 介紹

  • 作用:設定動畫 如何從初始值 過渡到 結束值 的邏輯
  1. 插值器(Interpolator)決定 值 的變化模式(勻速、加速blabla)
  2. 估值器(TypeEvaluator)決定 值 的具體變化數值

從5.1.2節可看到:

  • ValueAnimator.ofFloat()實現了 **將初始值 以浮點型的形式 過渡到結束值 ** 的邏輯,那麼這個過渡邏輯具體是怎麼樣的呢?
  • 其實是系統內建了一個 FloatEvaluator估值器,內部實現了初始值與結束值 以浮點型的過渡邏輯
  • 我們來看一下 FloatEvaluator的程式碼實現:
public class FloatEvaluator implements TypeEvaluator {  
// FloatEvaluator實現了TypeEvaluator介面

// 重寫evaluate()
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
// 引數說明
// fraction:表示動畫完成度(根據它來計算當前動畫的值)
// startValue、endValue:動畫的初始值和結束值
        float startFloat = ((Number) startValue).floatValue();  
        
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);  
        // 初始值 過渡 到結束值 的演算法是:
        // 1. 用結束值減去初始值,算出它們之間的差值
        // 2. 用上述差值乘以fraction係數
        // 3. 再加上初始值,就得到當前動畫的值
    }  
}  
  • ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具備系統內建的估值器,即FloatEvaluator & IntEvaluator

即系統已經預設實現了 如何從初始值 過渡到 結束值 的邏輯

  • 但對於ValueAnimator.ofObject(),從上面的工作原理可以看出並沒有系統預設實現,因為對物件的動畫操作複雜 & 多樣,系統無法知道如何從初始物件過度到結束物件
  • 因此,對於ValueAnimator.ofObject(),我們需自定義估值器(TypeEvaluator)來告知系統如何進行從 初始物件 過渡到 結束物件的邏輯
  • 自定義實現的邏輯如下
// 實現TypeEvaluator介面
public class ObjectEvaluator implements TypeEvaluator{  

// 複寫evaluate()
// 在evaluate()裡寫入物件動畫過渡的邏輯
    @Override  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        // 引數說明
        // fraction:表示動畫完成度(根據它來計算當前動畫的值)
        // startValue、endValue:動畫的初始值和結束值

        ... // 寫入物件動畫過渡的邏輯
        
        return value;  
        // 返回物件動畫過渡的邏輯計算後的值
    }  
  

例項說明

  • 下面我將用例項說明 該如何自定義TypeEvaluator介面並通過ValueAnimator.ofObject()實現動畫效果

  • 實現的動畫效果:一個圓從一個點 移動到 另外一個點

     

    效果圖

  • 工程目錄檔案如下:

     

    工程目錄

步驟1:定義物件類

  • 因為ValueAnimator.ofObject()是面向物件操作的,所以需要自定義物件類。
  • 本例需要操作的物件是 圓的點座標
    Point.java
public class Point {

    // 設定兩個變數用於記錄座標的位置
    private float x;
    private float y;

    // 構造方法用於設定座標
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }

    // get方法用於獲取座標
    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }
}

步驟2:根據需求實現TypeEvaluator介面

  • 實現TypeEvaluator介面的目的是自定義如何 從初始點座標 過渡 到結束點座標;
  • 本例實現的是一個從左上角到右下角的座標過渡邏輯。

     

    效果圖

PointEvaluator.java

// 實現TypeEvaluator介面
public class PointEvaluator implements TypeEvaluator {

    // 複寫evaluate()
    // 在evaluate()裡寫入物件動畫過渡的邏輯
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 將動畫初始值startValue 和 動畫結束值endValue 強制型別轉換成Point物件
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;

        // 根據fraction來計算當前動畫的x和y的值
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
        
        // 將計算後的座標封裝到一個新的Point物件中並返回
        Point point = new Point(x, y);
        return point;
    }

}
  • 上面步驟是根據需求自定義TypeEvaluator的實現
  • 下面將講解如何通過對 Point 物件進行動畫操作,從而實現整個自定義View的動畫效果。

步驟3:將屬性動畫作用到自定義View當中

MyView.java

/**
 * Created by Carson_Ho on 17/4/18.
 */
public class MyView extends View {
    // 設定需要用到的變數
    public static final float RADIUS = 70f;// 圓的半徑 = 70
    private Point currentPoint;// 當前點座標
    private Paint mPaint;// 繪圖畫筆
    

    // 構造方法(初始化畫筆)
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 複寫onDraw()從而實現繪製邏輯
    // 繪製邏輯:先在初始點畫圓,通過監聽當前座標值(currentPoint)的變化,每次變化都呼叫onDraw()重新繪製圓,從而實現圓的平移動畫效果
    @Override
    protected void onDraw(Canvas canvas) {
        // 如果當前點座標為空(即第一次)
        if (currentPoint == null) {
            currentPoint = new Point(RADIUS, RADIUS);
            // 建立一個點物件(座標是(70,70))

            // 在該點畫一個圓:圓心 = (70,70),半徑 = 70
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);


 // (重點關注)將屬性動畫作用到View中
            // 步驟1:建立初始動畫時的物件點  & 結束動畫時的物件點
            Point startPoint = new Point(RADIUS, RADIUS);// 初始點為圓心(70,70)
            Point endPoint = new Point(700, 1000);// 結束點為(700,1000)

            // 步驟2:建立動畫物件 & 設定初始值 和 結束值
            ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
            // 引數說明
            // 引數1:TypeEvaluator 型別引數 - 使用自定義的PointEvaluator(實現了TypeEvaluator介面)
            // 引數2:初始動畫的物件點
            // 引數3:結束動畫的物件點

            // 步驟3:設定動畫引數
            anim.setDuration(5000);
            // 設定動畫時長

// 步驟3:通過 值 的更新監聽器,將改變的物件手動賦值給當前物件
// 此處是將 改變後的座標值物件 賦給 當前的座標值物件
            // 設定 值的更新監聽器
            // 即每當座標值(Point物件)更新一次,該方法就會被呼叫一次
            anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    currentPoint = (Point) animation.getAnimatedValue();
                    // 將每次變化後的座標值(估值器PointEvaluator中evaluate()返回的Piont物件值)到當前座標值物件(currentPoint)
                    // 從而更新當前座標值(currentPoint)

// 步驟4:每次賦值後就重新繪製,從而實現動畫效果
                    invalidate();
                    // 呼叫invalidate()後,就會重新整理View,即才能看到重新繪製的介面,即onDraw()會被重新呼叫一次
                    // 所以座標值每改變一次,就會呼叫onDraw()一次
                }
            });

            anim.start();
            // 啟動動畫


        } else {
            // 如果座標值不為0,則畫圓
            // 所以座標值每改變一次,就會呼叫onDraw()一次,就會畫一次圓,從而實現動畫效果

            // 在該點畫一個圓:圓心 = (30,30),半徑 = 30
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);
        }
    }


}

步驟4:在佈局檔案加入自定義View空間

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="scut.carson_ho.valueanimator_ofobject.MainActivity">

    <scut.carson_ho.valueanimator_ofobject.MyView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
         />
</RelativeLayout>

步驟5:在主程式碼檔案設定顯示檢視

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

效果圖

效果圖

原始碼地址

Carson_Ho的Github地址:https://github.com/Carson-Ho/PropertyAnimator_ofObject

特別注意

從上面可以看出,其實ValueAnimator.ofObject()的本質還是操作 ** 值 **,只是是採用將 多個值 封裝到一個物件裡的方式 同時對多個值一起操作而已

就像上面的例子,本質還是操作座標中的x,y兩個值,只是將其封裝到Point物件裡,方便同時操作x,y兩個值而已


  • 至此,關於屬性動畫中最核心的 ValueAnimator類已經講解完畢
  • 下面我將繼續講解另外一個重要的類:ObjectAnimator

5.2 ObjectAnimator類

5.2.1 實現動畫的原理

直接對物件的屬性值進行改變操作,從而實現動畫效果

  1. 如直接改變 Viewalpha 屬性 從而實現透明度的動畫效果
  2. 繼承自ValueAnimator類,即底層的動畫實現機制是基於ValueAnimator
  • 本質原理: 通過不斷控制 值 的變化,再不斷 自動 賦給物件的屬性,從而實現動畫效果。如下圖:

工作原理

從上面的工作原理可以看出:ObjectAnimatorValueAnimator類的區別:

  • ValueAnimator 類是先改變值,然後 手動賦值 給物件的屬性從而實現動畫;是 間接 對物件屬性進行操作;
  • ObjectAnimator 類是先改變值,然後 自動賦值 給物件的屬性從而實現動畫;是 直接 對物件屬性進行操作;

至於是如何自動賦值給物件的屬性,下面會詳細說明

5.2.2 具體使用

由於是繼承了ValueAnimator類,所以使用的方法十分類似:XML 設定 / Java設定

設定方式1:Java 設定

ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);  

// ofFloat()作用有兩個
// 1. 建立動畫例項
// 2. 引數設定:引數說明如下
// Object object:需要操作的物件
// String property:需要操作的物件的屬性
// float ....values:動畫初始值 & 結束值(不固定長度)
// 若是兩個引數a,b,則動畫效果則是從屬性的a值到b值
// 若是三個引數a,b,c,則則動畫效果則是從屬性的a值到b值再到c值
// 以此類推
// 至於如何從初始值 過渡到 結束值,同樣是由估值器決定,此處ObjectAnimator.ofFloat()是有系統內建的浮點型估值器FloatEvaluator,同ValueAnimator講解

anim.setDuration(500);
        // 設定動畫執行的時長

        anim.setStartDelay(500);
        // 設定動畫延遲播放時間

        anim.setRepeatCount(0);
        // 設定動畫重複播放次數 = 重放次數+1
        // 動畫播放次數 = infinite時,動畫無限重複

        anim.setRepeatMode(ValueAnimator.RESTART);
        // 設定重複播放動畫模式
        // ValueAnimator.RESTART(預設):正序重放
        // ValueAnimator.REVERSE:倒序回放

animator.start();  
// 啟動動畫

設定方法2:在XML 程式碼中設定

  • 步驟1:在路徑 res/animator 的資料夾裡建立動畫效果.xml檔案

此處設定為res/animator/set_animation.xml

  • 步驟2:設定動畫引數

set_animation.xml

// ObjectAnimator 採用<animator>  標籤
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"  
    android:valueFrom="1"   // 初始值
    android:valueTo="0"  // 結束值
    android:valueType="floatType"  // 變化值型別 :floatType & intType
    android:propertyName="alpha" // 物件變化的屬性名稱

/>  

在Java程式碼中啟動動畫

Animator animator = AnimatorInflater.loadAnimator(context, R.animator.view_animation);  
// 載入XML動畫

animator.setTarget(view);  
// 設定動畫物件

animator.start();  
// 啟動動畫
  • 使用例項
    此處先展示四種基本變換:平移、旋轉、縮放 & 透明度

a. 透明度

mButton = (Button) findViewById(R.id.Button);
        // 建立動畫作用物件:此處以Button為例

        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f);
        // 表示的是:
        // 動畫作用物件是mButton
        // 動畫作用的物件的屬性是透明度alpha
        // 動畫效果是:常規 - 全透明 - 常規
        animator.setDuration(5000);
        animator.start();

屬性動畫 - 透明度.gif

b. 旋轉

mButton = (Button) findViewById(R.id.Button);
        // 建立動畫作用物件:此處以Button為例

  ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);

        // 表示的是:
        // 動畫作用物件是mButton
        // 動畫作用的物件的屬性是旋轉alpha
        // 動畫效果是:0 - 360
        animator.setDuration(5000);
        animator.start();

屬性動畫- 旋轉.gif

c. 平移

mButton = (Button) findViewById(R.id.Button);
        // 建立動畫作用物件:此處以Button為例

  float curTranslationX = mButton.getTranslationX();
        // 獲得當前按鈕的位置
        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);
        

        // 表示的是:
        // 動畫作用物件是mButton
        // 動畫作用的物件的屬性是X軸平移(在Y軸上平移同理,採用屬性"translationY"
        // 動畫效果是:從當前位置平移到 x=1500 再平移到初始位置
        animator.setDuration(5000);
        animator.start();

屬性動畫 - X軸平移.gif

d. 縮放

mButton = (Button) findViewById(R.id.Button);
        // 建立動畫作用物件:此處以Button為例

  ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "scaleX", 1f, 3f, 1f);
        // 表示的是:
        // 動畫作用物件是mButton
        // 動畫作用的物件的屬性是X軸縮放
        // 動畫效果是:放大到3倍,再縮小到初始大小
        animator.setDuration(5000);
        animator.start();

屬性動畫 - 縮放.gif


  • 在上面的講解,我們使用了屬性動畫最基本的四種動畫效果:透明度、平移、旋轉 & 縮放

即在ObjectAnimator.ofFloat()的第二個引數String property傳入alpharotationtranslationXscaleY 等blabla

屬性 作用 數值型別
Alpha 控制View的透明度 float
TranslationX 控制X方向的位移 float
TranslationY 控制Y方向的位移 float
ScaleX 控制X方向的縮放倍數 float
ScaleY 控制Y方向的縮放倍數 float
Rotation 控制以螢幕方向為軸的旋轉度數 float
RotationX 控制以X軸為軸的旋轉度數 float
RotationY 控制以Y軸為軸的旋轉度數 float

問題:那麼ofFloat()的第二個引數還能傳入什麼屬性值呢?
答案:任意屬性值。因為:

  • ObjectAnimator 類 對 物件屬性值 進行改變從而實現動畫效果的本質是:通過不斷控制 值 的變化,再不斷 自動 賦給物件的屬性,從而實現動畫效果

工作原理

  • 自動賦給物件的屬性的本質是呼叫該物件屬性的set() & get()方法進行賦值
  • 所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二個引數傳入值的作用是:讓ObjectAnimator類根據傳入的屬性名 去尋找 該物件對應屬性名的 set() & get()方法,從而進行物件屬性值的賦值,如上面的例子:
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);
// 其實Button物件中並沒有rotation這個屬性值
// ObjectAnimator並不是直接對我們傳入的屬性名進行操作
// 而是根據傳入的屬性值"rotation" 去尋找物件對應屬性名對應的get和set方法,從而通過set() &  get()對屬性進行賦值

// 因為Button物件中有rotation屬性所對應的get & set方法
// 所以傳入的rotation屬性是有效的
// 所以才能對rotation這個屬性進行操作賦值
public void setRotation(float value);  
public float getRotation();  

// 實際上,這兩個方法是由View物件提供的,所以任何繼承自View的物件都具備這個屬性


至於是如何進行自動賦值的,我們直接來看原始碼分析:

// 使用方法
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);  
anim.setDuration(500);
animator.start();  
// 啟動動畫,原始碼分析就直接從start()開始

<--  start()  -->
@Override  
public void start() {  
    AnimationHandler handler = sAnimationHandler.get();  

    if (handler != null) {  
        // 判斷等待動畫(Pending)中是否有和當前動畫相同的動畫,如果有就把相同的動畫給取消掉 
        numAnims = handler.mPendingAnimations.size();  
        for (int i = numAnims - 1; i >= 0; i--) {  
            if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {  
                ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);  
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {  
                    anim.cancel();  
                }  
            }  
        }  
      // 判斷延遲動畫(Delay)中是否有和當前動畫相同的動畫,如果有就把相同的動畫給取消掉 
        numAnims = handler.mDelayedAnims.size();  
        for (int i = numAnims - 1; i >= 0; i--) {  
            if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {  
                ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);  
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {  
                    anim.cancel();  
                }  
            }  
        }  
    }  
    
    super.start();  
   // 呼叫父類的start()
   // 因為ObjectAnimator類繼承ValueAnimator類,所以呼叫的是ValueAnimator的star()
   // 經過層層呼叫,最終會呼叫到 自動賦值給物件屬性值的方法
   // 下面就直接看該部分的方法
}  



<-- 自動賦值給物件屬性值的邏輯方法 ->>

// 步驟1:初始化動畫值
private void setupValue(Object target, Keyframe kf) {  
    if (mProperty != null) {  
        kf.setValue(mProperty.get(target));  
        // 初始化時,如果屬性的初始值沒有提供,則呼叫屬性的get()進行取值
    }  
        kf.setValue(mGetter.invoke(target));   
    }  
}  

// 步驟2:更新動畫值
// 當動畫下一幀來時(即動畫更新的時候),setAnimatedValue()都會被呼叫
void setAnimatedValue(Object target) {  
    if (mProperty != null) {  
        mProperty.set(target, getAnimatedValue());  
        // 內部呼叫物件該屬性的set()方法,從而從而將新的屬性值設定給物件屬性
    }  
    
}  

自動賦值的邏輯:

  1. 初始化時,如果屬性的初始值沒有提供,則呼叫屬性的 get()進行取值;
  2. 當 值 變化時,用物件該屬性的 set()方法,從而從而將新的屬性值設定給物件屬性。

所以:

  • ObjectAnimator 類針對的是任意物件 & 任意屬性值,並不是單單針對於View物件
  • 如果需要採用ObjectAnimator 類實現動畫效果,那麼需要操作的物件就必須有該屬性的set() & get()
  • 同理,針對上述另外的三種基本動畫效果,View 也存在著setRotation()getRotation()setTranslationX()getTranslationX()setScaleY()getScaleY()set() & get()

5.2.3 通過自定義物件屬性實現動畫效果

對於屬性動畫,其拓展性在於:不侷限於系統限定的動畫,可以自定義動畫,即自定義物件的屬性,並通過操作自定義的屬性從而實現動畫。

那麼,該如何自定義屬性呢?本質上,就是:

  • 為物件設定需要操作屬性的set() & get()方法
  • 通過實現TypeEvaluator類從而定義屬性變化的邏輯

類似於ValueAnimator的過程

下面,我將用一個例項來說明如何通過自定義屬性實現動畫效果

  • 實現的動畫效果:一個圓的顏色漸變

     

    屬性動畫 - 顏色變化

  • 自定義屬性的邏輯如下:(需要自定義屬性為圓的背景顏色)

自定義屬性的邏輯

步驟1:設定物件類屬性的set() & get()方法

設定物件類屬性的set() & get()有兩種方法:

  1. 通過繼承原始類,直接給類加上該屬性的 get()& set(),從而實現給物件加上該屬性的 get()& set()

  2. 通過包裝原始動畫物件,間接給物件加上該屬性的 get()&
    set()。即 用一個類來包裝原始物件

此處主要使用第一種方式進行展示。

關於第二種方式的使用,會在下一節進行詳細介紹。

MyView2.java

public class MyView2 extends View {
    // 設定需要用到的變數
    public static final float RADIUS = 100f;// 圓的半徑 = 100
    private Paint mPaint;// 繪圖畫筆

    private String color;
    // 設定背景顏色屬性

    // 設定背景顏色的get() & set()方法
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
        mPaint.setColor(Color.parseColor(color));
        // 將畫筆的顏色設定成方法引數傳入的顏色
        invalidate();
        // 呼叫了invalidate()方法,即畫筆顏色每次改變都會重新整理檢視,然後呼叫onDraw()方法重新繪製圓
        // 而因為每次呼叫onDraw()方法時畫筆的顏色都會改變,所以圓的顏色也會改變
    }


    // 構造方法(初始化畫筆)
    public MyView2(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化畫筆
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 複寫onDraw()從而實現繪製邏輯
    // 繪製邏輯:先在初始點畫圓,通過監聽當前座標值(currentPoint)的變化,每次變化都呼叫onDraw()重新繪製圓,從而實現圓的平移動畫效果
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(500, 500, RADIUS, mPaint);
    }
}

步驟2:在佈局檔案加入自定義View控制元件

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="scut.carson_ho.valueanimator_ofobject.MainActivity">

    <scut.carson_ho.valueanimator_ofobject.MyView2
        android:id="@+id/MyView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
         />
</RelativeLayout>

步驟3:根據需求實現TypeEvaluator介面

此處實現估值器的本質是:實現 顏色過渡的邏輯。

ColorEvaluator.java

public class ColorEvaluator implements TypeEvaluator {
    // 實現TypeEvaluator介面

    private int mCurrentRed;

    private int mCurrentGreen ;

    private int mCurrentBlue ;

    // 複寫evaluate()
    // 在evaluate()裡寫入物件動畫過渡的邏輯:此處是寫顏色過渡的邏輯
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 獲取到顏色的初始值和結束值
        String startColor = (String) startValue;
        String endColor = (String) endValue;

        // 通過字串擷取的方式將初始化顏色分為RGB三個部分,並將RGB的值轉換成十進位制數字
        // 那麼每個顏色的取值範圍就是0-255
        int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
        int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
        int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);

        int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
        int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
        int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);

        // 將初始化顏色的值定義為當前需要操作的顏色值
            mCurrentRed = startRed;
            mCurrentGreen = startGreen;
            mCurrentBlue = startBlue;


        // 計算初始顏色和結束顏色之間的差值
        // 該差值決定著顏色變化的快慢:初始顏色值和結束顏色值很相近,那麼顏色變化就會比較緩慢;否則,變化則很快
        // 具體如何根據差值來決定顏色變化快慢的邏輯寫在getCurrentColor()裡.
        int redDiff = Math.abs(startRed - endRed);
        int greenDiff = Math.abs(startGreen - endGreen);
        int blueDiff = Math.abs(startBlue - endBlue);
        int colorDiff = redDiff + greenDiff + blueDiff;
        if (mCurrentRed != endRed) {
            mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
                    fraction);
                    // getCurrentColor()決定如何根據差值來決定顏色變化的快慢 ->>關注1
        } else if (mCurrentGreen != endGreen) {
            mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
                    redDiff, fraction);
        } else if (mCurrentBlue != endBlue) {
            mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
                    redDiff + greenDiff, fraction);
        }
        // 將計算出的當前顏色的值組裝返回
        String currentColor = "#" + getHexString(mCurrentRed)
                + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);

        // 由於我們計算出的顏色是十進位制數字,所以需要轉換成十六進位制字串:呼叫getHexString()->>關注2
        // 最終將RGB顏色拼裝起來,並作為最終的結果返回
        return currentColor;
    }


    // 關注1:getCurrentColor()
    // 具體是根據fraction值來計算當前的顏色。

    private int getCurrentColor(int startColor, int endColor, int colorDiff,
                                int offset, float fraction) {
        int currentColor;
        if (startColor > endColor) {
            currentColor = (int) (startColor - (fraction * colorDiff - offset));
            if (currentColor < endColor) {
                currentColor = endColor;
            }
        } else {
            currentColor = (int) (startColor + (fraction * colorDiff - offset));
            if (currentColor > endColor) {
                currentColor = endColor;
            }
        }
        return currentColor;
    }

    // 關注2:將10進位制顏色值轉換成16進位制。
    private String getHexString(int value) {
        String hexString = Integer.toHexString(value);
        if (hexString.length() == 1) {
            hexString = "0" + hexString;
        }
        return hexString;
    }

}

步驟4:呼叫ObjectAnimator.ofObject()方法

MainActivity.java

public class MainActivity extends AppCompatActivity {

    MyView2 myView2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        myView2 = (MyView2) findViewById(R.id.MyView2);
        ObjectAnimator anim = ObjectAnimator.ofObject(myView2, "color", new ColorEvaluator(),
                "#0000FF", "#FF0000");
        // 設定自定義View物件、背景顏色屬性值 & 顏色估值器
        // 本質邏輯:
        // 步驟1:根據顏色估值器不斷 改變 值 
        // 步驟2:呼叫set()設定背景顏色的屬性值(實際上是通過畫筆進行顏色設定)
        // 步驟3:呼叫invalidate()重新整理檢視,即呼叫onDraw()重新繪製,從而實現動畫效果

        anim.setDuration(8000);
        anim.start();
    }
}

效果圖

屬性動畫 - 顏色變化

原始碼地址

Carson_Ho的Github地址


5.2.4 特別注意:如何手動設定物件類屬性的 set() & get()

a. 背景

  • ObjectAnimator 類 自動賦給物件的屬性 的本質是呼叫該物件屬性的set() & get()方法進行賦值
  • 所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二個引數傳入值的作用是:讓ObjectAnimator類根據傳入的屬性名 去尋找 該物件對應屬性名的 set() & get()方法,從而進行物件屬性值的賦值

從上面的原理可知,如果想讓物件的屬性a的動畫生效,屬性a需要同時滿足下面兩個條件:

  1. 物件必須要提供屬性a的set()方法

a. 如果沒傳遞初始值,那麼需要提供get()方法,因為系統要去拿屬性a的初始值
b. 若該條件不滿足,程式直接Crash

  1. 物件提供的 屬性a的set()方法 對 屬性a的改變 必須通過某種方法反映出來

a. 如帶來ui上的變化
b. 若這條不滿足,動畫無效,但不會Crash)

上述條件,一般第二條都會滿足,主要是在第一條

  1. 比如說:由於ViewsetWidth()並不是設定View的寬度,而是設定View的最大寬度和最小寬度的;所以通過setWidth()無法改變控制元件的寬度;所以對View檢視的width做屬性動畫沒有效果
  2. 具體請看下面Button按鈕的例子
       Button  mButton = (Button) findViewById(R.id.Button);
        // 建立動畫作用物件:此處以Button為例
        // 此Button的寬高設定具體為具體寬度200px

               ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();
                 // 設定動畫的物件

效果圖

效果圖:不會有動畫效果

為什麼沒有動畫效果呢?我們來看ViewsetWidth方法

public void setWidth(int pixels) {  
    mMaxWidth = mMinWidth = pixels;  
    mMaxWidthMode = mMinWidthMode = PIXELS;  
    // 因為setWidth()並不是設定View的寬度,而是設定Button的最大寬度和最小寬度的
    // 所以通過setWidth()無法改變控制元件的寬度
   // 所以對width屬性做屬性動畫沒有效果
  
    requestLayout();  
    invalidate();  
}  
  

@ViewDebug.ExportedProperty(category = "layout")  
public final int getWidth() {  
    return mRight - mLeft;  
    // getWidth的確是獲取View的寬度
}  

b. 問題

那麼,針對上述物件屬性的set()不是設定屬性 或 根本沒有set() / get ()的情況應該如何處理?

c. 解決方案

手動設定物件類屬性的set() & get()。共有兩種方法:

  1. 通過繼承原始類,直接給類加上該屬性的 get()& set(),從而實現給物件加上該屬性的 get()& set()

  2. 通過包裝原始動畫物件,間接給物件加上該屬性的 get()&
    set()。即 用一個類來包裝原始物件

對於第一種方法,在上面的例子已經說明;下面主要講解第二種方法:通過包裝原始動畫物件,間接給物件加上該屬性的get()& set()

本質上是採用了設計模式中的裝飾模式,即通過包裝類從而擴充套件物件的功能

還是採用上述 Button 按鈕的例子

public class MainActivity extends AppCompatActivity {
    Button mButton;
    ViewWrapper wrapper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.Button);
        // 建立動畫作用物件:此處以Button為例

        wrapper = new ViewWrapper(mButton);
        // 建立包裝類,並傳入動畫作用的物件
        
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(3000).start();
                // 設定動畫的物件是包裝類的物件
            }
        });

    }
    // 提供ViewWrapper類,用於包裝View物件
    // 本例:包裝Button物件
    private static class ViewWrapper {
        private View mTarget;

        // 構造方法:傳入需要包裝的物件
        public ViewWrapper(View target) {
            mTarget = target;
        }

        // 為寬度設定get() & set()
        public int getWidth() {
            return mTarget.getLayoutParams().width;
        }

        public void setWidth(int width) {
            mTarget.getLayoutParams().width = width;
            mTarget.requestLayout();
        }

    }

}

效果圖

效果圖 - 動畫有效


5.4 總結

  • 對比ValueAnimator類 & ObjectAnimator 類,其實二者都屬於屬性動畫,本質上都是一致的:先改變值,然後 賦值 給物件的屬性從而實現動畫效果。
  • 但二者的區別在於:
    ValueAnimator 類是先改變值,然後 手動賦值 給物件的屬性從而實現動畫;是 間接 對物件屬性進行操作;

ValueAnimator 類本質上是一種 改變 值 的操作機制

ObjectAnimator類是先改變值,然後 自動賦值 給物件的屬性從而實現動畫;是 直接 對物件屬性進行操作;

可以理解為:ObjectAnimator更加智慧、自動化程度更高


6. 額外的使用方法

6.1 組合動畫(AnimatorSet 類)

  • 單一動畫實現的效果相當有限,更多的使用場景是同時使用多種動畫效果,即組合動畫
  • 實現 組合動畫 的功能:AnimatorSet
  • 具體使用:
AnimatorSet.play(Animator anim)   :播放當前動畫
AnimatorSet.after(long delay)   :將現有動畫延遲x毫秒後執行
AnimatorSet.with(Animator anim)   :將現有動畫和傳入的動畫同時執行
AnimatorSet.after(Animator anim)   :將現有動畫插入到傳入的動畫之後執行
AnimatorSet.before(Animator anim) :  將現有動畫插入到傳入的動畫之前執行
  • 例項
    主要動畫是平移,平移過程中伴隨旋轉動畫,平移完後進行透明度變化

實現方式有 XML設定 / Java程式碼設定

設定方式1:Java程式碼設定

// 步驟1:設定需要組合的動畫效果
ObjectAnimator translation = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);  
// 平移動畫
ObjectAnimator rotate = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);  
// 旋轉動畫
ObjectAnimator alpha = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f);  
// 透明度動畫

// 步驟2:建立組合動畫的物件
AnimatorSet animSet = new AnimatorSet();  

// 步驟3:根據需求組合動畫
animSet.play(translation).with(rotate).before(alpha);  
animSet.setDuration(5000);  

// 步驟4:啟動動畫
animSet.start();  

效果圖

組合動畫.gif

設定方式2:XML設定

  • 步驟1:在 res/animator的資料夾裡建立動畫.xml檔案

此處為 res/animator/set_animation.xml

  • 步驟2:設定動畫效果

set_animation.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially" >
    // 表示Set集合內的動畫按順序進行
    // ordering的屬性值:sequentially & together
    // sequentially:表示set中的動畫,按照先後順序逐步進行(a 完成之後進行 b )
    // together:表示set中的動畫,在同一時間同時進行,為預設值

    <set android:ordering="together" >
        // 下面的動畫同時進行
        <objectAnimator
            android:duration="2000"
            android:propertyName="translationX"
            android:valueFrom="0"
            android:valueTo="300"
            android:valueType="floatType" >
        </objectAnimator>
        
        <objectAnimator
            android:duration="3000"
            android:propertyName="rotation"
            android:valueFrom="0"
            android:valueTo="360"
            android:valueType="floatType" >
        </objectAnimator>
    </set>

        <set android:ordering="sequentially" >
            // 下面的動畫按序進行