帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

猛猛的小盆友發表於2019-02-05

終於在新的一年的第一天完成了本篇文章,小盆友在此祝賀您,萬事如意,闔家幸福。?

目錄

一、前言

二、插值器與估值器

三、原始碼解析

四、實戰

五、寫在最後

一、前言

對於越來越追求豐富的互動體驗的客戶端,一個帶有動態效果的介面已經是不可避免。屬性動畫就是這其中必不可少的一把利器,所以今天便來分享下 屬性動畫融合在實際場景中 的使用,以及進行 原始碼分析。話不多說,先看看今天的實戰效果圖,然後開始進行分享之旅。

1、多維雷達圖

在這裡插入圖片描述

2、錶盤指示器

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI
3、活力四射的購物車
帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

老規矩,程式碼在實戰篇會給出,透過 API 看清原始碼,各種效果便是順手拈來?

二、插值器與估值器

對於屬性動畫來說,一定是繞不開 插值器估值器。接下來便一一道來

文章更多的使用 我在開發過程中 自我理解 的詞語,而儘量不使用 教科書式直接翻譯註釋 語句。如果有 晦澀難懂 或是 理解錯誤之處,歡迎 評論區 留言,我們進行探討。

1、插值器(TimeInterpolator)

(1)定義

是一個控制動畫 進度 的工具。

為何如此說?我們們可以這麼理解,我們藉助 SpringInterpolator 插值器的函式圖來講解。

  • x軸為 時間 。0表示還未開始,1表示時間結束。時間是 不可逆的 ,所以會一直往前推進,即 只會增加
  • y軸為 當前時間點,物體(我們的控制的動畫物件,例如View)距離目的地的進度。0表示進度還未走動,1表示進度已經走至目的地。但值得注意的是,到達目的地(A點)不代表動畫已經結束,可以 “衝破”(B點) 目的地,也可以 往回走(C點)結束與否是由時間軸決定。

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI
SpringInterpolator 的動態圖如下

下面動態圖是小盆友專門為插值器更為直觀的展示而開發的小工具,如果你感興趣,可以把自己的插值器也放入這其中進行展示,方便以後開發時需要,程式碼傳送門

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI
這個插值器,在小盆友的另一篇博文中有使用到,效果如下圖,有興趣的童鞋可以進入傳送門瞭解。
帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

(2)如何使用

Android 系統中已經幫我實現了一些比較常用插值器,這裡就不一一貼圖介紹器函式圖,需要的童鞋可以進入小盆友下面所提到的 插值器小工具 進行玩耍,這裡給一張工具的效果圖。

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI
加入到屬性動畫中也很簡單,只需以下一行程式碼,便可輕鬆搞定

// 將 插值器 設定進 Animator 
mAnimator.setInterpolator(new AccelerateInterpolator());
複製程式碼

在原始碼中插值器是如何作用的呢?先賣個關子,在原始碼解析小節給出答案。

但是在某些情況下,為了滿足動畫效果的需要,Android提供的插值器就滿足不了我們(?其實就是設計師搞事情)了,所以需要找到對應的公式進行自定義插值器。聰明的你,肯定已經發現,我們前面提到的 SpringInterpolator 並不是Android提供的插值器。定義 SpringInterpolator 時,只需要實現 TimeInterpolator 介面,並在 getInterpolation 方法中實現自己的邏輯即可,程式碼如下

/**
 * @author Jiang zinc
 * @date 建立時間:2019/1/27
 * @description 震旦效果
 */
public class SpringInterpolator implements TimeInterpolator {

    /**
     * 引數 x,即為 x軸的值
     * 返回值 便是 y 軸的值
     */
    @Override
    public float getInterpolation(float x) {
        float factor = 0.4f;
        return (float) (Math.pow(2, -10 * x) * Math.sin((x - factor / 4) * (2 * Math.PI) / factor) + 1);
    }
}
複製程式碼

使用時,和 Android提供的插值器 是一摸一樣的,如下所示

mAnimator.setInterpolator(new SpringInterpolator());
複製程式碼

2、估值器(TypeEvaluator)

(1)定義

插值器 中的 y軸數值 轉換為我們 需要的值型別 的工具。

emmmm....稍微有點抽象。我們來具體分析下,這句話的意思。

我們以一個 具體的場景 來分析這個定義,方便講解也更容易理解。我們進行旋轉一個View,在1秒內,從 0度 轉到 360度。具體程式碼如下:

// 例項化一個view
View view = new View(this);
// 設定 屬性動畫 的目標物件、作用的屬性、關鍵幀(即0,360)
// 作用的屬性值 rotation 會轉為對應的方法名 setRotation,這個是預設的規則。
ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(view, "rotation", 0, 360);
// 設定 插值器
rotationAnimator.setInterpolator(new SpringInterpolator());
// 設定 動畫時長
rotationAnimator.setDuration(1_000);
// 開啟 動畫
rotationAnimator.start();
複製程式碼

這裡其實有個問題,童鞋們應該也注意到了,插值器 的返回值(即函式圖中的 y軸數值)是一個 到 “目的地” 的距離百分比(這裡的百分比也就是我們前面所說的進度) ,而非我們例子中需要度數,所以需要進行 轉換,而起到轉換作用的就是我們的 估值器

怎麼轉換呢?這裡要 分兩種情況說明

第一種情況,我們通過以下程式碼,設定一個估值器,則計算規則由設定的估值器確定

ObjectAnimator mTranslateAnimator = ObjectAnimator.ofObject(view,
													        "position",
													        new BezierEvaluator(),
													        startPoint,
													        endPoint);
複製程式碼

Android 系統中提供了一些 常用的估值器

  • ArgbEvaluator:顏色估值器,可以用於從 開始顏色 漸變為 終止顏色;
  • FloatArrayEvaluator:浮點陣列估值器,將開始浮點陣列 逐漸變為 終止浮點陣列;
  • FloatEvaluator:浮點數值估值器,將開始浮點數 逐漸變為 終止浮點數;
  • IntArrayEvaluator:整型陣列估值器,將開始整型陣列 逐漸變為 終止整型陣列;
  • IntEvaluator:整型數值估值器,將開始整型數 逐漸變為 終止整型數;
  • PointFEvaluator:座標點估值器,將開始座標點 逐漸變為 終止座標點;
  • RectEvaluator:範圍估值器,將開始範圍 逐漸變為 終止範圍;

然鵝,在某些情況下(產品大大搞事),我們需要實現一個類似如下的新增到購物車的效果,商品是以 貝塞爾曲線 的路徑 “投到” 購物車中的,這時我們就需要 自定義估值器,因為 PointFEvaluator只是線性的將商品從起始點移到終止點,滿足不了產品大大的需求,如何自定義呢?請往下走

對貝塞爾曲線感興趣的童鞋,可以檢視小盆友的另一片博文 自帶美感的貝塞爾曲線原理與實戰

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI
相信你也猜到了,估值器 也是通過實現一個介面,以下便是介面和引數描述

public interface TypeEvaluator<T> {
	/**
	 * @param fraction   插值器返回的值(即函式圖中的 y軸數值)
     * @param startValue 動畫屬性起始值,例子中的 0度
     * @param endValue   動畫屬性終止值,例子中的 360度
	 */
    public T evaluate(float fraction, T startValue, T endValue);
}
複製程式碼

我們只需要實現他,填充自己的邏輯,以這個購物車的路徑為例,便是以下程式碼,這樣一個走 貝塞爾曲線 路徑的商品就出現了。

private static class BezierEvaluator implements TypeEvaluator<PointF> {

    private final List<PointF> pointList;

    public BezierEvaluator(PointF startPoint, PointF endPoint) {
        this.pointList = new ArrayList<>();

        PointF controlPointF = new PointF(endPoint.x, startPoint.y);

        pointList.add(startPoint);
        pointList.add(controlPointF);
        pointList.add(endPoint);

    }

    @Override
    public PointF evaluate(float fraction, PointF startPoint, PointF endPoint) {
        return new PointF(BezierUtils.calculatePointCoordinate(BezierUtils.X_TYPE, fraction, 2, 0, pointList),
                BezierUtils.calculatePointCoordinate(BezierUtils.Y_TYPE, fraction, 2, 0, pointList));
    }
}
複製程式碼

購物車的動畫思路如下:

  1. 點選新增商品後,初始化一個 ShoppingView(繼承至ImageView),設定其大小和商品的大小一致,並新增至 decorView 中,這樣才能讓 ShoppingView 在整個檢視中移動。
  2. 通過 getLocationOnScreen 方法,獲取商品圖片在螢幕中的座標A 和 購物車在螢幕中的座標B(值得注意的是,這個座標是 包含狀態列的高度 ),將 ShoppingView 移至座標A,然後啟動動畫。
  3. 進行以 ShoppingView 正中心為原點進行 從0到0.8縮小,接著順時針傾斜35度,緊接著以 貝塞爾曲線 路徑從座標A移至座標B,控制點C的x軸為B點的x軸座標,y軸為A點的y軸座標;
  4. 動畫完成後,將 ShoppingView 從 其父檢視中移除,同時回撥偵聽介面,以便購物車可以進行動畫。
  5. 接到回撥後,購物車數量加一,同時更換購物車圖示,已經顯示小紅點並且播放其動畫,以小紅點正中心為原點進行縮放,縮放規則為從 1 到 1.2 到 1 到 1.1 到 1, 產生一種彈動的效果。

如果對這一動畫感興趣,可以檢視具體程式碼,請進入傳送門

第二種情況,大多數情況下,我們不會進行設定估值器,因為原始碼中已經幫我們做了這一步的轉換。所以當我們沒有設定時,系統會以以下的公式進行轉換(我們這裡以 浮點數 為具體場景)

// 這段程式碼在 FloatKeyframeSet 的 getFloatValue 方法中
prevValue + intervalFraction * (nextValue - prevValue);
複製程式碼

這裡再賣個關子,公式的各個引數的意義先不給出,在原始碼解析一節中一起講解。

值得注意的是,如果屬性動畫中需要使用的是自己定義的型別,則必須要使用第一種情況自行定義估值器,否則會crash。

(2)如何使用

如何使用,在上一小節其實已經給出,這裡給一個完整的程式碼

ObjectAnimator mTranslateAnimator = ObjectAnimator.ofObject(this,
                "position",
                new BezierEvaluator(startPoint, endPoint),
                startPoint,
                endPoint);
mTranslateAnimator.setDuration(450);
mTranslateAnimator.setInterpolator(new AccelerateInterpolator());
mTranslateAnimator.start();

private static class BezierEvaluator implements TypeEvaluator<PointF> {

    private final List<PointF> pointList;

    public BezierEvaluator(PointF startPoint, PointF endPoint) {
        this.pointList = new ArrayList<>();

        PointF controlPointF = new PointF(endPoint.x, startPoint.y);

        pointList.add(startPoint);
        pointList.add(controlPointF);
        pointList.add(endPoint);

    }

    @Override
    public PointF evaluate(float fraction, PointF startPoint, PointF endPoint) {
        return new PointF(BezierUtils.calculatePointCoordinate(BezierUtils.X_TYPE, fraction, 2, 0, pointList),
                BezierUtils.calculatePointCoordinate(BezierUtils.Y_TYPE, fraction, 2, 0, pointList));
    }
}
複製程式碼

三、原始碼解析

1、約法三章

進入原始碼解析前,有必要先跟各位同學 確認一件事建立一個場景,否則原始碼解析過程會 沒有目標 ,而迷路。

(1)確認一件事

你已經會使用屬性動畫,會的意思是你已經能在 不借助文件 或是 “借鑑”別人的程式碼 的情況下,寫出一個屬性動畫,並讓他按照你所需要的效果 正常的執行 起來,效果難易程度不限定。

(2)建立一個場景

原始碼的閱讀在一個具體的場景中更容易理解,雖然會稍微片面些,但是漏掉的情景在懂得了一個場景後,後續使用過程中便會慢慢的補充,而且主線已懂,支線也就不難了。話不多說,我們來看看這個使用場景。

我們針對 ZincView 的 setZinc 方法進行值變動:

  • 值變動範圍:從 0 到 2,從 2 到 5(浮點數)
  • 動畫時長 2000 毫秒
  • 設定了 FloatEvaluator 估值器 (這裡是為了原始碼解析,所以特意加上去;如果正常情況下,該場景是不需要設定估值器的)
  • 設定了 更新回撥器
  • 設定了 生命週期監聽器
/**
 * @author Jiang zinc
 * @date 建立時間:2019/1/14
 * @description 屬性動畫原始碼分析
 */
public class SimpleAnimationActivity extends Activity {

    private static final String TAG = "SimpleAnimationActivity";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ZincView view = new ZincView(this);
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "zinc", 0f, 2f, 5f);
        // 時長
        objectAnimator.setDuration(2000);
        // 插值器
        objectAnimator.setInterpolator(new TimeInterpolator() {
            @Override
            public float getInterpolation(float input) {
                return input;
            }
        });
        // 估值器
        objectAnimator.setEvaluator(new FloatEvaluator());
        // 更新回撥
        objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue().getClass());
                Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue());
            }
        });
        // 生命週期監聽器
        objectAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                Log.i(TAG, "onAnimationStart: ");
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                Log.i(TAG, "onAnimationEnd: ");
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                Log.i(TAG, "onAnimationCancel: ");
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                Log.i(TAG, "onAnimationRepeat: ");
            }
        });
        // 開啟
        objectAnimator.start();
    }

    public static class ZincView extends View {
        public ZincView(Context context) {
            super(context);
        }
        public void setZinc(float value) {
            Log.i(TAG, "setZinc: " + value);
        }
    }

}
複製程式碼

接下來我們便 一行行程式碼的進入原始碼的世界 ,瞭解 “屬性動畫” 的背後祕密。請各位同學開啟自己的原始碼檢視器,或是 Android Studio,一邊跟著小盆友的思路走,一邊在原始碼間跳躍,才不容易懵。

這裡再次強調,以下程式碼分析都是基於這個場景,所以引數的講解和邏輯貫穿也會直接帶入場景中的數值或物件,且不再做特殊說明。並且以 "FLAG(數字)" 來作為錨,方便程式碼折回講解,各位童鞋可以使用瀏覽器搜尋功能迅速定位。

我們的原始碼版本是26,接下來就開始我們的每行分析。

2、第一行程式碼

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "zinc", 0f, 2f, 5f);
複製程式碼

進入 ObjectAnimator 的 ofFloat 方法

// ObjectAnimator類
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
    // 初始化 ObjectAnimator,將 目標物件 和 屬性 進行關聯
    ObjectAnimator anim = new ObjectAnimator(target, propertyName);
    // 設定關鍵幀 FLAG(1)
    anim.setFloatValues(values);
    return anim;
}
複製程式碼

上面?程式碼中的第一行,便是構建一個 ObjectAnimator 例項,同時將 目標物件( ZincView )和 屬性方法(zinc)一同傳入,具體的內容如下?程式碼,我們還需要再進入一層瞭解

// ObjectAnimator類
private ObjectAnimator(Object target, String propertyName) {
    setTarget(target);
    // FLAG(2)
    setPropertyName(propertyName);
}
複製程式碼

再進入上面?第一行程式碼,便來到以下?的程式碼,該方法主要功能是 停止之前目標物件的動畫(如果有之前目標物件的話),然後將目標物件置換為現在的ZincView物件

// ObjectAnimator類
public void setTarget(@Nullable Object target) {
    // 獲取之前的目標物件,這裡的場景 原來的目標物件 為空
    final Object oldTarget = getTarget();
    // 兩個目標物件不同,因為 oldTarget 為 null,target 為 ZincView,進入此if分支
    if (oldTarget != target) {

        // 如果已經是在執行,則進行取消
        // isStarted()方法具體請看下面程式碼段,只是獲取 mStarted 標記位,
        // 該標記初始化為false,動畫開啟開始之前,該標記一直為false,開啟之後,被置為true,稍後會提到
        if (isStarted()) {
        	// FLAG(24)
            cancel();
        }

        // 進行設定 新的目標物件,進行弱引用,防止記憶體洩漏
        mTarget = target == null ? null : new WeakReference<Object>(target);

        // 將 初始狀態置為 false
        mInitialized = false;
    }
}

// ValueAnimator類(ObjectAnimator 繼承至 ValueAnimator)
public boolean isStarted() {
	// mStarted 被初始化為 false,動畫未開始,該標記為則為false
    return mStarted;
}
複製程式碼

弱引用與記憶體洩漏 方面有興趣的同學,可以閱讀小盆友的另一片部落格,記憶體洩漏與排查流程

跳出 setTarget 方法,我們進入到 setPropertyName 方法,即 FLAG(2) ,可以看到以下?程式碼,這段程式碼其實就是將 屬性方法名(zinc)存至 類成員屬性 mPropertyName中,沒有其他的有意義操作

// ObjectAnimator類
public void setPropertyName(@NonNull String propertyName) {
	// 此場景中,mValues為空,此 if 分支不會進入,可以先不進行理會
	// 至於 mValues 是什麼,在什麼時候會初始化,很快就會揭曉
    if (mValues != null) {
        PropertyValuesHolder valuesHolder = mValues[0];
        String oldName = valuesHolder.getPropertyName();
        valuesHolder.setPropertyName(propertyName);
        mValuesMap.remove(oldName);
        mValuesMap.put(propertyName, valuesHolder);
    }
    // 將 屬性方法名(zinc) 存至 mPropertyName屬性 中
    mPropertyName = propertyName;
    mInitialized = false;
}
複製程式碼

小結一下

至此 ObjectAnimator 的構造方法走完,我們先來小結一下,做了兩件事:

  1. 目標物件ZincView弱引用的形式儲存在 mTarget屬性
  2. 屬性方法名zinc 儲存在 mPropertyName屬性

看完構造方法中的祕密,回到 ObjectAnimator 的 ofFloat方法 中,進入接下來的那行程式碼,即 FLAG(1)這行程式碼用於關鍵幀設定,具體如下?,因為 mValues 此時為空,且 mProperty 也為空,所以最終進入 setValues 那一行程式碼(即FLAG(3))

// ObjectAnimator類
public void setFloatValues(float... values) {
    // mValues 為空
    if (mValues == null || mValues.length == 0) {
        // mProperty 為空,在這個場景中,進入else分支
        if (mProperty != null) {
            setValues(PropertyValuesHolder.ofFloat(mProperty, values));
        } else {
            // 進行 PropertyValuesHolder 包裝
            // 將 關鍵幀 進行各自的封裝成 Keyframe
            // 然後打包成 KeyframeSet 與 mPropertyName 共同儲存進 PropertyValuesHolder中
            // FLAG(3)
            setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
        }
    } else {
        super.setFloatValues(values);
    }
}
複製程式碼

setValues 那一行程式碼中其實還包含了一句 PropertyValuesHolder 的構建語句,我們先進入PropertyValuesHolderofFloat 方法中,能看到如下程式碼段,例項化了一個 FloatPropertyValuesHolder 型別的物件,同時又將 屬性方法名 和 關鍵幀的值 傳入。

// PropertyValuesHolder類
public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
    return new FloatPropertyValuesHolder(propertyName, values);
}
複製程式碼

進入到構造方法,可以看到如下兩行程式碼,第一行是呼叫了父類的構造方法,而 FloatPropertyValuesHolder 繼承至 PropertyValuesHolder,所以便來到了 PropertyValuesHolder 的構造方法,可以看到就是將 屬性方法名zinc 存至 mPropertyName屬性 中。

// FloatPropertyValuesHolder類
public FloatPropertyValuesHolder(String propertyName, float... values) {
    super(propertyName);
    // FLAG(4)
    setFloatValues(values);
}

// PropertyValuesHolder類
private PropertyValuesHolder(String propertyName) {
    mPropertyName = propertyName;
}
複製程式碼

往下執行,進入 setFloatValues 方法,即 FLAG(4) ,便來到了下面這段程式碼,我們先直接進入 父類的 setFloatValues 方法,這個方法中,主要將關鍵幀的型別存至 mValueType 屬性中,然後進行建立關鍵幀集合存放至 mKeyframes 屬性中。

// FloatPropertyValuesHolder類
public void setFloatValues(float... values) {
    // 儲存 關鍵幀
    super.setFloatValues(values);
    // mKeyframes 已經在 super.setFloatValues(values); 中初始化完畢
    // 這裡將其強轉為 Keyframes.FloatKeyframes 浮點數型別的關鍵幀
    // FLAG(5)
    mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
}

// PropertyValuesHolder類
public void setFloatValues(float... values) {
    // 儲存關鍵幀型別為 float型別
    mValueType = float.class;
    // 進行拼湊 關鍵幀集合,最後將其返回
    mKeyframes = KeyframeSet.ofFloat(values);
}
複製程式碼

進入 KeyframeSetofFloat 方法(具體程式碼看下面?),ofFloat方法主要是將我們傳進來的關鍵幀數值轉換為關鍵幀物件,然後封裝成 FloatKeyframeSet型別的關鍵幀集合,方便後期動畫執行時使用(如何使用,在講解start時會詳細講解)。

// KeyframeSet類
/**
 * 建立 關鍵幀集合 物件
 * 傳進來的 "0f, 2f, 5f" 每個數值將被封裝成 Keyframe 關鍵幀物件
 * 最終被放置 FloatKeyframeSet關鍵幀集合中 向上轉型為 KeyframeSet
 *
 * @param values 傳進來的 0f, 2f, 5f 數值
 * @return 返回關鍵幀集合
 */
public static KeyframeSet ofFloat(float... values) {
	// 是否為壞資料標記 (not a number) 
    boolean badValue = false;

    // 關鍵幀數量,這裡長度為 3
    int numKeyframes = values.length;
    // 保證關鍵幀的陣列長度至少為 2
    FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes, 2)];

    // 當關鍵幀數量為1時,需要補足 兩個,該場景中,進入 else 分支
    if (numKeyframes == 1) {
        // 第一個用 空value 進行補充,預設會為0
        // 因為 Keyframe 的 mValue屬性型別為float,jvm會自動為其填充為 0
        keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
        // 填充 第二幀 為傳進來的數值
        keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);

        // 是否為 not a number,如果是則改變標記位
        if (Float.isNaN(values[0])) {
            badValue = true;
        }
    } else {

        // 關鍵幀 進行新增進集合
        // 傳進來的值:0f ------> fraction: 0    keyframes[0]
        // 傳進來的值:2f ------> fraction: 1/2  keyframes[1]
        // 傳進來的值:5f ------> fraction: 1    keyframes[2]
        // fraction 為ofFloat的第一個引數, value 為ofFloat的第二個引數;
        keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
        for (int i = 1; i < numKeyframes; ++i) {
            keyframes[i] =
                    (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
            
            // 是否為 not a number,如果是則改變標記位
            if (Float.isNaN(values[i])) {
                badValue = true;
            }
        }
    }
    // 如果為 not a number,則進行提示
    if (badValue) {
        Log.w("Animator", "Bad value (NaN) in float animator");
    }
    // 將建立好的 關鍵幀陣列keyframes 包裝成 FloatKeyframeSet型別 物件
    // 這樣封裝的好處是 動畫執行時更好的呼叫當前的關鍵幀
    return new FloatKeyframeSet(keyframes);
}
複製程式碼

FloatKeyframeSet是如何封裝的呢?我們繼續進入檢視,進入 FloatKeyframeSet類(看到下面?這段程式碼),發現直接是呼叫父類的構造方法,並且將入參一起往父類拋,所以我們進入父類 KeyframeSet 的構造方法。這裡需要先說明下這幾個類的繼承關係,後面也會用到,注意看清這幾個類和介面的名字。

graph LR
    A[FloatKeyframeSet] --> B[KeyframeSet] 
    B[KeyframeSet] --> C[Keyframes]
    A[FloatKeyframeSet] -.-> D[Keyframes.FloatKeyframes] 
複製程式碼
graph LR
    A[FloatKeyframe] --> B[Keyframe] 
複製程式碼

進入 KeyframeSet 的建構函式,因為 FloatKeyframe 繼承自 Keyframe,所以進入父類後,也就自動向上轉型了。在這構造方法中,只是進行儲存一些關鍵幀集合的狀態,例如:關鍵幀集合的長度,將關鍵幀陣列轉換為列表等(具體看下面程式碼註釋

// FloatKeyframeSet 類
public FloatKeyframeSet(FloatKeyframe... keyframes) {
    super(keyframes);
}

// KeyframeSet 類
public KeyframeSet(Keyframe... keyframes) {
    // 儲存 關鍵幀數量
    mNumKeyframes = keyframes.length;
    // 將 關鍵幀陣列轉 換為 不可變列表
    mKeyframes = Arrays.asList(keyframes);
    // 儲存 第一個 關鍵幀
    mFirstKeyframe = keyframes[0];
    // 儲存 最後一個 關鍵幀
    mLastKeyframe = keyframes[mNumKeyframes - 1];
    // 儲存最後一個關鍵幀的插值器  在這場景中,這裡的插值器為null
    mInterpolator = mLastKeyframe.getInterpolator();
}
複製程式碼

終於走到頭了,我們需要折回去,到 FLAG(5),這裡在貼上出來一次(請看下面?),執行接下來的一行程式碼,只是將 父類中 mKeyframes 屬性從 Keyframes型別 強轉為 FloatKeyframes 後儲存在 mFloatKeyframes 屬性中。

// FloatPropertyValuesHolder 類
public void setFloatValues(float... values) {
    // 剛才一直是在講解這一行程式碼的內容
    super.setFloatValues(values);
    // 這裡是講解這一句啦? 
    mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
}
複製程式碼

小結一下

到這裡 PropertyValuesHolder.ofFloat 的程式碼內容就走完了,我們再來小結一下

  1. PropertyValuesHolder 是一個用於裝載 關鍵幀集合屬性動畫名 的資料模型。
  2. 關鍵幀會被一個個存放在 Keyframe 類中,既 有多少個關鍵幀有多少個Keyframe
  3. Keyframe的fraction為 i / 關鍵幀的數量-1(i>=1),value 則為對應的關鍵幀數值。即將1進行等分為(關鍵幀數量-2)份,頭尾兩幀fraction則分別為0和1,中間幀則按順序各佔一份。

我們需要再折到 FLAG(3),進行檢視 setValues 這一句是如何儲存剛剛建立的 PropertyValuesHolder 物件。進入setValues,可以看到下面?這段程式碼,

/**
 * 將 PropertyValuesHolder組 進行儲存。分別存於:
 * 1、mValues ---------- PropertyValuesHolder組
 * 2、mValuesMap ------- key = PropertyName屬性名,value = PropertyValuesHolder
 * <p>
 * 值得注意:
 * PropertyValuesHolder 中已經存有 關鍵幀
 */
public void setValues(PropertyValuesHolder... values) {
	// 儲存 PropertyValuesHolder 的長度,該場景長度為1
    int numValues = values.length;
    // 儲存值
    mValues = values;
    mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);

	// 該場景中,這裡只迴圈一次。因為 PropertyValuesHolder 只有一個
    for (int i = 0; i < numValues; ++i) {
        PropertyValuesHolder valuesHolder = values[i];
        // 以 key為屬性方法名zinc ----> value為對應的PropertyValuesHolder 儲存到map中
        mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
    }

    mInitialized = false;
}
複製程式碼

至此,第一行程式碼就已經解析完畢,主要是進行 目標物件,屬性方法名 和 關鍵幀 的包裝和儲存。已經在每小段程式碼後進行小結,這裡就不再做冗餘操作。

3、第二行程式碼

// 時長
objectAnimator.setDuration(2000);
複製程式碼

來到第二行,進行設定動畫時長,進入 setDuration,可以看到?下面這段程式碼,是呼叫的父類ValueAnimator的setDuration方法,父類的setDuration方法進行值合法判斷,然後儲存至 mDuration 屬性中。

// ObjectAnimator 類
public ObjectAnimator setDuration(long duration) {
    super.setDuration(duration);
    return this;
}

// ValueAnimator 類
private long mDuration = 300;

// ValueAnimator 類
public ValueAnimator setDuration(long duration) {
    // 如果為負數,拋異常
    if (duration < 0) {
        throw new IllegalArgumentException("Animators cannot have negative duration: " +
                duration);
    }

    // 儲存時長
    mDuration = duration;
    return this;
}
複製程式碼

值得注意

敲黑板啦,童鞋們注意到沒,mDuration的初始值為300,也就是說,如果我們不進行動畫時長的設定,動畫時長就預設為300毫秒。

4、第三行程式碼

// 插值器
objectAnimator.setInterpolator(new TimeInterpolator() {
    @Override
    public float getInterpolation(float input) {
        return input;
    }
});
複製程式碼

接下來來到第三行設定插值器程式碼,進入後是直接來到 ValueAnimator 的 setInterpolator 方法,也就是說 ObjectAnimator 沒有進行重寫該方法。如果傳入 setInterpolator 方法的引數為 null,則會預設提供 LinearInterpolator 插值器;如果傳入了非空插值器,則儲存至 mInterpolator 屬性中。

// ValueAnimator 類
private static final TimeInterpolator sDefaultInterpolator =
            new AccelerateDecelerateInterpolator();

// ValueAnimator 類
private TimeInterpolator mInterpolator = sDefaultInterpolator;

// ValueAnimator 類
public void setInterpolator(TimeInterpolator value) {
    if (value != null) {
        mInterpolator = value;
    } else {
        mInterpolator = new LinearInterpolator();
    }
}
複製程式碼

值得注意

當我們沒有進行設定插值器時,預設的為我們初始化了 AccelerateDecelerateInterpolator 插值器,該插值器的走勢如下動態圖。

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

5、第四行程式碼

objectAnimator.setEvaluator(new FloatEvaluator());
複製程式碼

第四行程式碼用於設定估值器,進入原始碼,這裡同樣也是進入到父類 ValueAnimator 的 setEvaluator 方法,ObjectAnimator 沒有進行重寫。估值器傳入後,會對其進行合法性驗證,例如:估值器非空,mValues非空且長度大於零。如果合法性驗證不通過,則直接忽略傳入的估值器。注意哦(再敲黑板!)估值器沒有進行預設值的設定,至於他是如何正常運轉的,其實我們在前面講 “估值器定義” 一小節中就已經提到,但未深入探討,當然這裡也一樣先不探討,還未到時候,在 “第七行程式碼” 中會進行說明。

// ValueAnimator 類
PropertyValuesHolder[] mValues;

// ValueAnimator 類
public void setEvaluator(TypeEvaluator value) {
    if (value != null && mValues != null && mValues.length > 0) {
        mValues[0].setEvaluator(value);
    }
}
複製程式碼

值得注意

我們設定的估值器只會作用於 mValues 的第一項,但是我們這個場景中,也就只有一個元素。

mValues 是什麼?我們在 “第一行程式碼” 中就已經初始化完畢啦,忘記的童鞋往回看看?。看原始碼就是來來回回看,耐得住性子,才能更加牛x。

6、第五行程式碼

// 更新回撥
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue().getClass());
        Log.i(TAG, "onAnimationUpdate: " + animation.getAnimatedValue());
    }
});
複製程式碼

第五行程式碼用於設定更新回撥,進入原始碼,來到下面這段程式碼,同樣還是直接來到 其父類 ValueAnimator 中,這裡就比較簡單了,如果 mUpdateListeners 屬性未初始化,就建立一個列表,然後將更新監聽器新增入列表。

// ValueAnimator 類
ArrayList<AnimatorUpdateListener> mUpdateListeners = null;

// ValueAnimator 類
public void addUpdateListener(AnimatorUpdateListener listener) {
    if (mUpdateListeners == null) {
        mUpdateListeners = new ArrayList<AnimatorUpdateListener>();
    }
    mUpdateListeners.add(listener);
}
複製程式碼

值得注意

不知道童鞋們有沒有和小盆友一樣的錯覺,一直以為 AnimatorUpdateListenerAnimator.AnimatorListener(下一小節講)各自只能設定一個,如果多次設定是會覆蓋。看了原始碼才得知是有序列表持有。只怪自己之前太單純?。

7、第六行程式碼

// 生命週期監聽器
objectAnimator.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
        Log.i(TAG, "onAnimationStart: ");
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        Log.i(TAG, "onAnimationEnd: ");
    }

    @Override
    public void onAnimationCancel(Animator animation) {
        Log.i(TAG, "onAnimationCancel: ");
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
        Log.i(TAG, "onAnimationRepeat: ");
    }
});
複製程式碼

第六行程式碼進行設定生命週期監聽器,進入原始碼,這次是來到 ObjectAnimator 的爺爺類Animator 的 addListener 方法。

這裡有必要給出這幾個類的繼承關係和實現的介面,請記住他,因為在 “第七行程式碼” 中會提到。這裡立個FLAG(6),需要時我們再折回來看看。

graph LR
    A[ObjectAnimator] --> B[ValueAnimator] 
    B[ValueAnimator]  --> C[Animator] 
    B[ValueAnimator] -.-> D[AnimationHandler.AnimationFrameCallback]
複製程式碼

這裡的邏輯和 “第五行的程式碼” 的原始碼邏輯可以說是一樣的,都是先判空,如果為空,則進行例項化一個有序列表,然後將監聽器放入列表中。

// Animator 類
ArrayList<AnimatorListener> mListeners = null;

// Animator 類
public void addListener(AnimatorListener listener) {
    if (mListeners == null) {
        mListeners = new ArrayList<AnimatorListener>();
    }
    mListeners.add(listener);
}
複製程式碼

8、第七行程式碼

// 開啟
objectAnimator.start();
複製程式碼

來到了最後的第七行程式碼,這行程式碼雖然簡短,但卻蘊含著最多的祕密,讓我們來一點點揭開。進入start方法,看到如下程式碼。

// ObjectAnimator 類
public void start() {
	// FLAG(7)
    AnimationHandler.getInstance().autoCancelBasedOn(this);
    if (DBG) {
    	// 省略,用於除錯時的日誌輸出,DBG 是被定義為 靜態不可修改的 false,所以可以忽略這個分支
    	......
    }
    // FLAG(8)
    super.start();
}
複製程式碼

我們先進入第一行程式碼 的 第一小段,即 getInstance()。看到如下程式碼,其實看到getInstance這個方法名我們應該就會想到一個設計模式——單例模式,通過方法內的程式碼也確實驗證了這個猜想。但是有些許不同的是單例物件放在 ThreadLocal 中,用於確保的是 執行緒單例,而非程式中全域性單例,換句話說,不同執行緒的AnimationHandler物件是不相同的

// AnimationHandler 類
/**
 * 保證 AnimationHandler 當前執行緒單例
 */
public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();

// AnimationHandler 類
/**
 * 獲取 AnimationHandler 執行緒單例
 */
public static AnimationHandler getInstance() {
    if (sAnimatorHandler.get() == null) {
        sAnimatorHandler.set(new AnimationHandler());
    }
    return sAnimatorHandler.get();
}
複製程式碼

經過上面程式碼,我們便獲取到了 AnimationHandler 物件。AnimationHandler 是一個用於接受脈衝(即 垂直同步訊號),讓同一執行緒的動畫使用的計算時間是相同的,這樣的作用是讓同步動畫成為可能。 至於 AnimationHandler 是如何接受垂直同步訊號,我們繼續賣關子,稍後就會知道。

我們折回到 FLAG(7),看第二小段程式碼,具體程式碼如下,這裡的程式碼其實在我們設定的場景中是不會執行的,因為 mAnimationCallbacks 此時長度還為0。但我們還是進行深入的分析,具體的每行程式碼講解請看註釋,總結起來就是 如果 mAnimationCallbacks列表中的元素 和 引數objectAnimator物件 存在相同的目標物件和相同的PropertyValuesHolder,則將mAnimationCallbacks列表中對應的元素進行取消操作。

// AnimationHandler 類
/**
 * AnimationFrameCallback 此場景中就是 我們第一行程式碼例項化的 ObjectAnimator 物件,
 * 因為 ObjectAnimator 的父類實現了 AnimationFrameCallback 介面,具體繼承關係可以看 FLAG(6) 處的類圖
 */
private final ArrayList<AnimationFrameCallback> mAnimationCallbacks =
        new ArrayList<>();

// AnimationHandler 類
void autoCancelBasedOn(ObjectAnimator objectAnimator) {
	// 場景中,mAnimationCallback 此時長度為0,所以其實此迴圈不會進入
    for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
        AnimationFrameCallback cb = mAnimationCallbacks.get(i);
        if (cb == null) {
            continue;
        }
        // 將 相同的目標物件 且 PropertyValuesHolder完全一樣的動畫進行取消操作
        // 取消操作在 “第一行程式碼” 便已經詳細闡述,這裡就不再贅述
        if (objectAnimator.shouldAutoCancel(cb)) {
            ((Animator) mAnimationCallbacks.get(i)).cancel();
        }
    }
}

// ObjectAnimator 類
/**
 * 是否可以 進行取消
 */
boolean shouldAutoCancel(AnimationHandler.AnimationFrameCallback anim) {
    // 為空,則返回
    if (anim == null) {
        return false;
    }

    if (anim instanceof ObjectAnimator) {
        ObjectAnimator objAnim = (ObjectAnimator) anim;
        // 該動畫可以自動取消 且 當前的物件和anim 的所持的目標物件和PropertyValuesHolder一樣
        // 則可以 進行取消,返回true
        if (objAnim.mAutoCancel && hasSameTargetAndProperties(objAnim)) {
            return true;
        }
    }
    // 否則不取消
    return false;
}

// ObjectAnimator 類
/**
 * ObjectAnimator 是否有相同的 目標物件target 和 PropertyValuesHolder
 * PropertyValuesHolder 的初始化在第一行程式碼已經詳細講述,忘記的童鞋折回去再?看一遍
 */
private boolean hasSameTargetAndProperties(@Nullable Animator anim) {
    if (anim instanceof ObjectAnimator) {
    	// 獲取 PropertyValuesHolder 
        PropertyValuesHolder[] theirValues = ((ObjectAnimator) anim).getValues();
        // 目標物件相同 且 PropertyValuesHolder長度相同
        if (((ObjectAnimator) anim).getTarget() == getTarget() &&
                mValues.length == theirValues.length) {
			// 迴圈檢測 PropertyValuesHolder 中 屬性名是否 “完全相同”,只要有一個不同 則返回false
            for (int i = 0; i < mValues.length; ++i) {
                PropertyValuesHolder pvhMine = mValues[i];
                PropertyValuesHolder pvhTheirs = theirValues[i];
                if (pvhMine.getPropertyName() == null ||
                        !pvhMine.getPropertyName().equals(pvhTheirs.getPropertyName())) {
                    return false;
                }
            }
            // 全部相同,返回true
            return true;
        }
    }
    // 不是 ObjectAnimator 直接返回false
    return false;
}
複製程式碼

看完首行程式碼,我們來到呼叫 父類ValueAnimator 的 start 方法這行FLAG(8),進入該方法,具體程式碼如下,可以看到呼叫了 start() 的過載方法 start(boolean),playBackwards是用於標記是否要反向播放,顯然傳入的為false,表示正向播放。start方法中做了這幾件事:

  1. 初始化一些屬性,例如執行的狀態標記(注意此處 開始狀態mStarted便置為true);
  2. 幀和動畫的播放時間則置為-1;
  3. 新增動畫回撥,用於接受 垂直同步訊號;
  4. 設定當前的播放 fraction;
// ValueAnimator 類
public void start() {
    start(false);
}

// ValueAnimator 類
/**
 * @param playBackwards ValueAnimator 是否應該開始反向播放。
 */
private void start(boolean playBackwards) {
    // 必須要在有 looper 的執行緒中執行
    if (Looper.myLooper() == null) {
        throw new AndroidRuntimeException("Animators may only be run on Looper threads");
    }

    // 是否反向
    mReversing = playBackwards;
    // 是否接受脈衝,mSuppressSelfPulseRequested初始化為false,所以這裡為true,表示接受脈衝
    mSelfPulse = !mSuppressSelfPulseRequested;

    // 此處 playBackwards 為false,該分支不理會,處理反向播放
    if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
        if (mRepeatCount == INFINITE) {
            float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
            mSeekFraction = 1 - fraction;
        } else {
            mSeekFraction = 1 + mRepeatCount - mSeekFraction;
        }
    }

    // 將開始狀態(mStarted)置為true
    // 暫停狀態(mPaused)置為false
    // 執行狀態(mRunning)置為false
    // 是否終止動畫狀態(mAnimationEndRequested)置為false
    mStarted = true;
    mPaused = false;
    mRunning = false;
    mAnimationEndRequested = false;

    // 重置 mLastFrameTime,這樣如果動畫正在執行,呼叫 start() 會將動畫置於已啟動但尚未到達的第一幀階段。
    mLastFrameTime = -1;
    mFirstFrameTime = -1;
    mStartTime = -1;

    // 新增動畫回撥,用於接受 垂直同步訊號
    addAnimationCallback(0);

    // 此場景中,mStartDelay為0,所以進入分支
    if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
		// FLAG(14)
        startAnimation();

        // 設定 mSeekFraction,這個屬性是通過 setCurrentPlayTime() 進行設定
        if (mSeekFraction == -1) {
            // FLAG(16)
            setCurrentPlayTime(0);
        } else {
            setCurrentFraction(mSeekFraction);
        }
    }
}
複製程式碼

屬性的初始化和各自意義,我們就不單獨講解,使用到的時候自然就能體會到他的存在意義。所以我們直接進入到第三步,即新增動畫回撥addAnimationCallback的程式碼。

這裡進行判斷是否要接受脈衝,我們上面的程式碼已經將 mSelfPulse設定為true,表示需要接受脈衝,所以不進入if分支,來到下一行程式碼,是不是很熟悉?這裡獲取的便是我們上面已經初始化的 AnimationHandler,這裡呼叫了 AnimationHandleraddAnimationFrameCallback,同時把 自己this 和 延時delay(這裡為0)一同帶入。

// ValueAnimator 類
private void addAnimationCallback(long delay) {
	// 如果不接受脈衝,則不會新增回撥,這樣自然就中斷了脈衝帶來的更新
	// 在 start 方法中已經設定為 true,所以不進入if分支
    if (!mSelfPulse) {
        return;
    }
    getAnimationHandler().addAnimationFrameCallback(this, delay);
}

// ValueAnimator 類
public AnimationHandler getAnimationHandler() {
    return AnimationHandler.getInstance();
}
複製程式碼

這樣我們便來到了 AnimationHandleraddAnimationFrameCallback 方法,根據該方法的官方註釋可知,註冊的callback會在下一幀呼叫,但需要延時指定的delay之後,可是我們這裡的delay為0,所以在我們這場景中可以進行忽略,減少干擾因素。

來到第一行,因為 mAnimationCallbacks 此時長度為0,所以進入該if分支。 我們需要先進入 getProvider() 方法,待會再折回來,往下看

/**
 * Register to get a callback on the next frame after the delay.
 * 註冊回撥,可以讓下一幀進行回撥。
 */
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
    /**
     * 第一次進來的時候 mAnimationCallbacks 是空的,
     * 所以會向 {@link MyFrameCallbackProvider#mChoreographer} 提交一次回撥。
     */
    if (mAnimationCallbacks.size() == 0) {
    	// FLAG(9)
        getProvider().postFrameCallback(mFrameCallback);
    }

    /**
     * 此處的 callback 即為 ValueAnimator 和 ObjectAnimator
     * 因為 ObjectAnimator 繼承於 ValueAnimator,ValueAnimator 實現了 AnimationFrameCallback 介面
     * 這裡 callback 從 {@link ValueAnimator#start()} 傳進來,使用了 this
     */
    // FLAG(13)
    if (!mAnimationCallbacks.contains(callback)) {
        mAnimationCallbacks.add(callback);
    }

    // 記錄延時的回撥 和 延時的時間
    if (delay > 0) {
        mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
    }
}
複製程式碼

來到 getProvider 方法,這裡初始化一個 MyFrameCallbackProvider 物件,他負責與 Choreographer 進行互動。

這裡值得一提的是 MyFrameCallbackProvider 實現了 AnimationFrameCallbackProvider 介面(關係如下圖所示),而AnimationHandler中,提供定時的幀回撥,並不是規定一定要通過 Choreographer 來接收垂直同步來達到效果,也可以自己行實現 AnimationFrameCallbackProvider 介面,自行提供不同的定時脈衝來實現效果,來頂替這裡的 MyFrameCallbackProviderAnimationHandler 同時也提供了 setProvider 方法來進行設定該 AnimationFrameCallbackProvider 類。

graph LR
    A[MyFrameCallbackProvider] -.-> B[AnimationFrameCallbackProvider] 
複製程式碼
// AnimationHandler 類
private AnimationFrameCallbackProvider getProvider() {
    if (mProvider == null) {
        mProvider = new MyFrameCallbackProvider();
    }
    return mProvider;
}

// AnimationHandler 類
/**
 * 使用 Choreographer 提供定時脈衝 進行幀回撥
 */
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

    final Choreographer mChoreographer = Choreographer.getInstance();

	// 省略 介面的實現
	......
}
複製程式碼

話說回來 Choreographer 是什麼?

Choreographer 是用於接收定時脈衝(例如 垂直同步),協調 “動畫、輸入、繪製” 時機的類。 我們這裡不展開闡述 Choreographer 內部的運轉機制,但是我們必須知道的是,Android手機每秒會有60幀的回撥,即約16.66毫秒便會呼叫一次 Choreographer 中的 型別為FrameDisplayEventReceiver的mDisplayEventReceiver屬性 中的 onVsync 方法。後續還會繼續用到這裡的知識點(敲黑板了,要考的),來講解屬性動畫是怎麼動起來的,我們先打個標記FLAG(10)。

我們先折回 FLAG(9),看後半段 postFrameCallback 做了什麼操作,進入程式碼,具體如下,這裡做了一件很重要的事,就是 註冊進Choreographer,接收垂直同步訊號Choreographer 中多次過載了 postFrameCallbackDelayed 方法,最終在FLAG(10)處,將我們從 MyFrameCallbackProvider 傳入的 callback 儲存在了 ChoreographermCallbackQueues 中,這裡需要在打一個標記FLAG(11),後續需要再用到。

// AnimationHandler$MyFrameCallbackProvider 類
@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
    mChoreographer.postFrameCallback(callback);
}

// Choreographer 類
public void postFrameCallback(FrameCallback callback) {
    postFrameCallbackDelayed(callback, 0);
}

// Choreographer 類
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    if (callback == null) {
        throw new IllegalArgumentException("callback must not be null");
    }
    postCallbackDelayedInternal(CALLBACK_ANIMATION,
            callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

// Choreographer 類
private void postCallbackDelayedInternal(int callbackType,
                                             Object action,
                                             Object token,
                                             long delayMillis) {
    if (DEBUG_FRAMES) {
    	// 除錯時,日誌輸出
    	......
    }

    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        /**
         *  此處將 {@link FrameCallback} 新增到對應的回撥佇列中
         * 	FLAG(10)
         */
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}
複製程式碼

我們需要再次折回 FLAG(9),需要說明下傳入的引數 mFrameCallback, 實現了 Choreographer.FrameCallback 介面,這裡面會呼叫 doAnimationFrame 方法,這個先不展開,待會講到幀回撥時,在具體剖析。先來到下面的if分支,用於將自己(mFrameCallback)再次新增進 Choreographer,執行的邏輯和上面剛剛闡述的邏輯是一模一樣。為什麼還要再新增一次呢?這是因為新增進的回撥,在每次被呼叫後就會被移除,如果還想繼續接收到垂直訊號,則需要將自己再次新增。

private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
    	// FLAG(12)
    	// 這裡後面講
        doAnimationFrame(getProvider().getFrameTime());

		/**
         * 再次將自己新增進脈衝回撥中
         * 因為 {@link Choreographer#postFrameCallback(Choreographer.FrameCallback)} 每呼叫一次
         * 就會將新增的回撥移除
         */
        if (mAnimationCallbacks.size() > 0) {
            getProvider().postFrameCallback(this);
        }
    }
};
複製程式碼

折回到FLAG(13),就是下面這段程式碼,做了很普通的一件事,就是把我們在 “第一行程式碼” 例項化的 ObjectAnimator 物件存至 mAnimationCallbacks 回撥列表中。接下去的分支,我們這場景中不需理會,因為我們不做延時操作。

/**
 * 此處的 callback 即為 ValueAnimator 和 ObjectAnimator
 * 因為 ObjectAnimator 繼承於 ValueAnimator,ValueAnimator 實現了 AnimationFrameCallback 介面
 * 這裡 callback 從 {@link ValueAnimator#start()} 傳進來,使用了 this
 */
if (!mAnimationCallbacks.contains(callback)) {
    mAnimationCallbacks.add(callback);
}

if (delay > 0) {
    mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
複製程式碼

我們需要折回到FLAG(14),進入到 startAnimation 方法中,具體程式碼如下,這個方法做了如下幾個步驟:

  1. 初始化動畫,具體是設定關鍵幀集合的估值器;
  2. 將執行狀態置為true;
  3. 設定 mOverallFraction(這個屬性我們後面用到時在說明是什麼作用);
  4. 回撥 監聽器;

接下來我們分析第一和第四小點

// ValueAnimator 類
private void startAnimation() {
    // 省略跟蹤程式碼
    ......
    mAnimationEndRequested = false;
    // 初始化 動畫
    initAnimation();
    // 將執行狀態置為 true
    mRunning = true;
    
    if (mSeekFraction >= 0) {
        mOverallFraction = mSeekFraction;
    } else {
    	// 跟蹤動畫的 fraction,範圍從0到mRepeatCount + 1
    	// mRepeatCount 為我們設定動畫迴圈次數,我們這裡沒有設定,則預設為0,只執行一次
        mOverallFraction = 0f;
    }

	// FLAG(15)
    if (mListeners != null) {
        // 進行開始回撥
        notifyStartListeners();
    }
}
複製程式碼

先進行分析第一小點,我們進入 initAnimation 方法,當首次進入時,mInitialized 為false,所以進入該分支,這裡迴圈呼叫了 mValues元素(PropertyValuesHolder型別)init 方法。

// ValueAnimator 類
void initAnimation() {
    // 當首次進入時,mInitialized為false
    // 初始化 估值器Evaluator
    if (!mInitialized) {
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            mValues[i].init();
        }

        // 將初始化標記 轉為true,防止多次初始化
        mInitialized = true;
    }
}
複製程式碼

進入到 PropertyValuesHolderinit 方法中,程式碼如下,該方法做了一件事,就是初始化 mKeyframes估值器,而這估值器在我們講述 “第四行程式碼” 時,就已經置入到 PropertyValuesHolder 中,這一方法是將這個估值器置入關鍵幀集合中。

// PropertyValuesHolder 類
/**
 * 初始化 Evaluator 估值器
 */
void init() {
    /**
     * 如果 Evaluator 為空,則根據 mValueType 型別進行設定,但是也只是提供
     * {@link IntEvaluator} 和 {@link FloatEvaluator}
     * 如果均不是這兩種型別,則為null
     */
    if (mEvaluator == null) {
        // We already handle int and float automatically, but not their Object
        // equivalents
        mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
                (mValueType == Float.class) ? sFloatEvaluator :
                        null;
    }

    // 如果有估值器,則進行設定
    if (mEvaluator != null) {
        // KeyframeSet knows how to evaluate the common types - only give it a custom
        // evaluator if one has been set on this class
        mKeyframes.setEvaluator(mEvaluator);
    }

}
複製程式碼

這裡需要說句題外話,下面下面這段程式碼,返回的是false,也就是說?上面設定估值器的程式碼中,當mEvaluator為空時,如果我們使用的簡單型別的float,此處的並不會使用預設的sFloatEvaluator,而是還是為null。

System.out.println(float.class == Float.class);  // 輸出的是false
複製程式碼

接下來進行分析第四小點,折回到FLAG(15),此時 mListeners 已經在 “第六行程式碼” 時就初始化,並新增了一個監聽器,所以會進入該if分支,進入 notifyStartListeners 方法,具體程式碼如下,這裡便回撥到了我們 “第六行程式碼” 設定的生命週期監聽器的 onAnimationStart 方法中,同時將自身 ObjectAnimator 物件作為引數帶出。

// ValueAnimator 類
private void notifyStartListeners() {
    // 有 回撥監聽器,且從未回撥
    if (mListeners != null && !mStartListenersCalled) {
        ArrayList<AnimatorListener> tmpListeners =
                (ArrayList<AnimatorListener>) mListeners.clone();
        int numListeners = tmpListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            /**
             * 進行回撥開始,這裡便 回撥到 我們設定
             * {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator)}
             * 的方法中
             */
            tmpListeners.get(i).onAnimationStart(this, mReversing);
        }
    }

    mStartListenersCalled = true;
}

// Animator$AnimatorListener 類
default void onAnimationStart(Animator animation, boolean isReverse) {
   onAnimationStart(animation);
}
複製程式碼

這裡一路呼叫進來,算是比較深遠了,但無大礙,我們回到FLAG(16),繼續看 start 方法的最後一行程式碼 setCurrentPlayTime(0) 的具體內容,程式碼如下,這方法是為了讓如果動畫時長小於或等於零時,直接到達動畫的末尾,即fraction置為1。

// ValueAnimator 類
public void setCurrentPlayTime(long playTime) {
    // 如果設定的 mDuration 為 2000,playTime 為 0,則 fraction 為 0
    // 如果設定的 mDuration 為 0,則 fraction 為 1,直接到最後一個關鍵幀
    float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
    setCurrentFraction(fraction);
}
複製程式碼

接下來看 setCurrentFraction 做了什麼操作,第一行的 initAnimation 在之前就已經執行過了,所以並不會再次初始化,clampFraction方法是為了讓 fraction 落在合法的區域內,即 [0,mRepeatCount + 1],這裡不再展開(篇幅太長了?)。

來到 if-else,isPulsingInternal方法內判斷的mLastFrameTime 是否大於等於0,但是 mLastFrameTime 到目前為止還是 -1,所以進入else分支,將fraction儲存至mSeekFraction。

// ValueAnimator 類
public void setCurrentFraction(float fraction) {
    // 初始化 動畫,但這裡其實只是做了 估值器的賦值初始化
    initAnimation();

    // 獲取 合法的fraction
    fraction = clampFraction(fraction);

    mStartTimeCommitted = true;

    // 動畫是否已進入動畫迴圈
    if (isPulsingInternal()) {
        // 獲取動畫已經使用的時長
        long seekTime = (long) (getScaledDuration() * fraction);
        // 獲取當前動畫時間
        long currentTime = AnimationUtils.currentAnimationTimeMillis();
        // 僅修改動畫執行時的開始時間。 Seek Fraction將確保非執行動畫跳到正確的開始時間。
        mStartTime = currentTime - seekTime;
    } else {
        // 如果動畫迴圈尚未開始,或者在開始延遲期間。seekTime,一旦延遲過去,startTime會基於seekTime進行調整。
        mSeekFraction = fraction;
    }

    // 總fraction,攜帶有迭代次數
    mOverallFraction = fraction;

    // 計算當次迭代的 fraction
    final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
    
    // 根據 fraction ,計算出對應的value
    // FLAG(17)
    animateValue(currentIterationFraction);
}
複製程式碼

接下來是 getCurrentIterationFraction 方法,這個方法用於獲取當前迭代的 fraction,因為我們需要知道的是,如果設定了多次迴圈播放動畫,即mRepeatCount>0時,則fraction是包含有mRepeatCount次數的。而這個方法就是去除了次數,只剩下當次的進度,即範圍為 [0,1] 。

// ValueAnimator 類
private float getCurrentIterationFraction(float fraction, boolean inReverse) {
    // 確保 fraction 的範圍在合法範圍 [0,mRepeatCount+1] 中
    fraction = clampFraction(fraction);
    // 當前迭代次數
    int iteration = getCurrentIteration(fraction);
    /**
     * fraction 是 包含有 mRepeatCount 的值
     * iteration 是 迭代的次數
     * 兩者相減 fraction - iteration 得出的 currentFraction 則為當前迭代中的 進度
     */
    float currentFraction = fraction - iteration;
    // 計算最終當次迭代的 fraction 值,主要是受 inReverse 和 REVERSE 的影響
    return shouldPlayBackward(iteration, inReverse) ? 1f - currentFraction : currentFraction;
}
複製程式碼

我們回到 FLAG(17),進入 animateValue 方法,具體程式碼如下。我們需要明確的是,傳進來的引數是當次的進度,也就是不含迴圈次數的。

我們會先進入到 ObjectAnimatoranimateValue,但是原始碼會先進入其父類 ValueAnimatoranimateValue。所以我們先進入父類的 animateValue

看到第一行程式碼,你或許就明白了,我們在 “第三行程式碼” 設定的插值器就在這個時候發揮作用了,同時會 將插值器返回的值設定回 fraction,起到改變進度的快慢的作用 。(這便揭開了我們在 “插值器” 一節中賣的關子)

// ObjectAnimator 類
void animateValue(float fraction) {
    final Object target = getTarget();
    // 不會進入,此時的target在 “第一行程式碼” 時就設定了,所以不為空
    if (mTarget != null && target == null) {
        cancel();
        return;
    }

    // 進入父類 ValueAnimator
    super.animateValue(fraction);
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
    	// FLAG(25)
        mValues[i].setAnimatedValue(target);
    }
}

// ValueAnimator 類
void animateValue(float fraction) {
    // 通過 插值器 進行計算出 fraction
    fraction = mInterpolator.getInterpolation(fraction);

    // 當前次數的進度
    mCurrentFraction = fraction;

    // 迴圈 所有的 PropertyValuesHolder,進行估值器計算
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
        mValues[i].calculateValue(fraction);
    }

    /**
     * 進行回撥 {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)}
     */
    // FLAG(21)
    if (mUpdateListeners != null) {
        int numListeners = mUpdateListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            mUpdateListeners.get(i).onAnimationUpdate(this);
        }
    }
}
複製程式碼

緊接著進行迴圈呼叫 mValues 中的元素的 calculateValue 方法(我們這場景中 mValues 的元素其實只有一個),進入該方法,可以看到如下程式碼。這裡進入的是 PropertyValuesHolder 的子類 FloatPropertyValuesHolder

// FloatPropertyValuesHolder 類
void calculateValue(float fraction) {
	// mFloatKeyframes 在 “第一行程式碼” 時就已經初始化
    mFloatAnimatedValue = mFloatKeyframes.getFloatValue(fraction);
}
複製程式碼

我們接著進入 getFloatValue 方法,其具體實現類是 FloatKeyframeSet。具體程式碼如下,getFloatValue 中分了幾個情況,我們接下來分情況討論,往下走。

// FloatKeyframeSet 類
/**
 * 獲取當前 進度數值為fraction的 value
 * 落實一個場景: 0f, 2f, 5f
 * mKeyframes 中存了三個 FloatKeyframe
 * mNumKeyframes 則為 3
 *
 * @param fraction The elapsed fraction of the animation
 * @return
 */
@Override
public float getFloatValue(float fraction) {
	// FLAG(18)
    if (fraction <= 0f) {
        // 獲取 0f 關鍵幀
        final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0);
        // 獲取 2f 關鍵幀
        final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(1);
        // 獲取 0f 關鍵幀的值 即 0f
        float prevValue = prevKeyframe.getFloatValue();
        // 獲取 2f 關鍵幀的值 即 1f
        float nextValue = nextKeyframe.getFloatValue();
        // 獲取 0f 關鍵幀的fraction,這裡為0
        float prevFraction = prevKeyframe.getFraction();
        // 獲取 2f 關鍵幀的fraction,這裡為1/2
        float nextFraction = nextKeyframe.getFraction();

        // 這裡的插值器為空,並不會執行該分支
        final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        
        float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
        return mEvaluator == null ?
                prevValue + intervalFraction * (nextValue - prevValue) :
                ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
                        floatValue();

    } else if (fraction >= 1f) {	// FLAG(19)
        final FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 2);
        final FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(mNumKeyframes - 1);
        float prevValue = prevKeyframe.getFloatValue();
        float nextValue = nextKeyframe.getFloatValue();
        float prevFraction = prevKeyframe.getFraction();
        float nextFraction = nextKeyframe.getFraction();
        final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        float intervalFraction = (fraction - prevFraction) / (nextFraction - prevFraction);
        return mEvaluator == null ?
                prevValue + intervalFraction * (nextValue - prevValue) :
                ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
                        floatValue();
    }

	// FLAG(20)
    // 初始化第一幀, 0f
    FloatKeyframe prevKeyframe = (FloatKeyframe) mKeyframes.get(0);
    // 從第二幀開始迴圈
    for (int i = 1; i < mNumKeyframes; ++i) {
        // 相對於prevKeyframe,取下一幀
        FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i);

		// 判斷是否落在 該區間
        if (fraction < nextKeyframe.getFraction()) {
            final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
            float intervalFraction = (fraction - prevKeyframe.getFraction()) /
                    (nextKeyframe.getFraction() - prevKeyframe.getFraction());

            float prevValue = prevKeyframe.getFloatValue();
            float nextValue = nextKeyframe.getFloatValue();
            if (interpolator != null) {
                intervalFraction = interpolator.getInterpolation(intervalFraction);
            }
            // 估值器計算
            return mEvaluator == null ?
                    prevValue + intervalFraction * (nextValue - prevValue) :
                    ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
                            floatValue();
        }
		// 變換前一幀
        prevKeyframe = nextKeyframe;
    }

    // 正常情況下不應該執行到這
    return ((Number) mKeyframes.get(mNumKeyframes - 1).getValue()).floatValue();
}
複製程式碼

以下圖片均為手寫,勿噴?

情況一:fraction = 0。進入FLAG(18)

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

情況二:fraction = 1/4。進入FLAG(20)

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

情況三:fraction = 3/4。進入FLAG(20)

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

情況四:fraction = 1。進入FLAG(19)

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI
經過上面四種情況,我們可以知道 intervalFraction 值,即為 當前幀段的比例數(幀段即為 0f-2f,2f-5f) 而 返回值 即為 fraction 通過估值器轉換為 真實需要的值,即我們程式設計師可以拿來用,例如我們這裡需要的是 0f-5f的值。

還記得我們在 “估值器” 一小節中賣的關子麼?情況二中的公式就是我們用於計算 此處的返回值,在估值器為null時。如果估值器不為null,則按照設定的估值器邏輯計算。

mEvaluator == null ?
				 prevValue + intervalFraction * (nextValue - prevValue) :
				 ((Number) mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
				         floatValue();
複製程式碼

你可能會有疑惑,我們這場景中不是有設定一個 FloatEvaluator 估值器麼?確實是,但FloatEvaluator內部邏輯其實就和我們估值器為null時是一模一樣的。具體程式碼如下

public class FloatEvaluator implements TypeEvaluator<Number> {
    public Float evaluate(float fraction, Number startValue, Number endValue) {
        float startFloat = startValue.floatValue();
        return startFloat + fraction * (endValue.floatValue() - startFloat);
    }
}
複製程式碼

至此我們得到了 估值器計算出來的我們需要的值

我們折回 FLAG(21),看到 mUpdateListeners 這屬性,童鞋們應該也知道這是在 “第五行程式碼” 設定的更新監聽器,進入該分支,會迴圈著呼叫更新監聽器的 onAnimationUpdate 方法。這便進入到了我們設定的更新監聽器的程式碼中。

接下來我們需要折回到父類 ObjectAnimatoranimateValue,即FLAG(25)。會進行迴圈呼叫 mValuessetAnimatedValue,而我們這裡的場景的 mValuesFloatPropertyValuesHolder 型別,所以會呼叫該類的 setAnimatedValue, 具體程式碼如下。而 FLAG(26) 就是將我們剛才上面所計算的 mFloatAnimatedValue 值通過 native方法nCallFloatMethod 設定到對應的物件的方法中,也就是我們此處場景中的 ZincView類的setZinc方法

// FloatPropertyValuesHolder 類
void setAnimatedValue(Object target) {
    if (mFloatProperty != null) {
        mFloatProperty.setValue(target, mFloatAnimatedValue);
        return;
    }
    if (mProperty != null) {
        mProperty.set(target, mFloatAnimatedValue);
        return;
    }
    if (mJniSetter != 0) {
    	// FLAG(26)
        nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue);
        return;
    }
    if (mSetter != null) {
        try {
            mTmpValueArray[0] = mFloatAnimatedValue;
            mSetter.invoke(target, mTmpValueArray);
        } catch (InvocationTargetException e) {
            Log.e("PropertyValuesHolder", e.toString());
        } catch (IllegalAccessException e) {
            Log.e("PropertyValuesHolder", e.toString());
        }
    }
}
複製程式碼

至此,我們一個流程走完,但並不代表著就已經完成了,因為這裡面還只是第一幀的回撥,而後續的幀回撥還未闡述,還有動畫的終止還未說清。所以我們繼續前行,先來 解決後續幀回撥問題

還記得我們在講 Choreographer 時,通過 AnimationHandler 注入了一個回撥麼?這個時候後續的幀回撥就全靠他了。我們前面說過每次 “垂直同步” 訊號的到來,回撥用到 Choreographer$FrameDisplayEventReceiveronVsync 的,而該方法最終會呼叫到我們在FLAG(11)放入回撥佇列 mCallbackQueues 中的 mFrameCallbackdoFrame 方法。這就回到了我們FLAG(12)標記的地方。

// AnimationHandler 類
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        mCurrentFrameTime = System.currentTimeMillis();
        doAnimationFrame(mCurrentFrameTime);
        if (mAnimationCallbacks.size() > 0) {
            getProvider().postFrameCallback(this);
        }
    }
};
複製程式碼

進入 doAnimationFrame 方法,便看到對 mAnimationCallbacks 進行了遍歷,呼叫 doAnimationFrame 方法。 而 mAnimationCallbacks 是我們在講解 addAnimationFrameCallback方法時,就將傳進來的 ObjectAnimator 物件放入其中的。

// AnimationHandler 類
private void doAnimationFrame(long frameTime) {
    long currentTime = SystemClock.uptimeMillis();
    final int size = mAnimationCallbacks.size();
    for (int i = 0; i < size; i++) {
        final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
        if (callback == null) {
            continue;
        }
        // isCallbackDue 方法用於剔除需要延時呼叫的回撥
        // 如果該 callback 是在延時佇列的,並且延時還未完成,不進行回撥
        if (isCallbackDue(callback, currentTime)) {
			// 進入這一行
            callback.doAnimationFrame(frameTime);
            if (mCommitCallbacks.contains(callback)) {
                getProvider().postCommitCallback(new Runnable() {
                    @Override
                    public void run() {
                        commitAnimationFrame(callback, getProvider().getFrameTime());
                    }
                });
            }
        }
    }
    cleanUpList();
}
複製程式碼

當呼叫 doAnimationFrame 方法,則來到了下面的這段程式碼,該方法主要是對 啟動時間進行容錯處理,然後保證動畫進行啟動,同時在 animateBasedOnTime 方法中進行更新的監聽回撥(我們接下來分析),最後根據 animateBasedOnTime 的返回值,判斷是否動畫已經結束,結束的話進行 動畫生命週期 的回撥(待會也會分析)。

// ValueAnimator 類
public final boolean doAnimationFrame(long frameTime) {
	// 初始化第一幀,同時考慮延時
    if (mStartTime < 0) {
        mStartTime = mReversing ? frameTime : frameTime + (long) (mStartDelay * sDurationScale);
    }

    // 處理 暫停 和 恢復 的情況,這裡我們不考慮
    if (mPaused) {
        mPauseTime = frameTime;
        removeAnimationCallback();
        return false;
    } else if (mResumed) {
        mResumed = false;
        if (mPauseTime > 0) {
            mStartTime += (frameTime - mPauseTime);
        }
    }
	
	// mRunning 在 startAnimation()方法中就被置為了 true
	// 但實際程式碼情況是 先新增回撥,再呼叫 startAnimation方法
	// 所以有可能會出現 幀回撥 快於 startAnimation方法 先執行,
	// 如果出現這種情況,則此時的 mRunning狀態值為false,就進入此分支進行處理
    if (!mRunning) {
        // 處理延時操作,如果未延時,此時的 mStartTime==frameTime,在首行程式碼便是做這操作
        if (mStartTime > frameTime && mSeekFraction == -1) {
            return false;
        } else {
            // 如果還未執行,則先將 mRunning置為true,然後啟動動畫,startAnimation的邏輯在前面已經闡述
            mRunning = true;
            startAnimation();
        }
    }

	// 第一次進來時,mLastFrameTime為-1,則進入分支
	// mLastFrameTime用於記錄 最後一幀到達的時間(以毫秒為單位)
    if (mLastFrameTime < 0) {
    	// 這裡是進行 mStartTime 的調整,因為 初始化開始時間 和 實際繪製幀 之間是有可能存在偏差
    	// 我們這場景中 mSeekFraction 一直為 -1,所以無需理會
        if (mSeekFraction >= 0) {
            long seekTime = (long) (getScaledDuration() * mSeekFraction);
            mStartTime = frameTime - seekTime;
            mSeekFraction = -1;
        }
        mStartTimeCommitted = false;
    }
    // 重新整理最後一幀到達的時間
    mLastFrameTime = frameTime;

	// 這一句是為了保證 當前幀時間 必須在開始的時間之後。
	// 保證不會逆向而行的出現,但這種情況很少見。
    final long currentTime = Math.max(frameTime, mStartTime);
    // 這裡面 便進行了值的回撥,我們接下來具體分析
    boolean finished = animateBasedOnTime(currentTime);

	// 是否動畫已結束,結束的話進行生命週期的回撥通知
    if (finished) {
        endAnimation();
    }
    return finished;
}
複製程式碼

進入 animateBasedOnTime 方法,該方法會通過當前時間計算出當前動畫的進度,最後通過 animateValue 方法,進行更新回撥,這樣就達到了 後續幀 的更新目的。

// ValueAnimator 類
boolean animateBasedOnTime(long currentTime) {
    boolean done = false;
    if (mRunning) {
        // 獲取縮放時長,但縮放因子為 1,所以一直為動畫時長
        final long scaledDuration = getScaledDuration();

        // 計算 fraction ,其實就是 已經執行時間佔 動畫時長的百分比
        final float fraction = scaledDuration > 0 ?
                (float) (currentTime - mStartTime) / scaledDuration : 1f;

        // 獲取 動畫的整體進度 (帶迴圈次數)
        final float lastFraction = mOverallFraction;

        // 是否為新的迭代
        final boolean newIteration = (int) fraction > (int) lastFraction;

        // 最後一次迭代完成
       	// FLAG(22)
        final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
                (mRepeatCount != INFINITE);

        // 如果時長為0,則直接結束
        if (scaledDuration == 0) {
            // 0時長的動畫,忽略重複計數 並 結束動畫
            done = true;
        } else if (newIteration && !lastIterationFinished) {    // 為新的迭代 且 不是最後一次
        	// 回撥 動畫迴圈次數 
            if (mListeners != null) {
                int numListeners = mListeners.size();
                for (int i = 0; i < numListeners; ++i) {
                    mListeners.get(i).onAnimationRepeat(this);
                }
            }
        } else if (lastIterationFinished) { // 最後一次
            done = true;
        }

        // 更新 動畫的整體進度
        mOverallFraction = clampFraction(fraction);
        // 當前迭代的fraction(即不包含迭代次數),getCurrentIterationFraction方法在前面已經分析
        float currentIterationFraction = getCurrentIterationFraction(
                mOverallFraction, mReversing);
        // 進行值更新回撥
        animateValue(currentIterationFraction);
    }
    return done;
}
複製程式碼

最後就是 動畫終止 的問題,我們前面也提到了根據 animateBasedOnTime的返回值來決定是否終止動畫,而在 animateBasedOnTime 方法中,返回true的地方,有兩個:

  1. 動畫時長為0;
  2. 最後一次迭代完畢,至於判斷是否完成最後一次迭代,則通過判斷當前進度是否已經大於我們迴圈的次數,並且動畫不是無限迴圈播放,判斷的程式碼可以看FLAG(22)。

如果 animateBasedOnTime 返回了true,便執行終止程式碼,即執行 endAnimation 方法,具體程式碼如下。可以看到,該方法主要是執行標記位的復位回撥的清楚生命週期監聽器回撥。在FLAG(23)的程式碼,則最終回撥到我們在 “第六行程式碼” 時設定的 生命週期監聽器。

private void endAnimation() {
    // 如果已經終止了,就不再重複執行
    if (mAnimationEndRequested) {
        return;
    }

    // 移除 回撥
    removeAnimationCallback();

    // 將 動畫 置為已經 終止
    mAnimationEndRequested = true;
    mPaused = false;

    boolean notify = (mStarted || mRunning) && mListeners != null;
    /**
     * 如果有 需要回撥, 但還未進行執行,說明 需要先回撥一次
     * {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator)}
     */
    if (notify && !mRunning) {
        notifyStartListeners();
    }

    mRunning = false;
    mStarted = false;
    mStartListenersCalled = false;
    mLastFrameTime = -1;
    mFirstFrameTime = -1;
    mStartTime = -1;

    /**
     * 呼叫回撥 {@link android.animation.Animator.AnimatorListener#onAnimationEnd(Animator)}
     */
    if (notify && mListeners != null) {
        ArrayList<AnimatorListener> tmpListeners =
                (ArrayList<AnimatorListener>) mListeners.clone();
        int numListeners = tmpListeners.size();
        for (int i = 0; i < numListeners; ++i) {
        	// FLAG(23)
            tmpListeners.get(i).onAnimationEnd(this, mReversing);
        }
    }

    mReversing = false;
    if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
        Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                System.identityHashCode(this));
    }
}
複製程式碼

最後我們還需要折回去FLAG(24),說下我們經常用來終止動畫的 cancel 方法。cancel 方法的具體程式碼如下,我們會發現如果已經開始動畫,但未執行(即mRunning 為 false),則會先走一次 notifyStartListeners 方法,保證呼叫了 生命週期監聽器中的 onAnimationStart 方法,緊接著呼叫了 onAnimationCancel 方法,最後執行我們上面提到的 endAnimation 方法進行終止動畫,並且回撥 onAnimationEnd 方法。

@Override
public void cancel() {
    if (Looper.myLooper() == null) {
        throw new AndroidRuntimeException("Animators may only be run on Looper threads");
    }

    /**
     * 如果已經請求結束,則通過前一個end()或cancel()呼叫,執行空操作
     * 直到動畫再次啟動。
     */
    if (mAnimationEndRequested) {
        return;
    }

    /**
     * 當動畫已經開始 或 已經執行 並且需要回撥
     */
    if ((mStarted || mRunning) && mListeners != null) {
        /**
         * 如果還沒執行,則先進行回撥 {@link android.animation.Animator.AnimatorListener#onAnimationStart(Animator)}
         */
        if (!mRunning) {
            // If it's not yet running, then start listeners weren't called. Call them now.
            notifyStartListeners();
        }

        ArrayList<AnimatorListener> tmpListeners =
                (ArrayList<AnimatorListener>) mListeners.clone();
        for (AnimatorListener listener : tmpListeners) {
            /**
             * 進行回撥 {@link android.animation.Animator.AnimatorListener#onAnimationCancel(Animator)}
             */
            listener.onAnimationCancel(this);
        }
    }

    // 進行終止動畫
    endAnimation();

}
複製程式碼

至此,屬性動畫的原始碼分析便完成了。

四、實戰

1、多維雷達圖

文章開頭出現的就是以下效果圖,現在我們來進行拆解實現。

效果圖

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

動畫分析

繪製相對應維度的雷達圖,在設定完資料後,進行設定屬性動畫,最後根據屬性動畫回撥值進行每個維度的展開。emmm,有些抽象。我們進行拆解為需要的零件:

  1. 每個頂點的座標;
  2. 維度展開的屬性動畫;

準備零件

(1)頂點座標 一圖勝千言,我們以六維雷達圖為例,以比較有代表性的A,B,C三點來計算其座標。但這裡面有一個前提是,需要將畫布的原點移至view的中心。接下來具體的計算請看圖,中間涉及到一些簡單的三角函式,這裡就不過多的說明。

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

根據圖片中的計算規則,我們可以得知以 畫布的負y軸 為基準,依次使用 sin(角度) * L 得出該點的 x座標,用 cos(角度) * L 得出該點的 y座標 。具體的程式碼如下:

// 迴圈遍歷計算頂點座標
for (int i = 0; i < mDimenCount; ++i) {
    PointF point = new PointF();
    // 當前角度
    double curAngle = i * mAngle;
    // 轉弧度制
    double radian = Math.toRadians(curAngle);
    // 計算其 x、y 的座標
    // y軸需要進行取反,因為canvas的座標軸和我們數學中的座標軸的y軸正好是上下相反的
    point.x = (float) (mLength * Math.sin(radian));
    point.y = (float) -(mLength * Math.cos(radian));
    mVertexList.add(point);
}
複製程式碼

(2)維度展開的屬性動畫 從第一小節我們得到了所有頂點的座標,再根據傳入的資料(資料是以百分比傳入,即0f-1f),便可以計算出每個維度的資料的最終頂點座標,具體程式碼如下

/**
 * 計算資料的頂點座標
 *
 * @param isBase 是否為 基礎資料
 */
private void calculateDataVertex(boolean isBase) {

    List<Data> calDataList = isBase ? mBaseDataList : mDataList;

    for (int i = 0; i < calDataList.size(); ++i) {

        Data data = calDataList.get(i);

        // 獲取 比例資料
        List<Float> pointDataList = data.getData();

        // 設定路徑
        Path curPath = new Path();
        data.setPath(curPath);

        curPath.reset();
        for (int j = 0; j < pointDataList.size(); ++j) {

            // 當前維度的資料比例
            float ratio = pointDataList.get(j);
            // 當前維度的頂點座標
            PointF curDimenPoint = mVertexList.get(j);

            if (j == 0) {
                curPath.moveTo(curDimenPoint.x * ratio,
                        curDimenPoint.y * ratio);
            } else {
                curPath.lineTo(curDimenPoint.x * ratio,
                        curDimenPoint.y * ratio);
            }

        }
        curPath.close();

    }

}
複製程式碼

經過以上程式碼的計算,得到每個資料中每個維度的最終頂點最座標,最後就是設定的屬性動畫起始值和終止值,以及更新處理。

起始值當然是 0,而終止值是 資料量個數 * (維度數-1),動畫時長為 每個維度的動畫時長 * 終止值。具體如下程式碼

mTotalLoopCount = (mDimenCount - 1) * mDataList.size();
mAnimator = ValueAnimator.ofFloat(0f, mTotalLoopCount);
mAnimator.setDuration(DURATION * mTotalLoopCount);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {

        float value = (float) animation.getAnimatedValue();

        // 整數部分即為當前的動畫資料下標
        mCurLoopCount = (int) value;

        // 小數部分極為當前維度正在展開的進度百分比
        mAnimCurValue = value - mCurLoopCount;

        invalidate();
    }
});
mAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        // 動畫結束,將狀態置為初始狀態,並再重新整理一次,讓最後的資料全部顯示
        mCurState = INIT;
        invalidate();
    }
});
複製程式碼

最後就是如何將這個值使用起來,因為我們傳入的是浮點數,所以在 AnimatorUpdateListener 回撥時,獲得的數會有 整數部分小數部分 ,對 整數部分 進行 除以(維度數-1),得到 當前的資料量下標;對 整數部分 進行 (維度數-1)取餘,再加1,得到 當前資料的維度數,而小數部分就是我們的維度進度。 程式碼如下:

// 當前資料的下標(-1因為第一個維度不用動畫)
int curIndex = mCurLoopCount / (mDimenCount - 1);
// 當前資料的維度(-1因為第一個維度不用動畫)
int curDimen = (mCurLoopCount % (mDimenCount - 1)) + 1;
複製程式碼

組裝零件

零件都已經備好了,組裝起來就是我們看到的效果。因為程式碼稍微較長,但主要點我們已經攻破了,並且程式碼註釋也比較多,這裡就不再貼出來了,需要的請進傳送門

2、錶盤指示器

文章最開始出現的第二個就是以下這張效果圖,具體的操作其實和 “多維雷達圖” 沒有太多的出入,只是將維度的展開,變為 畫布的旋轉後繪製指標,達到指標旋轉的效果,再加上插值器的公式輔助,到達擺動迴盪的效果。限於文章篇幅過長這裡就不再具體闡述,有興趣的同學請入傳送門

效果圖

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

五、寫在最後

屬性動畫是高階UI中又一把不可缺少的利器,使用得當,能讓整個介面富有互動感,提高使用者體驗。最後如果你從這篇文章有所收穫,請給我個贊❤️,並關注我吧。文章中如有理解錯誤或是晦澀難懂的語句,請評論區留言,我們進行討論共同進步。你的鼓勵是我前進的最大動力。

高階UI系列的Github地址:請進入傳送門,如果喜歡的話給我一個star吧?

如果需要更多的交流與探討,可以通過以下微信二維碼加小盆友好友。

帶有活力的屬性動畫原始碼分析與實戰——Android高階UI

相關文章