天天看點

帶有活力的屬性動畫源碼分析與實戰——Android進階UI

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

目錄

一、前言

二、插值器與估值器

三、源碼解析

四、實戰

五、寫在最後

一、前言

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

1、多元雷達圖

帶有活力的屬性動畫源碼分析與實戰——Android進階UI

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提供的插值器 是一摸一樣的,如下所示

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 的 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 的建構語句,我們先進入PropertyValuesHolder 的 ofFloat 方法中,能看到如下代碼段,執行個體化了一個 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);
}
           

進入 KeyframeSet 的 ofFloat 方法(具體代碼看下面?),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 的構造方法。這裡需要先說明下這幾個類的繼承關系,後面也會用到,注意看清這幾個類和接口的名字。

進入 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、第四行代碼

第四行代碼用于設定估值器,進入源碼,這裡同樣也是進入到父類 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);
}
           
值得注意

不知道童鞋們有沒有和小盆友一樣的錯覺,一直以為 AnimatorUpdateListener 和 Animator.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),需要時我們再折回來看看。

這裡的邏輯和 “第五行的代碼” 的源碼邏輯可以說是一樣的,都是先判空,如果為空,則進行執行個體化一個有序清單,然後将監聽器放入清單中。

// 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,這裡調用了 AnimationHandler 的 addAnimationFrameCallback,同時把 自己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();
}
           

這樣我們便來到了 AnimationHandler 的 addAnimationFrameCallback 方法,根據該方法的官方注釋可知,注冊的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 接口,自行提供不同的定時脈沖來實作效果,來頂替這裡的 MyFrameCallbackProvider。AnimationHandler 同時也提供了 setProvider 方法來進行設定該 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 儲存在了 Choreographer 的 mCallbackQueues 中,這裡需要在打一個标記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;
    }
}
           

進入到 PropertyValuesHolder 的 init 方法中,代碼如下,該方法做了一件事,就是初始化 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。

接下來進行分析第四小點,折回到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 方法,具體代碼如下。我們需要明确的是,傳進來的參數是當次的進度,也就是不含循環次數的。

我們會先進入到 ObjectAnimator 的 animateValue,但是源碼會先進入其父類 ValueAnimator 的 animateValue。是以我們先進入父類的 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 方法。這便進入到了我們設定的更新監聽器的代碼中。

接下來我們需要折回到父類 ObjectAnimator 的 animateValue,即FLAG(25)。會進行循環調用 mValues 的 setAnimatedValue,而我們這裡的場景的 mValues 為 FloatPropertyValuesHolder 類型,是以會調用該類的 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$FrameDisplayEventReceiver 的 onVsync 的,而該方法最終會調用到我們在FLAG(11)放入回調隊列 mCallbackQueues 中的 mFrameCallback 的 doFrame 方法。這就回到了我們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中又一把不可缺少的利器,使用得當,能讓整個界面富有互動感,提高使用者體驗。最後如果你從這篇文章有所收獲,請給我個贊❤️,并關注我吧。文章中如有了解錯誤或是晦澀難懂的語句,請評論區留言,我們進行讨論共同進步。你的鼓勵是我前進的最大動力。