1. 程式人生 > >淺析Android動畫(二),屬性動畫高階例項探究

淺析Android動畫(二),屬性動畫高階例項探究

轉載請註明出處!http://www.cnblogs.com/wondertwo/p/5312482.html

ObjectAnimator實現屬性動畫

為了寫好Android動畫這幾篇部落格,在動筆之前我是下過很大決心的,我對自己的要求是儘量把一個小知識點寫清楚寫明白,說白了就是相對於大而全的長篇大論,我更傾向於去寫小而美的部落格!為了保證在高產的同時能堅持每篇部落格質量上讓我滿意,我翻閱了很多大牛的部落格,這其中尤以郭霖大神的部落格我印象最為深刻,也給我帶來了很多啟發,在此表示感謝並在部落格的最後貼了出來供大家參考!另外值得一提的是,在這篇部落格的第三部分,會對兩個非常酷炫的屬性動畫例項進行分析,分別是桌面彈球動畫和仿Win10系統開機小圓點旋轉動畫,為了激發大家閱讀的興趣,我們先來看一下效果如何?

下面言歸正傳,要了解屬性動畫我還是習慣先去翻看谷歌的API文件介紹,相對檢視動畫Added in API level 1,屬性動畫Added in API level 11,那我們可能會糾結既然已經有了檢視動畫,為什麼還要加入屬性動畫呢?仔細來看屬性動畫雖然叫做動畫,但是其意義已經不僅僅侷限於實現炫酷的動畫啦,藉助於插值器(Interpolator)和估值器(TypeEvaluator),我們可以更具體的描述他:一種按照一定變化率對屬性值進行操作的機制,變化率就是依賴Interpolator控制,而值操作則是TypeEvaluator控制!由此可見,屬性動畫不是一般的強大,與檢視動畫的區別主要在以下幾點:

  • 屬性動畫作用的物件可以是任何一個Object物件,也就是說我們完全可以給任意Object物件設定屬性動畫,而這個物件可以不是一個View元件,也不管這個物件是否是可見的,而檢視動畫的作用物件只能是一個View物件,這是最大的不同;
  • 檢視動畫的一個致命缺陷就是,通過檢視動畫將一個View物件(比如一個TextViewButton)位置改編後,該物件的觸控事件的焦點依然在原位置,而這在實際的開發中是不能容忍的,屬性動畫就很好的解決了這一缺陷;
  • 屬性動畫可以控制動畫執行過程中的任意時刻的任意屬性值,這麼說可能不好理解,但是大家肯定知道,我在第一篇部落格 [淺析Android動畫(一),View動畫高階例項 http://www.cnblogs.com/wondertwo/p/5295976.html ] 中也提及,檢視動畫從本質上來說是一種補間動畫,他只對動畫的起始值和結束值進行賦值,而動畫中間執行過程中的屬性值則是系統幫我們計算的。那我們怎樣自己用程式碼控制動畫執行過程中的屬性值呢?屬性動畫就提供了很好地解決方案,就是自定義估值器;

那麼屬性動畫是怎樣完美的解決上述問題的呢?下面就開始學習屬性動畫的基本用法,我們來看屬性動畫的繼承關係,如下如所示:

顯然關注的焦點應該是ValueAnimatorObjectAnimator這兩個類啦,ObjectAnimator繼承自ValueAnimator,是屬性動畫中非常重要的一個實現類,通過ObjectAnimator類的靜態歐工廠方法來建立ObjectAnimator物件,這些靜態工廠方法包括:ObjectAnimator.ofFloat()ObjectAnimator.ofInt()等等,當然最為重要的一個靜態工廠方法是ObjectAnimator.ofObject(),可以接收一個Object物件併為其設定屬性動畫,瞬間高大上了有木有?這些靜態工廠方法接收的引數分別是:

  1. 要設定動畫的目標物件;
  2. 動畫的屬性型別;
  3. 一個或多個屬性值;當只指定一個屬性值,系統預設此值為結束值;當指定兩個屬性值,系統預設分別為起始值和結束值;當指定三個或三個以上時,系統預設線性插值;

ValueAnimator是整個屬性動畫機制當中最核心的一個類,前面我們已經提到了,屬性動畫的執行機制是通過不斷地對值進行操作來實現的,而初始值和結束值之間的動畫過渡就是由ValueAnimator這個類來負責計算的,ValueAnimator對過渡動畫值的計算依靠一個時間因子fraction,而這個時間因子fraction是系統由setDuration()方法設定的動畫執行時間通過計算得來的,所以ValueAnimator還負責管理動畫的持續時間、播放次數、播放模式、以及對動畫設定監聽器等,確實是一個非常重要的類。ValueAnimator使用起來也很簡單,會在本篇部落格的的二部分詳細講解!下面看一個例項:通過ValueAnimator的子類ObjectAnimator實現影子效果,類似於西遊記中的元神出竅,哈哈是不是很好玩?先看效果如下:

那這個效果是怎麼實現的呢?你所看到的四個影子並不是真實的影子,而是把四張相同的圖片設定了半透明效果,並同時向四個不同的方向做位移變換。佈局很簡單,就是在根佈局RelativeLayout中放置五個ImageViewsrc值都引用同一個圖片的資源id,這樣五張圖片就會重疊在一起,因此造成了你看到的只有一張圖片的假象,動畫實現類EffectAni的程式碼如下:

package com.wondertwo.effect;

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.animation.BounceInterpolator;
import android.widget.ImageView;
import android.widget.Toast;

import com.wondertwo.R;

import java.util.ArrayList;


/**
 * 屬性動畫PropertyAni
 *
 * 常用的屬性動畫的屬性值有:
 *       - translationX、translationY----控制view物件相對其左上角座標在X、Y軸上偏移的距離
 *       - rotation、rotationX、rotationY----控制view物件繞支點進行2D和3D旋轉
 *       - scaleX、scaleY----控制view物件繞支點進行2D縮放
 *       - pivotX、pivotY----控制view物件的支點位置,這個位置一般就是view物件的中心點。圍繞這個支點可以進行旋轉和縮放處理
 *       - x、y----描述view物件在容器中的最終位置,是最初的左上角座標和translationX、translationY值的累計和
 *       - alpha----表示view物件的透明度。預設值是1(完全透明)、0(不透明)
 *
 * Created by wondertwo on 2016/3/11.
 */
public class EffectAni extends AppCompatActivity implements View.OnClickListener {

    // ImageView元件id陣列
    private int[] mRes = new int[]{R.id.iv_a, R.id.iv_b, R.id.iv_c, R.id.iv_d, R.id.iv_e};
    // ImageView物件集合
    private ArrayList<ImageView> mImViews = new ArrayList<>();
    private boolean flag = true;// 啟動動畫、關閉動畫的標記位

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_effect);
        // for迴圈建立ImageView物件,並新增到集合中
        for (int i = 0; i < mRes.length; i++) {
            ImageView iv_a = (ImageView) findViewById(mRes[i]);
            iv_a.setOnClickListener(this);
            mImViews.add(iv_a);
        }
    }

    // 按鈕點選事件
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.iv_a:
                if (flag) {
                    startAnim();
                } else {
                    closeAnim();
                }
                break;
            default:
                Toast.makeText(EffectAni.this, "" + v.getId(), Toast.LENGTH_SHORT).show();
                break;
        }
    }

    // 關閉動畫
    private void closeAnim() {
        // 建立ObjectAnimator屬性物件,引數分別是動畫要設定的View物件、動畫屬性、屬性值
        ObjectAnimator animator0 = ObjectAnimator.ofFloat(mImViews.get(0),
                                                            "alpha",
                                                            0.5F,
                                                            1F);
        ObjectAnimator animator1 = ObjectAnimator.ofFloat(mImViews.get(1),
                                                            "translationY",
                                                            200F,
                                                            0);
        ObjectAnimator animator2 = ObjectAnimator.ofFloat(mImViews.get(2),
                                                            "translationX",
                                                            200F,
                                                            0);
        ObjectAnimator animator3 = ObjectAnimator.ofFloat(mImViews.get(3),
                                                            "translationY",
                                                            -200F,
                                                            0);
        ObjectAnimator animator4 = ObjectAnimator.ofFloat(mImViews.get(4),
                                                            "translationX",
                                                            -200F,
                                                            0);
        AnimatorSet aniSet = new AnimatorSet();
        aniSet.setDuration(4000);
        aniSet.setInterpolator(new BounceInterpolator());// 彈跳效果的插值器
        aniSet.playTogether(animator0,
                            animator1,
                            animator2,
                            animator3,
                            animator4);// 同時啟動5個動畫
        aniSet.start();

        // 重置標記位
        flag = true;
    }

    // 啟動動畫
    private void startAnim() {
        // 建立ObjectAnimator屬性物件,引數分別是動畫要設定的View物件、動畫屬性、屬性值
        ObjectAnimator animator0 = ObjectAnimator.ofFloat(
                                                        mImViews.get(0),
                                                        "alpha",
                                                        1f,
                                                        0.5f);
        ObjectAnimator animator1 = ObjectAnimator.ofFloat(
                                                        mImViews.get(1),
                                                        "translationY",
                                                        200f);
        ObjectAnimator animator2 = ObjectAnimator.ofFloat(
                                                        mImViews.get(2),
                                                        "translationX",
                                                        200f);
        ObjectAnimator animator3 = ObjectAnimator.ofFloat(
                                                        mImViews.get(3),
                                                        "translationY",
                                                        -200f);
        ObjectAnimator animator4 = ObjectAnimator.ofFloat(
                                                        mImViews.get(4),
                                                        "translationX",
                                                        -200f);
        AnimatorSet aniSet = new AnimatorSet();
        aniSet.setDuration(4000);
        aniSet.setInterpolator(new BounceInterpolator());// 彈跳效果的插值器
        aniSet.playTogether(animator0,
                            animator1,
                            animator2,
                            animator3,
                            animator4);// 同時啟動5個動畫
        aniSet.start();

        // 重置標記位
        flag = false;
    }
}

上面程式碼看起來挺長,但是實現屬性動畫的程式碼也沒幾行程式碼,屬性動畫的邏輯就是startAnim()closeAnim()這兩個方法,先來看啟動動畫的方法startAnim(),上面也提到過,首先是通過ObjectAnimator.ofFloat()靜態工廠方法建立ObjectAnimator物件,可以看到上面一共建立了5個ObjectAnimator物件,傳入的第一個引數就是我們佈局檔案中的五個ImageView,但是注意了,給他們設定的屬性動畫卻是各不相同的,對應的第二個引數就表示要設定的屬性對應的字串,系統會自動解析它們對應的是哪個屬性,我們分別傳入了"alpha""translationY""translationX",你肯定很清楚是用來設定透明度、Y軸方向平移、X軸方向平移的。除了上述幾個,還可以傳入"rotation""rotationY""rotationX""rotationZ""scaleY""scaleX"等等,而關閉動畫方法closeAnim()的作用正好相反,把剛才變化的動畫移回原位。最後我們註冊了點選事件,判斷一個布林型的標記位的值,使響應事件分別呼叫開啟動畫和關閉動畫這兩個方法。

以上介紹的這些就是ObjectAnimator的最基本的用法,同時也是Android開發者進階必須掌握的內容,當然作為一個很有上進心的開發者,要求當然不能是隻是會用基本用法這麼低,我們再繼續來學習屬性動畫的監聽和ValueAnimator的用法,這就比上面的要高階很多了。

ValueAnimator和屬性動畫的監聽

上面在學習ObjectAnimator的使用方法的同時,已經對ValueAnimator的繼承關係進行了初步的介紹,下面更進一步,我們一起來更深入的學習ValueAnimator,如果要用一句話來概括ValueAnimator的特性,可以這樣概括,在屬性動畫的執行過程中,不斷計算並修改動畫的屬性值。這樣說可能比較晦澀難懂,但接下來就會有一個例項,看完這個例項你就明白為什麼會這樣描述ValueAnimator

考慮這樣一個場景,我們希望在6秒內把一個view控制元件的背景顏色從從紅色漸變到藍色,我們該怎樣去實現它呢?不用想了,前面學過的檢視動畫完成不了這個需求,就是這樣一個簡單的不能再簡單的需求,就能看出檢視動畫的侷限性了。對的,那我們還有屬性動畫呢,用屬性動畫就可以輕鬆實現上面的場景啦,不過在此之前我們還需要了解一下屬性動畫的原理到底是什麼?

屬性動畫需要不斷改變物件的某個屬性值,從而達到動畫的效果,那麼問題來了,既然是改變物件的屬性值,比如上面所說的view控制元件的背景顏色這一屬性的值,那麼背景顏色這個屬性一定是要不斷地反覆的被賦值並在手機螢幕上顯示出來的,注意上面的這句話,我們首先應該畫出的關鍵字是“不斷地被賦值”這幾個字,說到賦值,Android系統最常見的取值、賦值方法自然是settergetter方法啦,所以自然而然的,屬性動畫要求定義的屬性必須有settergetter方法,就算沒有getter方法在某些特殊的情況下是允許的,但是所有情況下setter方法必須要有,如果系統沒有提供那就需要我們自己動手去寫setter方法啦!其次對於上面那句話我們畫出的關鍵字,你是否注意到有“不斷地”這三個字?不斷地賦值那麼這些值是怎麼來的呢?我可以告訴你有兩種方式來不斷得到這些值:

  • ValueAnimator物件設定動畫監聽,程式碼如下所示:valueAnimator.addUpdateListener(),需要傳入一個AnimatorUpdateListener物件,一般我們傳入的是AnimatorUpdateListener的匿名物件,即:valueAnimator.addUpdateListener(new AnimatorUpdateListener(){...}),需要重寫它的onAnimationUpdate()方法,那麼上述值的計算邏輯就放在onAnimationUpdate()方法體內;
  • 重寫TypeEvaluatorTypeEvaluator這個詞直譯過來就是型別值演算法,也被譯作估值器,我覺得這個叫法很形象,因為他就是用來計算屬性動畫某個時刻的屬性值的具體值的,關於估值器和插值器我會在下一篇部落格中詳細介紹;

現在繼續來完成上面提到的這個場景,這也是我認為本篇部落格最值得一看的地方:在6秒內把一個view控制元件的背景顏色從從紅色漸變到藍色。先來看效果圖如下,這裡我們實現的是把一個按鈕的背景顏色從藍色漸變到紅色,並且同時做縮放動畫,效果還是很明顯的。

佈局檔案很簡單,在RelativeLayout中定義了一個Button,我們來看主要程式碼,實現這種效果的程式碼BuleToRed.java我貼出來,下面會詳細分析實現的細節:

package com.wondertwo.propertyanime;

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

/**
 * BuleToRed實現目標物件背景色的漸變
 * Created by wondertwo on 2016/3/23.
 */
public class BuleToRed extends Activity {

    private Button targetView;
    private int mCurrentRed = -1;
    private int mCurrentGreen = -1;
    private int mCurrentBlue = -1;

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

        targetView = (Button) findViewById(R.id.tv_color_backgroound);

        /**
         * 註冊點選事件,展示效果
         */
        targetView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                displayResult(targetView, "#0000ff", "#ff0000");
            }
        });
    }

    /**
     * displayResult()展示結果
     */
    private void displayResult(final View target, final String start, final String end) {
        // 建立ValueAnimator物件,實現顏色漸變
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 100f);

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 獲取當前動畫的進度值,1~100
                float currentValue = (float) animation.getAnimatedValue();
                Log.d("當前動畫值", "current value : " + currentValue);

                // 獲取動畫當前時間流逝的百分比,範圍在0~1之間
                float fraction = animation.getAnimatedFraction();
                // 直接呼叫evaluateForColor()方法,通過百分比計算出對應的顏色值
                String colorResult = evaluateForColor(fraction, start, end);

                /**
                 * 通過Color.parseColor(colorResult)解析字串顏色值,傳給ColorDrawable,建立ColorDrawable物件
                 */
                /*LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) target.getLayoutParams();*/
                ColorDrawable colorDrawable = new ColorDrawable(Color.parseColor(colorResult));
                // 把ColorDrawable物件設定為target的背景
                target.setBackground(colorDrawable);
                target.invalidate();
            }
        });
        valueAnimator.setDuration(6 * 1000);


        // 組裝縮放動畫
        ValueAnimator animator_1 = ObjectAnimator.ofFloat(target, "scaleX", 1f, 0.5f);
        ValueAnimator animator_2 = ObjectAnimator.ofFloat(target, "scaleY", 1f, 0.5f);
        ValueAnimator animator_3 = ObjectAnimator.ofFloat(target, "scaleX", 0.5f, 1f);
        ValueAnimator animator_4 = ObjectAnimator.ofFloat(target, "scaleY", 0.5f, 1f);
        AnimatorSet set_1 = new AnimatorSet();
        set_1.play(animator_1).with(animator_2);
        AnimatorSet set_2 = new AnimatorSet();
        set_2.play(animator_3).with(animator_4);
        AnimatorSet set_3 = new AnimatorSet();
        set_3.play(set_1).before(set_2);
        set_3.setDuration(3 * 1000);

        // 組裝顏色動畫和縮放動畫,並啟動動畫
        AnimatorSet set_4 = new AnimatorSet();
        set_4.play(valueAnimator).with(set_3);
        set_4.start();
    }

    /**
     * evaluateForColor()計算顏色值並返回
     */
    private String evaluateForColor(float fraction, String startValue, String endValue) {

        String startColor = startValue;
        String endColor = endValue;
        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);

        // 初始化顏色的值
        if (mCurrentRed == -1) {
            mCurrentRed = startRed;
        }
        if (mCurrentGreen == -1) {
            mCurrentGreen = startGreen;
        }
        if (mCurrentBlue == -1) {
            mCurrentBlue = startBlue;
        }

        // 計算初始顏色和結束顏色之間的差值
        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);
        } 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);
        return currentColor;
    }

    /**
     * 根據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;
    }

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

乍一看起來似乎程式碼很長很繁瑣,有170多行,但是別急,都是你見過的知道的東西,所以分析起來會很簡單!在BuleToRed.java類中,首先我們在onCreate()方法中拿到目標物件,也就是我們定義的Button物件,可以看到屬性動畫的啟動入口就是Button的點選事件中displayResult()方法,再往下看我們發現displayResult(final View target, final String start, final String end)方法接收三個引數,分別是:

  • 要設定顏色漸變的目標物件的例項,這裡我們直接傳入了Button按鈕的例項物件;
  • 顏色起始值,我們傳入"#0000ff",即藍色;
  • 顏色結束值,我們傳入"#ff0000",即紅色;

displayResult()方法中,先是建立ValueAnimator物件用於實現顏色漸變的動畫效果,我們為ValueAnimator物件valueAnimator設定了監聽器ValueAnimator.AnimatorUpdateListener(),動畫的執行過程中會不斷回撥AnimatorUpdateListener()中的onAnimationUpdate(ValueAnimator animation)方法,所以要實現背景顏色的漸變效果,則控制顏色漸變的邏輯必須要放在onAnimationUpdate()中,緊接著我們在onAnimationUpdate()中通過animation.getAnimatedValue()拿到監聽的數值,程式碼如下:

// 獲取當前動畫的進度值,1~100
float currentValue = (float) animation.getAnimatedValue();
Log.d("當前動畫值", "current value : " + currentValue);

並獲取當前時間流逝所佔的百分比引數fraction,接著呼叫evaluateForColor(fraction, start, end)方法,這個方法就是專門負責計算當前對應的顏色值,需要傳入我們剛才計算出來的fraction引數,程式碼如下:

// 獲取動畫當前時間流逝的百分比,範圍在0~1之間
float fraction = animation.getAnimatedFraction();
// 直接呼叫evaluateForColor()方法,通過百分比計算出對應的顏色值
String colorResult = evaluateForColor(fraction, start, end);

很自然的,我們用String colorResult來接受evaluateForColor()方法返回的顏色值,是一個十六進位制的字串,到這裡你可能會糾結我們怎麼才能把這個字串解析出來並設定給目標物件呢?程式碼如下:

/**
 * 通過Color.parseColor(colorResult)解析字串顏色值,傳給ColorDrawable,建立ColorDrawable物件
 */
ColorDrawable colorDrawable = new ColorDrawable(Color.parseColor(colorResult));
// 把ColorDrawable物件設定為target的背景
target.setBackground(colorDrawable);
target.invalidate();

給目標物件設定背景target.setBackground(colorDrawable)setBackground()方法接收一個Drawable物件,順水推舟我們很容易就會聯想到ColorDrawableColorDrawableDrawable介面的一個實現類,我們只需要建立一個ColorDrawable物件並把它傳給setBackground()方法就OK,而ColorDrawable的構造方法需要接收一個int型別的顏色值,這個好辦,我們用Color類的靜態工廠方法parseColor()把字串顏色值colorResult解析成int型別的顏色值傳進去就好,程式碼是這樣的:Color.parseColor(colorResult),到這裡displayResult()方法就講完了,因為後面的那幾行程式碼已經出現過好多次了,就是在顏色漸變的同時給目標物件再加一個縮放的動畫效果。

接下來我還想再補充一下,把evaluateForColor()方法計算顏色值的具體過程在這裡分析一下,其實計算顏色值的邏輯也不復雜,首先計算出紅綠藍三種顏色的對應的初始值和結束值,然後根據初始值和結束值之間的差值來計算當前對應的顏色值,getCurrentColor(startGreen, endGreen, colorDiff, redDiff, fraction)就是完成這個計算的邏輯所在,接著把計算得到的三種顏色值的int型資料轉換為十六進位制字串資料,並把它們組裝在一起後返回,而getHexString()方法就負責將int型顏色值資料轉換為十六進位制資料。

桌面彈球和Win10開機小圓點旋轉動畫的例項探究

我們先來分析簡單一點的Win10開機小圓點旋轉動畫,用過Win10系統的同學都應該知道,Win10開機系統初始化的時候會顯示一圈環形小圓點旋轉的動畫,相信這個動畫效果我一說你肯定歷歷在目記憶猶新,先來看一下最終的效果圖如下:

作為對ObjectAniumator的用法的高階探究,其實他還是很簡單的,佈局檔案先定義了4個小圓點ImageView,把每個小圓點ImageView都放在了一個LinearLayout中,這很簡單!說到繪製小圓點,我比較推薦的一種做法是在res/drawable目錄下直接通過xml定義shape資原始檔,這樣定義的好處是可以避免使用圖片資源造成不必要的記憶體佔用。這裡我把我的小圓點定義程式碼貼一下:

<?xml version="1.0" encoding="utf-8"?>
<shape 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid android:color="@android:color/holo_red_dark" />

</shape>

實際上我們定義的只是一個橢圓,要顯示出小圓點我們需要指定它的寬高相等,即android:layout_widthandroid:layout_height的值要相等,否則就會顯示成橢圓。然後只需要像引用圖片資源一樣,在drawable目錄下引用它就好,比如:

<LinearLayout
    android:id="@+id/ll_point_circle_4"
    android:layout_width="wrap_content"
    android:layout_height="240dp"
    android:layout_centerInParent="true"
    android:orientation="vertical">
    <ImageView
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:src="@drawable/shape_point" />
</LinearLayout>

下面是完整的佈局檔案,僅供參考:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/start_ani_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="@string/start_ani" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerVertical="true">

        <LinearLayout
            android:id="@+id/ll_point_circle_1"
            android:layout_width="wrap_content"
            android:layout_height="240dp"
            android:layout_centerInParent="true"
            android:orientation="vertical">
            <ImageView
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:src="@drawable/shape_point"
                />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_point_circle_2"
            android:layout_width="wrap_content"
            android:layout_height="240dp"
            android:layout_centerInParent="true"
            android:orientation="vertical">
            <ImageView
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:src="@drawable/shape_point"
                />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_point_circle_3"
            android:layout_width="wrap_content"
            android:layout_height="240dp"
            android:layout_centerInParent="true"
            android:orientation="vertical">
            <ImageView
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:src="@drawable/shape_point"
                />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/ll_point_circle_4"
            android:layout_width="wrap_content"
            android:layout_height="240dp"
            android:layout_centerInParent="true"
            android:orientation="vertical">
            <ImageView
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:src="@drawable/shape_point"
                />
        </LinearLayout>

    </RelativeLayout>

</RelativeLayout>

接著把CircleProgress屬性動畫類的程式碼貼出來,並在CircleProgress屬性動畫類中拿到上面4個小圓點的物件,動畫實現的細節會在程式碼後面詳細講解:

package com.wondertwo.propertyanime;

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Button;
import android.widget.LinearLayout;

/**
 * ObjectAnimator高階例項探究
 * Created by wondertwo on 2016/3/22.
 */
public class CircleProgress extends Activity {

    private LinearLayout mPoint_1;
    private LinearLayout mPoint_2;
    private LinearLayout mPoint_3;
    private LinearLayout mPoint_4;

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

        mPoint_1 = (LinearLayout) findViewById(R.id.ll_point_circle_1);
        mPoint_2 = (LinearLayout) findViewById(R.id.ll_point_circle_2);
        mPoint_3 = (LinearLayout) findViewById(R.id.ll_point_circle_3);
        mPoint_4 = (LinearLayout) findViewById(R.id.ll_point_circle_4);

        Button startAni = (Button) findViewById(R.id.start_ani_2);
        startAni.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                beginPropertyAni();
            }
        });
    }

    /**
     * 開啟動畫
     */
    private void beginPropertyAni() {
        ObjectAnimator animator_1 = ObjectAnimator.ofFloat(
                mPoint_1,
                "rotation",
                0,
                360);
        animator_1.setDuration(2000);
        animator_1.setInterpolator(new AccelerateDecelerateInterpolator());

        ObjectAnimator animator_2 = ObjectAnimator.ofFloat(
                mPoint_2,
                "rotation",
                0,
                360);
        animator_2.setStartDelay(150);
        animator_2.setDuration(2000 + 150);
        animator_2.setInterpolator(new AccelerateDecelerateInterpolator());

        ObjectAnimator animator_3 = ObjectAnimator.ofFloat(
                mPoint_3,
                "rotation",
                0,
                360);
        animator_3.setStartDelay(2 * 150);
        animator_3.setDuration(2000 + 2 * 150);
        animator_3.setInterpolator(new AccelerateDecelerateInterpolator());

        ObjectAnimator animator_4 = ObjectAnimator.ofFloat(
                mPoint_4,
                "rotation",
                0,
                360);
        animator_4.setStartDelay(3 * 150);
        animator_4.setDuration(2000 + 3 * 150);
        animator_4.setInterpolator(new AccelerateDecelerateInterpolator());

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.play(animator_1).with(animator_2).with(animator_3).with(animator_4);
        animatorSet.start();
    }
}

程式碼確實不長只有80多行,但是麻雀雖小五臟俱全,很顯然beginPropertyAni()方法就是啟動動畫的方法,呼叫ObjectAnimator.ofFloat()靜態工廠方法建立ObjectAnimator物件我就不解釋了,很容易看懂!重點來了,Win10開機小圓點旋轉動畫的難點不在旋轉,如果我們把旋轉的最高點看作是旋轉的起始點,小圓點的旋轉是一個先加速後減速的過程,這恰好符合高中物理的規律,小球內切圓環軌道做圓周運動,不知道我這樣解釋是不是很形象呢?那麼控制旋轉的加速度很好辦,只要設定一個AccelerateDecelerateInterpolator()插值器就OK,但是我們發現,這不是一個小球在旋轉,而是有4個同時在旋轉,而且旋轉還不同步,這又該如何解決呢?你只要從第二個小球開始,每個小球設定固定時間間隔的延時啟動,就能完美解決上面的問題。程式碼是這樣的:

animator_2.setStartDelay(150);
animator_3.setStartDelay(2 * 150);
animator_4.setStartDelay(3 * 150);

寫到這裡已經三萬字了,最後一起來學習一個桌面彈球動畫,這也是這篇部落格的收尾工作。老習慣我們還是先展示桌面彈球動畫的酷炫效果吧:

在動畫中可以清晰的看到小球下落過程中的加速運動,碰到桌面(手機螢幕的底部)後的變形壓扁,以及小球彈起的動畫,非常形象生動!先貼程式碼後面再做分析:

package com.wondertwo.propertyanime;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;

import java.util.ArrayList;

/**
 * 小球下落動畫加強版XBallsFallActivity,增加了小球桌底時的壓扁、回彈動畫
 * Created by wondertwo on 2016/3/20.
 */
public class XBallsFallActivity extends Activity {

    static final float BALL_SIZE = 50f;// 小球直徑
    static final float FULL_TIME = 1000;// 下落時間

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_x_ball_fall);

        LinearLayout xContainer = (LinearLayout) findViewById(R.id.xcontainer);

        // 設定要顯示的view元件
        xContainer.addView(new XBallView(this));
    }

    /**
     * 自定義動畫元件XBallView
     */
    public class XBallView extends View implements ValueAnimator.AnimatorUpdateListener {

        public final ArrayList<XShapeHolder> balls = new ArrayList<>();// 建立balls集合來儲存XShapeHolder物件

        public XBallView(Context context) {
            super(context);
            setBackgroundColor(Color.WHITE);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            // 遮蔽ACTION_UP事件
            if (event.getAction() != MotionEvent.ACTION_DOWN && event.getAction() != MotionEvent.ACTION_MOVE) {
                return false;
            }
            // 在ACTION_DOWN事件發生點生成小球
            XShapeHolder newBall = addBall(event.getX(), event.getY());
            // 計算小球下落動畫開始時Y座標
            float startY = newBall.getY();
            // 計算小球下落動畫結束時的Y座標,即螢幕高度減去startY
            float endY = getHeight() - BALL_SIZE;
            // 獲取螢幕高度
            float h = (float) getHeight();
            float eventY = event.getY();
            // 計算動畫持續時間
            int duration = (int) (FULL_TIME * ((h - eventY) / h));

            /**
             * 下面開始定義小球的下落,著地壓扁,反彈等屬性動畫
             */
            // 定義小球下落動畫
            ValueAnimator fallAni = ObjectAnimator.ofFloat(
                    newBall,
                    "y",
                    startY,
                    endY);
            // 設定動畫持續時間
            fallAni.setDuration(duration);
            // 設定加速插值器
            fallAni.setInterpolator(new AccelerateInterpolator());
            // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
            fallAni.addUpdateListener(this);

            // 定義小球壓扁動畫,控制小球x座標左移半個球寬度
            ValueAnimator squashshAni1 = ObjectAnimator.ofFloat(
                    newBall,
                    "x",
                    newBall.getX(),
                    newBall.getX() - BALL_SIZE / 2);
            squashshAni1.setDuration(duration / 4);
            squashshAni1.setRepeatCount(1);
            squashshAni1.setRepeatMode(ValueAnimator.REVERSE);
            squashshAni1.setInterpolator(new DecelerateInterpolator());
            // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
            squashshAni1.addUpdateListener(this);

            // 定義小球壓扁動畫,控制小球寬度加倍
            ValueAnimator squashshAni2 = ObjectAnimator.ofFloat(
                    newBall,
                    "width",
                    newBall.getWidth(),
                    newBall.getWidth() + BALL_SIZE);
            squashshAni2.setDuration(duration / 4);
            squashshAni2.setRepeatCount(1);
            squashshAni2.setRepeatMode(ValueAnimator.REVERSE);
            squashshAni2.setInterpolator(new DecelerateInterpolator());
            // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
            squashshAni2.addUpdateListener(this);

            // 定義小球拉伸動畫, 控制小球的y座標下移半個球高度
            ValueAnimator stretchAni1 = ObjectAnimator.ofFloat(
                    newBall,
                    "y",
                    endY,
                    endY + BALL_SIZE / 2);
            stretchAni1.setDuration(duration / 4);
            stretchAni1.setRepeatCount(1);
            stretchAni1.setRepeatMode(ValueAnimator.REVERSE);
            stretchAni1.setInterpolator(new DecelerateInterpolator());
            // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
            stretchAni1.addUpdateListener(this);

            // 定義小球拉伸動畫, 控制小球的高度減半
            ValueAnimator stretchAni2 = ObjectAnimator.ofFloat(
                    newBall,
                    "height",
                    newBall.getHeight(),
                    newBall.getHeight() - BALL_SIZE / 2);
            stretchAni2.setDuration(duration / 4);
            stretchAni2.setRepeatCount(1);
            stretchAni2.setRepeatMode(ValueAnimator.REVERSE);
            stretchAni2.setInterpolator(new DecelerateInterpolator());
            // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
            stretchAni2.addUpdateListener(this);

            // 定義小球彈起動畫
            ValueAnimator bounceAni = ObjectAnimator.ofFloat(
                    newBall,
                    "y",
                    endY,
                    startY);
            bounceAni.setDuration(duration);
            bounceAni.setInterpolator(new DecelerateInterpolator());
            // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
            bounceAni.addUpdateListener(this);

            // 定義AnimatorSet,按順序播放[下落、壓扁&拉伸、彈起]動畫
            AnimatorSet set = new AnimatorSet();
            //在squashshAni1之前播放fallAni
            set.play(fallAni).before(squashshAni1);
            /**
             * 由於小球彈起時壓扁,即寬度加倍,x座標左移,高度減半,y座標下移
             * 因此播放squashshAni1的同時還要播放squashshAni2,stretchAni1,stretchAni2
             */
            set.play(squashshAni1).with(squashshAni2);
            set.play(squashshAni1).with(stretchAni1);
            set.play(squashshAni1).with(stretchAni2);
            // 在stretchAni2之後播放bounceAni
            set.play(bounceAni).after(stretchAni2);

            // newBall物件的漸隱動畫,設定alpha屬性值1--->0
            ObjectAnimator fadeAni = ObjectAnimator.ofFloat(
                    newBall,
                    "alpha",
                    1f,
                    0f);
            // 設定動畫持續時間
            fadeAni.setDuration(250);
            // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
            fadeAni.addUpdateListener(this);

            // 為fadeAni設定監聽
            fadeAni.addListener(new AnimatorListenerAdapter() {
                // 動畫結束
                @Override
                public void onAnimationEnd(Animator animation) {
                    // 動畫結束時將該動畫關聯的ShapeHolder刪除
                    balls.remove(((ObjectAnimator) (animation)).getTarget());
                }
            });

            // 再次定義一個AnimatorSet動畫集合,來組合動畫
            AnimatorSet aniSet = new AnimatorSet();
            // 指定在fadeAni之前播放set動畫集合
            aniSet.play(set).before(fadeAni);

            // 開始播放動畫
            aniSet.start();

            return true;
        }

        @Override
        protected void onDraw(Canvas canvas) {
            for (XShapeHolder xShapeHolder : balls) {
                canvas.save();
                canvas.translate(xShapeHolder.getX(), xShapeHolder.getY());
                xShapeHolder.getShape().draw(canvas);
                canvas.restore();
            }
        }

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // 指定重繪介面
            this.invalidate();
        }

        /**
         * addBall()方法返回XShapeHolder物件,ShapeHolder物件持有小球
         */
        private XShapeHolder addBall(float x, float y) {
            // 建立一個橢圓
            OvalShape circle = new OvalShape();
            // 設定橢圓寬高
            circle.resize(BALL_SIZE, BALL_SIZE);
            // 把橢圓包裝成Drawable物件
            ShapeDrawable drawble = new ShapeDrawable(circle);
            // 建立XShapeHolder物件
            XShapeHolder holder = new XShapeHolder(drawble);
            // 設定holder座標
            holder.setX(x - BALL_SIZE / 2);
            holder.setY(y - BALL_SIZE / 2);

            // 生成隨機組合的ARGB顏色
            int red = (int) (Math.random() * 255);
            int green = (int) (Math.random() * 255);
            int blue = (int) (Math.random() * 255);
            // 把red,green,blue三個顏色隨機數組合成ARGB顏色
            int color = 0xff000000 + red << 16 | green << 8 | blue;
            // 把red,green,blue三個顏色隨機數除以4得到商值組合成ARGB顏色
            int darkColor = 0xff000000 | red / 4 << 16 | green / 4 << 8 | blue / 4;

            // 建立圓形漸變效果
            RadialGradient gradient = new RadialGradient(
                    37.5f,
                    12.5f,
                    BALL_SIZE,
                    color,
                    darkColor,
                    Shader.TileMode.CLAMP);

            // 獲取drawble關聯的畫筆
            Paint paint = drawble.getPaint();
            paint.setShader(gradient);

            // 為XShapeHolder物件設定畫筆
            holder.setPaint(paint);
            balls.add(holder);
            return holder;
        }
    }
}

這次的程式碼挺長有260多行,如果把它拆分開來,你會覺得程式碼還是原來的套路,還是很熟悉的有木有?我們首先來看,彈球動畫類XBallsFallActivity中的程式碼分為兩塊,一是onCreate()方法,這是每個Activity都要重寫的方法,那我們在onCreate()方法中幹了什麼呢?只幹了一件事就是拿到LinearLayout佈局的物件,並呼叫addBall()方法給它新增XBallView這個view物件,程式碼是這樣的:

xContainer.addView(new XBallView(this));

XBallView物件又是什麼鬼呢?一個自定義view元件,也就是實現我們小球的view元件,這也是我們這個動畫的難點所在,我們慢慢來分析,程式碼定位到XBallView類,第一眼你會發現這個類不僅繼承了View類,而且還實現了ValueAnimator.AnimatorUpdateListener這樣一個介面,再仔細一看你又會發現,這個介面怎麼聽起來這麼耳熟呢?沒錯,這就是上面我們在上面第二部分[ValueAnimator和屬性動畫的監聽]中講過的AnimatorUpdateListener類!實現了這個介面就意味著可以在XBallView中直接呼叫addUpdateListener(this)方法對屬性動畫進行監聽,只需要傳入this即可!

那我們再繼續往下看看有沒有我們要找的定義屬性動畫的邏輯呢?果然有!XBallView類中一共定義了7個動畫和兩個AnimatorSet動畫集合,我把這段程式碼摘錄出來:

        /**
         * 下面開始定義小球的下落,著地壓扁,反彈等屬性動畫
         */
        // 定義小球下落動畫
        ValueAnimator fallAni = ObjectAnimator.ofFloat(
                newBall,
                "y",
                startY,
                endY);
        // 設定動畫持續時間
        fallAni.setDuration(duration);
        // 設定加速插值器
        fallAni.setInterpolator(new AccelerateInterpolator());
        // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
        fallAni.addUpdateListener(this);

        // 定義小球壓扁動畫,控制小球x座標左移半個球寬度
        ValueAnimator squashshAni1 = ObjectAnimator.ofFloat(
                newBall,
                "x",
                newBall.getX(),
                newBall.getX() - BALL_SIZE / 2);
        squashshAni1.setDuration(duration / 4);
        squashshAni1.setRepeatCount(1);
        squashshAni1.setRepeatMode(ValueAnimator.REVERSE);
        squashshAni1.setInterpolator(new DecelerateInterpolator());
        // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
        squashshAni1.addUpdateListener(this);

        // 定義小球壓扁動畫,控制小球寬度加倍
        ValueAnimator squashshAni2 = ObjectAnimator.ofFloat(
                newBall,
                "width",
                newBall.getWidth(),
                newBall.getWidth() + BALL_SIZE);
        squashshAni2.setDuration(duration / 4);
        squashshAni2.setRepeatCount(1);
        squashshAni2.setRepeatMode(ValueAnimator.REVERSE);
        squashshAni2.setInterpolator(new DecelerateInterpolator());
        // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
        squashshAni2.addUpdateListener(this);

        // 定義小球拉伸動畫, 控制小球的y座標下移半個球高度
        ValueAnimator stretchAni1 = ObjectAnimator.ofFloat(
                newBall,
                "y",
                endY,
                endY + BALL_SIZE / 2);
        stretchAni1.setDuration(duration / 4);
        stretchAni1.setRepeatCount(1);
        stretchAni1.setRepeatMode(ValueAnimator.REVERSE);
        stretchAni1.setInterpolator(new DecelerateInterpolator());
        // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
        stretchAni1.addUpdateListener(this);

        // 定義小球拉伸動畫, 控制小球的高度減半
        ValueAnimator stretchAni2 = ObjectAnimator.ofFloat(
                newBall,
                "height",
                newBall.getHeight(),
                newBall.getHeight() - BALL_SIZE / 2);
        stretchAni2.setDuration(duration / 4);
        stretchAni2.setRepeatCount(1);
        stretchAni2.setRepeatMode(ValueAnimator.REVERSE);
        stretchAni2.setInterpolator(new DecelerateInterpolator());
        // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
        stretchAni2.addUpdateListener(this);

        // 定義小球彈起動畫
        ValueAnimator bounceAni = ObjectAnimator.ofFloat(
                newBall,
                "y",
                endY,
                startY);
        bounceAni.setDuration(duration);
        bounceAni.setInterpolator(new DecelerateInterpolator());
        // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
        bounceAni.addUpdateListener(this);

        // 定義AnimatorSet,按順序播放[下落、壓扁&拉伸、彈起]動畫
        AnimatorSet set = new AnimatorSet();
        //在squashshAni1之前播放fallAni
        set.play(fallAni).before(squashshAni1);
        /**
         * 由於小球彈起時壓扁,即寬度加倍,x座標左移,高度減半,y座標下移
         * 因此播放squashshAni1的同時還要播放squashshAni2,stretchAni1,stretchAni2
         */
        set.play(squashshAni1).with(squashshAni2);
        set.play(squashshAni1).with(stretchAni1);
        set.play(squashshAni1).with(stretchAni2);
        // 在stretchAni2之後播放bounceAni
        set.play(bounceAni).after(stretchAni2);

        // newBall物件的漸隱動畫,設定alpha屬性值1--->0
        ObjectAnimator fadeAni = ObjectAnimator.ofFloat(
                newBall,
                "alpha",
                1f,
                0f);
        // 設定動畫持續時間
        fadeAni.setDuration(250);
        // 新增addUpdateListener監聽器,當ValueAnimator屬性值改變時會激發事件監聽方法
        fadeAni.addUpdateListener(this);

        // 為fadeAni設定監聽
        fadeAni.addListener(new AnimatorListenerAdapter() {
            // 動畫結束
            @Override
            public void onAnimationEnd(Animator animation) {
                // 動畫結束時將該動畫關聯的ShapeHolder刪除
                balls.remove(((ObjectAnimator) (animation)).getTarget());
            }
        });

        // 再次定義一個AnimatorSet動畫集合,來組合動畫
        AnimatorSet aniSet = new AnimatorSet();
        // 指定在fadeAni之前播放set動畫集合
        aniSet.play(set).before(fadeAni);

        // 開始播放動畫
        aniSet.start();

邏輯很簡單,動畫fallAni控制小球下落,動畫squashshAni1控制小球壓扁時小球x座標左移半個球寬度,動畫squashshAni2控制小球壓扁時小球寬度加倍,動畫stretchAni1,控制小球拉伸動畫時小球的y座標下移半個球高度,動畫stretchAni2控制小球水平拉伸時控制小球的高度減半,動畫bounceAni定義小球彈起動畫,接著用一個AnimatorSet動畫集合把這六個動畫先組裝起來,下落動畫fallAni之後是squashshAni1squashshAni2stretchAni1stretchAni2這四個動畫同時播放,這也是小球落地瞬間的完美詮釋,再之後是小球彈起bounceAni。最後還有一個fadeAni漸隱動畫控制小球彈回起始高度後消失,接著再用一個AnimatorSet動畫集合把前面的那個動畫集合和第七個fadeAni漸隱動畫組裝起來,整個桌面彈球動畫就大功告成了!

需要注意的是,在addBall()方法中,返回的是一個XShapeHolder型別的物件,那麼XShapeHolder是什麼呢?XShapeHolder包裝了ShapeDrawable物件,並且為x,y,width,height,alpha等屬性提供了settergetter方法,程式碼如下:

package com.wondertwo.propertyanime;

import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.Shape;

/**
 *
 * Created by wondertwo on 2016/3/20.
 */
public class XShapeHolder {

    private float x = 0, y = 0;
    private ShapeDrawable shape;
    private int color;
    private RadialGradient gradient;
    private float alpha = 1f;
    private Paint paint;

    public XShapeHolder(ShapeDrawable shape) {
        this.shape = shape;
    }

    public float getWidth() {
        return shape.getShape().getWidth();
    }

    public void setWidth(float width) {
        Shape s = shape.getShape();
        s.resize(width, s.getHeight());
    }

    public float getHeight() {
        return shape.getShape().getHeight();
    }

    public void setHeight(float height) {
        Shape s = shape.getShape();
        s.resize(s.getWidth(), height);
    }

    public float getX() {
        return x;
    }

    public void setX(float x) {
        this.x = x;
    }

    public float getY() {
        return y;
    }

    public void setY(float y) {
        this.y = y;
    }

    public ShapeDrawable getShape() {
        return shape;
    }

    public void setShape(ShapeDrawable shape) {
        this.shape = shape;
    }

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }

    public RadialGradient getGradient() {
        return gradient;
    }

    public void setGradient(RadialGradient gradient) {
        this.gradient = gradient;
    }

    public float getAlpha() {
        return alpha;
    }

    public void setAlpha(float alpha) {
        this.alpha = alpha;
    }

    public Paint getPaint() {
        return paint;
    }

    public void setPaint(Paint paint) {
        this.paint = paint;
    }
}

部落格的最後再放上郭霖大神的兩篇部落格,供大家參考!

  1. Android屬性動畫完全解析(上),初識屬性動畫的基本用法 http://blog.csdn.net/guolin_blog/article/details/43536355
  2. Android屬性動畫完全解析(中),ValueAnimator和ObjectAnimator的高階用法 http://blog.csdn.net/guolin_blog/article/details/43816093
  3. Android屬性動畫完全解析(下),Interpolator和ViewPropertyAnimator的用法 http://blog.csdn.net/guolin_blog/article/details/44171115

在最後附上淺析Android動畫系列的三篇文章:

  1. 淺析Android動畫(一),View動畫高階例項探究 http://www.cnblogs.com/wondertwo/p/5295976.html
  2. 淺析Android動畫(二),屬性動畫與高階例項探究 http://www.cnblogs.com/wondertwo/p/5312482.html
  3. 淺析Android動畫(三),自定義Interpolator與TypeEvaluator http://www.cnblogs.com/wondertwo/p/5327586.html

如果覺得不錯,請繼續關注我哦!

相關推薦

淺析Android動畫屬性動畫高階例項探究

轉載請註明出處!http://www.cnblogs.com/wondertwo/p/5312482.html ObjectAnimator實現屬性動畫 為了寫好Android動畫這幾篇部落格,在動筆之前我是下過很大決心的,我對自己的要求是儘量把一個小知識點寫清楚寫明白,說白了就是相對於大而全的長篇大論,我

Android動畫-屬性動畫

概述 上一篇主要介紹了ViewAnim和幀動畫,篇幅有點長,另起一篇。上篇介紹的兩種動畫開發中用到的不多,主要還是本篇的屬性動畫使用比較廣。 1 補間動畫 1.1 Property Anim 開發中Property Anim使用比View Anim要更為廣泛,主要還是出於剛剛

android動畫 插值器

插值器 首先要了解為什麼需要插值器,因為在補間動畫中,我們一般只定義關鍵幀(首幀或尾幀),然後由系統自動生成中間幀,生成中間幀的這個過程可以成為“插值”。插值器定義了動畫變化的速率,提供不同的函式定義變化值相對於時間的變化規則,可以定義各種各樣的非線性變化函式,比如加速、減速等。下面是幾種常

react-native-android-unity建立unity專案並匯出為android程式碼包嵌入android專案中

1.建立unity專案 給Main Camera新增指令碼Android,使用C#開發,指令碼內容如下: using System.Collections;
using System.Colle

自學Android開發 AndroidStudio 匯入Github上下載的專案以及使用Genymotion所遇到的問題

因為是自學Android自然是想找一些原始碼,自己跑著試試,然後自己再去解讀這些程式碼~~本著這樣的想法,於是就在Github上找了一個Android的小遊戲https://github.com/HurTeng/StormPlane    連線奉上~~   -。-高高興興的下

Android Animation動畫詳解: 組合動畫特效

前言     上一篇部落格Android Animation動畫詳解(一): 補間動畫 我已經為大家介紹了Android補間動畫的四種形式,相信讀過該部落格的兄弟們一起都瞭解了。如果你還不瞭解,那點連結過去研讀一番,然後再過來跟著我一起學習如何把簡單的動畫效果組合在一起,做

Android 開源框架Glide應用_佔位&動畫&Gif

在APP顯示圖片時,擁有一個良好的體驗是非常重要的,即圖片不會突兀的出現,同樣在出錯時,需要有明顯的提示,對於這些Glide都提供了介面,幫你去提升APP的體驗。 佔位圖片:.placeholder 只需要呼叫.placeholder(),Glide將會顯示

Android 自定義View線的繪製

public class PointLine extends View { Paint mLinePaint; Paint mPointPaint; float width; float height; float pointAddress

Android動畫學習屬性動畫實現Tween的效果和高階屬性示例

一、基本介紹 Golang設計者為了程式設計方便,提供了一些函式,這些函式可以直接使用,我們稱為Go的內建函式。文件:https://studygolang.com/pkgdoc -> buil

CSS3動畫:波浪效果

col -1 loading ack css代碼 code load width ase 實現效果 如圖所示: 首先得準備三張圖,一張是淺黃色的背景圖loading_bg.png,一張是深紅色的圖loading.png,最後一張為bolang.png。 css代碼

PythonWeb開發教程搭建第一個django項目

translate -s 分享圖片 ble show main tab table python 這篇寫怎麽創建django項目,以及把django項目運行起來。 1、創建django項目 a.使用命令創建,安裝完django之後就有djang

WPF中的動畫——From/To/By 動畫

forever byte idt prope repeat 並且 -c 數據結構 類型 原文:WPF中的動畫——(二)From/To/By 動畫我們所實現的的動畫中,很大一部分是讓一個屬性在起始值和結束值之間變化,例如,我在前文中實現的改變寬度的動畫: var w

mavenLinux安裝maven3.5.3及配置

col sha TP sharp 全局 local roo mave pat   Linux系統,ubuntu-16.04.4,安裝maven3.5.3   一、創建文件夾   註意Linux用戶,這個如果不是root用戶,命令前面需要加:sudo //創建一個目錄 mk

xpath學習通過xpath 采集數據

編碼方式 img 模塊 界面 工具 技術分享 bubuko 這一 獲取 通過上一篇文章我們已經知道如何通過xpah精準定位到網頁中的某個元素了。今天再來看看昨天在網頁中獲取的數據該怎麽辦? 一、打開模板測試工具 二、雙擊run.bat 在執行這一步時我們必須安裝Jav

JS學習物件屬性的獲取和訪問

var array = ["one","two","three"];//陣列 var obj = {//物件     field : "self",     printInfo : function (){        

mybatis對映檔案的使用引數傳遞

對映檔案、介面定義和測試程式碼 package canger.study.chapter04.mapper; import canger.study.chapter04.bean.Actor; import org.apache.ibatis.annotations.Param; import j

MapReduce 統計手機使用者的上行流量下行流量總流量並對輸出的結果進行倒序排序。劃分省份輸出到不同的檔案

在(一)的基礎上,寫一個自己的partitioner就好了。   分割槽的預設實現HashPartitioner,它根據key的hashcode和Interger.  在Reduce過程中,可以根據實際需求(比如按某個維度進行歸檔,類似於資料庫的分組),把Map完的資

Android學習—— Android SDK

在上一篇文章中,提到了一個概念,叫SDK,這篇文章就來對SDK進行一個簡單的講解 Android SDK Android SDK(Software Development Kit)提供了在Windows/Linux/Mac平臺上開發Android應用的開發元件,Android支援所有的平臺,其包含了在An

CSS佈局學習 - flex屬性

flex屬性 定義 flex佈局包括最外層的容器和內部的元素,flex屬性是內部元素屬性。flex屬性是flex-grow, flex-shrink, flex-basis三個屬性的簡寫   flex-grow   設定元素佔flex容器所剩空餘空間的比例   flex-shrink   設定元素