1. 程式人生 > >屬性動畫之—ObjectAnimator基本使用

屬性動畫之—ObjectAnimator基本使用

一、概述

1、引入

上幾篇給大家講了ValueAnimator,但ValueAnimator有個缺點,就是隻能對數值對動畫計算。我們要想對哪個控制元件操作,需要監聽動畫過程,在監聽中對控制元件操作。這樣使用起來相比補間動畫而言就相對比較麻煩。
為了能讓動畫直接與對應控制元件相關聯,以使我們從監聽動畫過程中解放出來,谷歌的開發人員在ValueAnimator的基礎上,又派生了一個類ObjectAnimator;
由於ObjectAnimator是派生自ValueAnimator的,所以ValueAnimator中所能使用的方法,在ObjectAnimator中都可以正常使用。
但ObjectAnimator也重寫了幾個方法,比如ofInt(),ofFloat()等。我們先看看利用ObjectAnimator重寫的ofFloat方法如何實現一個動畫:(改變透明度)
[java]
view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv,“alpha”,1,0,1);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv,"alpha",1,0,1); 
animator.setDuration(2000);
animator.start();
效果圖如下:

我們這裡還是直接使用上一篇的框架程式碼;(當點選start anim時執行動畫)從上面的程式碼中可以看到構造ObjectAnimator的方法非常簡單:

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. publicstatic ObjectAnimator ofFloat(Object target, String propertyName, float… values)   
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) 
  • 第一個引數用於指定這個動畫要操作的是哪個控制元件
  • 第二個引數用於指定這個動畫要操作這個控制元件的哪個屬性
  • 第三個引數是可變長引數,這個就跟ValueAnimator中的可變長引數的意義一樣了,就是指這個屬性值是從哪變到哪。
    像我們上面的程式碼中指定的就是將textview的alpha屬性從0變到1再變到0;
下面我們再來看一下如何實現旋轉效果:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv,“rotation”,0,180,0);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv,"rotation",0,180,0); 
animator.setDuration(2000);
animator.start();
效果圖如下:

從程式碼中可以看到,我們只需要改變ofFloat()的第二個引數的值就可以實現對應的動畫;
那麼問題來了,我們怎麼知道第二個引數的值是啥呢?

2、setter函式

我們再回來看構造改變rotation值的ObjectAnimator的方法
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv,“rotation”,0,180,0);  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv,"rotation",0,180,0);
TextView控制元件有rotation這個屬性嗎?沒有,不光TextView沒有,連它的父類View中也沒有這個屬性。那它是怎麼來改變這個值的呢?其實,ObjectAnimator做動畫,並不是根據控制元件xml中的屬性來改變的,而是通過指定屬性所對應的set方法來改變的。比如,我們上面指定的改變rotation的屬性值,ObjectAnimator在做動畫時就會到指定控制元件(TextView)中去找對應的setRotation()方法來改變控制元件中對應的值。同樣的道理,當我們在最開始的示例程式碼中,指定改變”alpha”屬性值的時候,ObjectAnimator也會到TextView中去找對應的setAlpha()方法。那TextView中都有這些方法嗎,有的,這些方法都是從View中繼承過來的,在View中有關動畫,總共有下面幾組set方法:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. //1、透明度:alpha
  2. publicvoid setAlpha(float alpha)  
  3. //2、旋轉度數:rotation、rotationX、rotationY
  4. publicvoid setRotation(float rotation)  
  5. publicvoid setRotationX(float rotationX)  
  6. publicvoid setRotationY(float rotationY)  
  7. //3、平移:translationX、translationY
  8. publicvoid setTranslationX(float translationX)   
  9. publicvoid setTranslationY(float translationY)  
  10. //縮放:scaleX、scaleY
  11. publicvoid setScaleX(float scaleX)  
  12. publicvoid setScaleY(float scaleY)  
//1、透明度:alpha 
public void setAlpha(float alpha)

//2、旋轉度數:rotation、rotationX、rotationY
public void setRotation(float rotation)
public void setRotationX(float rotationX)
public void setRotationY(float rotationY)

//3、平移:translationX、translationY
public void setTranslationX(float translationX)
public void setTranslationY(float translationY)

//縮放:scaleX、scaleY
public void setScaleX(float scaleX)
public void setScaleY(float scaleY)

可以看到在View中已經實現了有關alpha,rotaion,translate,scale相關的set方法。所以我們在構造ObjectAnimator時可以直接使用。
在開始逐個看這些函式的使用方法前,我們先做一個總結:
1、要使用ObjectAnimator來構造對畫,要操作的控制元件中,必須存在對應的屬性的set方法
2、setter 方法的命名必須以駱駝拼寫法命名,即set後每個單詞首字母大寫,其餘字母小寫,即類似於setPropertyName所對應的屬性為propertyName

下面我們就來看一下上面中各個方法的使用方法及作用。
有關alpha的用法,上面已經講過了,下面我們來看看其它的

(1)、setRotationX、setRotationY與setRotation

  • setRotationX(float rotationX):表示圍繞X軸旋轉,rotationX表示旋轉度數 
  • setRotationY(rotationY):表示圍繞Y軸旋轉,rotationY表示旋轉度數 
  • setRotation(float rotation):表示圍繞Z旋轉,rotation表示旋轉度數 
先來看看setRotationX的效果:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv,“rotationX”,0,270,0);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv,"rotationX",0,270,0); 
animator.setDuration(2000);
animator.start();

效果圖如下:


從效果圖中明顯看出,textview的旋轉方法是圍繞X軸旋轉的,我們設定為從0度旋轉到270度再返回0度。
然後再來看看setRotationY的使用方法與效果:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv,“rotationY”,0,180,0);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv,"rotationY",0,180,0); 
animator.setDuration(2000);
animator.start();

效果圖如下:


從效果圖中明顯可以看出圍繞Y軸旋轉的。
我們再來看看setRotation的用法與效果:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv,“rotation”,0,270,0);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv,"rotation",0,270,0); 
animator.setDuration(2000);
animator.start();

我們上面說了,setRotation是圍繞Z軸旋轉的,可能有些同學不理解什麼是Z軸,我們來看一張圖:


從這張圖中,綠色框部分表示手機螢幕,很明顯可以看出Z軸就是從螢幕左上角原點向外伸出的一條軸。這樣,我們也就可以理解圍繞Z軸旋轉,為什麼是這樣子轉了。

(2)、setTranslationX與setTranslationY

  • setTranslationX(float translationX) :表示在X軸上的平移距離,以當前控制元件為原點,向右為正方向,引數translationX表示移動的距離。 
  • setTranslationY(float translationY) :表示在Y軸上的平移距離,以當前控制元件為原點,向下為正方向,引數translationY表示移動的距離。 
我們先看看setTranslationX的用法:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv, “translationX”0200, -200,0);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv, "translationX", 0, 200, -200,0); 
animator.setDuration(2000);
animator.start();
效果圖如下:

所以,我們上面在構造動畫時,指定的移動距離是(0, 200, -200,0),所以控制元件會從自身所有位置向右移動200畫素,然後再移動到距離原點-200的位置,最後回到原點;
然後我們來看看setTranslateY的用法:

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv, “translationY”0200, -100,0);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv, "translationY", 0, 200, -100,0); 
animator.setDuration(2000);
animator.start();
效果圖如下:(為了方便看到效果,將textview垂直居中)

同樣,移動位置的座標也都是以當前控制元件所在位置為中心點的。所以對應的移動位置從原點移動向下移動200畫素,然後再移動到向下到距原點200畫素的位置,最後再回到(0,0)從效果圖中很明顯可以看出來。
從上面可以看出:每次移動距離的計算都是以原點為中心的;比如初始動畫為ObjectAnimator.ofFloat(tv, “translationY”, 0, 200, -100,0)表示首先從0移動到正方向200的位置,然後再移動到負方向100的位置,最後移動到原點。

(3)、setScaleX與setScaleY

  • setScaleX(float scaleX):在X軸上縮放,scaleX表示縮放倍數 
  • setScaleY(float scaleY):在Y軸上縮放,scaleY表示縮放倍數 
我們來看看setScaleX的用法:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv, “scaleX”031);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv, "scaleX", 0, 3, 1); 
animator.setDuration(2000);
animator.start();
效果圖如下:

在效果圖中,從0倍放大到3倍,然後再還原到1倍的原始狀態。
然後再來看看setScaleY的用法

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv, “scaleY”031);  
  2. animator.setDuration(2000);  
  3. animator.start();  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv, "scaleY", 0, 3, 1); 
animator.setDuration(2000);
animator.start();
為了更好的看到效果,我把textview垂直居中了,效果圖如下:

原始碼在文章底部給出 好了,到這裡有關View中自帶的set函式講完了,我們來看看ObjectAnimator是如何實現控制元件動畫效果的。

3、ObjectAnimator動畫原理

我們先來看張圖:


在這張圖中,將ValueAnimator的動畫流程與ObjectAnimator的動畫流程做了個對比。
可以看到ObjectAnimator的動畫流程中,也是首先通過加速器產生當前進度的百分比,然後再經過Evaluator生成對應百分比所對應的數字值。這兩步與ValueAnimator是完全一樣的,唯一不同的是最後一步,在ValueAnimator中,我們要通過新增監聽器來監聽當前數字值。而在ObjectAnimator中,則是先根據屬性值拼裝成對應的set函式的名字,比如這裡的scaleY的拼裝方法就是將屬性的第一個字母強制大寫後,與set拼接,所以就是setScaleY。然後通過反射找到對應控制元件的setScaleY(float scaleY)函式,將當前數字值做為setScaleY(float scale)的引數將其傳入。
這裡在找到控制元件的set函式以後,是通過反射來呼叫這個函式的,有關反射的使用大家可以參考《夯實JAVA基本之二 —— 反射(1):基本類周邊資訊獲取》
這就是ObjectAnimator的流程,最後一步總結起來就是呼叫對應屬性的set方法,將動畫當前數字值做為引數傳進去。
根據上面的流程,這裡有幾個注意事項:
(1)、拼接set函式的方法:上面我們也說了是首先是強制將屬性的第一個字母大寫,然後與set拼接,就是對應的set函式的名字。注意,只是強制將屬性的第一個字母大寫,後面的部分是保持不變的。反過來,如果我們的函式名命名為setScalePointX(float ),那我們在寫屬性時可以寫成”scalePointX”或者寫成“ScalePointX”都是可以的,即第一個字母大小寫可以隨意,但後面的部分必須與set方法後的大小寫保持一致。
(2)、如何確定函式的引數型別:上面我們知道了如何找到對應的函式名,那對應的引數方法的引數型別如何確定呢?我們在講ValueAnimator的時候說過,動畫過程中產生的數字值與構造時傳入的值型別是一樣的。由於ObjectAnimator與ValueAnimator在插值器和Evaluator這兩步是完全一樣的,而當前動畫數值的產生是在Evaluator這一步產生的,所以ObjectAnimator的動畫中產生的數值型別也是與構造時的型別一樣的。那麼問題來了,像我們的構造方法。

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. ObjectAnimator animator = ObjectAnimator.ofFloat(tv, “scaleY”031);  
ObjectAnimator animator = ObjectAnimator.ofFloat(tv, "scaleY", 0, 3, 1);
由於構造時使用的是ofFloat函式,所以中間值的型別應該是Float型別的,所以在最後一步拼裝出來的set函式應該是setScaleY(float xxx)的樣式;這時,系統就會利用反射來找到setScaleY(float xxx)函式,並把當前的動畫數值做為引數傳進去。
那問題來了,如果沒有類似setScaleY(float xxx)的函式,我們只實現了一個setScaleY(int xxx)的函式怎麼辦?這裡雖然函式名一樣,但引數型別是不一樣的,那麼系統就會報一個錯誤:

意思就是對應函式的指定引數型別沒有找到。
(3)、呼叫set函式以後怎麼辦?從ObjectAnimator的流程可以看到,ObjectAnimator只負責把動畫過程中的數值傳到對應屬性的set函式中就結束了,注意傳給set函式以後就結束了!set函式就相當我們在ValueAnimator中新增的監聽的作用,set函式中的對控制元件的操作還是需要我們自己來寫的。

那我們來看看View中的setScaleY是怎麼實現的吧:

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. / 
  2.   Sets the amount that the view is scaled in Y around the pivot point, as a proportion of 
  3.   the view’s unscaled width. A value of 1 means that no scaling is applied. 
  4.   @param scaleY The scaling factor. 
  5.   @see #getPivotX() 
  6.   @see #getPivotY() 
  7.   @attr ref android.R.styleable#View_scaleY 
  8.  */
  9. publicvoid setScaleY(float scaleY) {  
  10.     ensureTransformationInfo();  
  11.     final TransformationInfo info = mTransformationInfo;  
  12.     if (info.mScaleY != scaleY) {  
  13.         invalidateParentCaches();  
  14.         // Double-invalidation is necessary to capture view’s old and new areas
  15.         invalidate(false);  
  16.         info.mScaleY = scaleY;  
  17.         info.mMatrixDirty = true;  
  18.         mPrivateFlags |= DRAWN; // force another invalidation with the new orientation
  19.         invalidate(false);  
  20.     }  
  21. }  
/ 
* Sets the amount that the view is scaled in Y around the pivot point, as a proportion of
* the view's unscaled width. A value of 1 means that no scaling is applied.
*
* @param scaleY The scaling factor.
* @see #getPivotX()
* @see #getPivotY()
*
* @attr ref android.R.styleable#View_scaleY
*/
public void setScaleY(float scaleY) {
ensureTransformationInfo();
final TransformationInfo info = mTransformationInfo;
if (info.mScaleY != scaleY) {
invalidateParentCaches();
// Double-invalidation is necessary to capture view's old and new areas
invalidate(false);
info.mScaleY = scaleY;
info.mMatrixDirty = true;
mPrivateFlags |= DRAWN; // force another invalidation with the new orientation
invalidate(false);
}
}
大家不必理解這一坨程式碼的意義,因為這些程式碼是需要讀懂View的整體流程以後才能看得懂的,只需要跟著我的步驟來理解就行。這段程式碼總共分為兩部分:第一步重新設定當前控制元件的引數,第二步呼叫Invalidate()強制重繪;
所以在重繪時,控制元件就會根據最新的控制元件引數來繪製了,所以我們就看到當前控制元件被縮放了。
(4)、set函式呼叫頻率是多少:由於我們知道動畫在進行時,每隔十幾毫秒會重新整理一次,所以我們的set函式也會每隔十幾毫秒會被呼叫一次。
講了這麼多,就是為了強調一點:ObjectAnimator只負責把當前運動動畫的數值傳給set函式。至於set函式裡面怎麼來做,是我們自己的事了。
好了,在知道了ObjectAnimator的原理以後,下面就來看看如何自定義一個ObjectAnimator的屬性吧。

二、自定義ObjectAnimator屬性

上面我們已經看了使用View自帶的set函式所對應屬性的方法,而且理解了ObjectAnimator的動畫實現原理,下面我們來自定義一個屬性來看看實現效果吧。
我們在開始之前再來捋一下ObjectAnimator的動畫設定流程:ObjectAnimator需要指定操作的控制元件物件,在開始動畫時,到控制元件類中去尋找設定屬性所對應的set函式,然後把動畫中間值做為引數傳給這個set函式並執行它。
所以,我們說了,控制元件類中必須所要設定屬性所要對應的set函式。所以為了自由控制控制元件的實現,我們這裡自定義一個控制元件。大家知道在這個自定義控制元件中,肯定存在一個set函式與我們自定義的屬性相對應。
我們先來看看這段要實現的效果:

這個效果圖與我們上篇自定義控制元件實現的效果差不多,這個控制元件中存在一個圓形,也是在動畫時先將這個圓形放大,然後再將圓形還原。

1、儲存圓形資訊類——Point

為了,儲存圓形的資訊,我們先定義一個類:(Point.java)
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. publicclass Point {  
  2.     privateint mRadius;  
  3.     public Point(int radius){  
  4.         mRadius = radius;  
  5.     }  
  6.     publicint getRadius() {  
  7.         return mRadius;  
  8.     }  
  9.     publicvoid setRadius(int radius) {  
  10.         mRadius = radius;  
  11.     }  
  12. }  
public class Point { 
private int mRadius;
public Point(int radius){
    mRadius = radius;
}

public int getRadius() {
    return mRadius;
}

public void setRadius(int radius) {
    mRadius = radius;
}

}

這個類很好理解,只有一個成員變數mRadius,表示圓的半徑。

2、自定義控制元件——MyPointView

然後我們自定義一個控制元件MyPointView,完整程式碼如下:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. publicclass MyPointView extends View {  
  2.     private Point mPoint = new Point(100);  
  3.     public MyPointView(Context context, AttributeSet attrs) {  
  4.         super(context, attrs);  
  5.     }  
  6.     @Override
  7.     protectedvoid onDraw(Canvas canvas) {  
  8.         if (mPoint != null){  
  9.             Paint paint = new Paint();  
  10.             paint.setAntiAlias(true);  
  11.             paint.setColor(Color.RED);  
  12.             paint.setStyle(Paint.Style.FILL);  
  13.             canvas.drawCircle(300,300,mPoint.getRadius(),paint);  
  14.         }  
  15.         super.onDraw(canvas);  
  16.     }  
  17.     void setPointRadius(int radius){  
  18.         mPoint.setRadius(radius);  
  19.         invalidate();  
  20.     }  
  21. }  
public class MyPointView extends View { 
private Point mPoint = new Point(100);
public MyPointView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

@Override
protected void onDraw(Canvas canvas) {
    if (mPoint != null){
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(300,300,mPoint.getRadius(),paint);
    }
    super.onDraw(canvas);
}

void setPointRadius(int radius){
    mPoint.setRadius(radius);
    invalidate();
}

}

在這段程式碼中,首先來看我們前面講到的set函式:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. void setPointRadius(int radius){  
  2.     mPoint.setRadius(radius);  
  3.     invalidate();  
  4. }  
void setPointRadius(int radius){ 
mPoint.setRadius(radius);
invalidate();
}
第一點,這個set函式所對應的屬性應該是pointRadius或者PointRadius。前面我們已經講了第一個字母大小寫無所謂,後面的字母必須保持與set函式完全一致。
第二點,在setPointRadius中,先將當前動畫傳過來的值儲存到mPoint中,做為當前圓形的半徑。然後強制介面重新整理

在介面重新整理後,就開始執行onDraw()函式:
[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. @Override
  2. protectedvoid onDraw(Canvas canvas) {  
  3.     if (mPoint != null){  
  4.         Paint paint = new Paint();  
  5.         paint.setAntiAlias(true);  
  6.         paint.setColor(Color.RED);  
  7.         paint.setStyle(Paint.Style.FILL);  
  8.         canvas.drawCircle(300,300,mPoint.getRadius(),paint);  
  9.     }  
  10.     super.onDraw(canvas);  
  11. }  
@Override 
protected void onDraw(Canvas canvas) {
if (mPoint != null){
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(300,300,mPoint.getRadius(),paint);
}
super.onDraw(canvas);
}
在onDraw函式中,就是根據當前mPoint的半徑值在(300,300)點外畫一個圓;有關畫圓的知識,大家可以參考《android Graphics(一):概述及基本幾何圖形繪製》

3、使用MyPointView

首先,在MyActivity的佈局中新增MyPointView的使用(main.xml):
[html] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  1. <?xmlversion=“1.0”encoding=“utf-8”?>
  2.                 android:orientation=“vertical”
  3.                 android:layout_width=“fill_parent”
  4.                 android:layout_height=“fill_parent”>
  5.     <Button
  6.             android:id=“@+id/btn”
  7.             android:layout_width=“wrap_content”
  8.             android:layout_height=“wrap_content”
  9.             android:layout_alignParentLeft=“true”
  10.             android:padding=“10dp”
  11. <