終於在新的一年的第一天完成了本篇文章,小盆友在此祝賀您,萬事如意,闔家幸福。?
目錄
一、前言
二、插值器與估值器
三、原始碼解析
四、實戰
五、寫在最後
一、前言
對於越來越追求豐富的互動體驗的客戶端,一個帶有動態效果的介面已經是不可避免。屬性動畫就是這其中必不可少的一把利器,所以今天便來分享下 屬性動畫融合在實際場景中 的使用,以及進行 原始碼分析。話不多說,先看看今天的實戰效果圖,然後開始進行分享之旅。
1、多維雷達圖
2、錶盤指示器
3、活力四射的購物車老規矩,程式碼在實戰篇會給出,透過 API 看清原始碼,各種效果便是順手拈來?
二、插值器與估值器
對於屬性動畫來說,一定是繞不開 插值器 與 估值器。接下來便一一道來
文章更多的使用 我在開發過程中 自我理解 的詞語,而儘量不使用 教科書式 或 直接翻譯註釋 語句。如果有 晦澀難懂 或是 理解錯誤之處,歡迎 評論區 留言,我們進行探討。
1、插值器(TimeInterpolator)
(1)定義
是一個控制動畫 進度 的工具。
為何如此說?我們們可以這麼理解,我們藉助 SpringInterpolator 插值器的函式圖來講解。
- x軸為 時間 。0表示還未開始,1表示時間結束。時間是 不可逆的 ,所以會一直往前推進,即 只會增加。
- y軸為 當前時間點,物體(我們的控制的動畫物件,例如View)距離目的地的進度。0表示進度還未走動,1表示進度已經走至目的地。但值得注意的是,到達目的地(A點)不代表動畫已經結束,可以 “衝破”(B點) 目的地,也可以 往回走(C點) ,結束與否是由時間軸決定。
這個插值器,在小盆友的另一篇博文中有使用到,效果如下圖,有興趣的童鞋可以進入傳送門瞭解。下面動態圖是小盆友專門為插值器更為直觀的展示而開發的小工具,如果你感興趣,可以把自己的插值器也放入這其中進行展示,方便以後開發時需要,程式碼傳送門。
(2)如何使用
Android 系統中已經幫我實現了一些比較常用插值器,這裡就不一一貼圖介紹器函式圖,需要的童鞋可以進入小盆友下面所提到的 插值器小工具 進行玩耍,這裡給一張工具的效果圖。
加入到屬性動畫中也很簡單,只需以下一行程式碼,便可輕鬆搞定// 將 插值器 設定進 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只是線性的將商品從起始點移到終止點,滿足不了產品大大的需求,如何自定義呢?請往下走
相信你也猜到了,估值器 也是通過實現一個介面,以下便是介面和引數描述對貝塞爾曲線感興趣的童鞋,可以檢視小盆友的另一片博文 自帶美感的貝塞爾曲線原理與實戰
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));
}
}
複製程式碼
購物車的動畫思路如下:
- 點選新增商品後,初始化一個 ShoppingView(繼承至ImageView),設定其大小和商品的大小一致,並新增至 decorView 中,這樣才能讓 ShoppingView 在整個檢視中移動。
- 通過 getLocationOnScreen 方法,獲取商品圖片在螢幕中的座標A 和 購物車在螢幕中的座標B(值得注意的是,這個座標是 包含狀態列的高度 ),將 ShoppingView 移至座標A,然後啟動動畫。
- 進行以 ShoppingView 正中心為原點進行 從0到0.8縮小,接著順時針傾斜35度,緊接著以 貝塞爾曲線 路徑從座標A移至座標B,控制點C的x軸為B點的x軸座標,y軸為A點的y軸座標;
- 動畫完成後,將 ShoppingView 從 其父檢視中移除,同時回撥偵聽介面,以便購物車可以進行動畫。
- 接到回撥後,購物車數量加一,同時更換購物車圖示,已經顯示小紅點並且播放其動畫,以小紅點正中心為原點進行縮放,縮放規則為從 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 的構造方法走完,我們先來小結一下,做了兩件事:
- 將 目標物件ZincView 以弱引用的形式儲存在 mTarget屬性 中
- 將 屬性方法名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 的構造方法。這裡需要先說明下這幾個類的繼承關係,後面也會用到,注意看清這幾個類和介面的名字。
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 的程式碼內容就走完了,我們再來小結一下
- PropertyValuesHolder 是一個用於裝載 關鍵幀集合 和 屬性動畫名 的資料模型。
- 關鍵幀會被一個個存放在 Keyframe 類中,既 有多少個關鍵幀 則 有多少個Keyframe。
- 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 插值器,該插值器的走勢如下動態圖。
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);
}
複製程式碼
值得注意
不知道童鞋們有沒有和小盆友一樣的錯覺,一直以為 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),需要時我們再折回來看看。
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方法中做了這幾件事:
- 初始化一些屬性,例如執行的狀態標記(注意此處 開始狀態mStarted便置為true);
- 幀和動畫的播放時間則置為-1;
- 新增動畫回撥,用於接受 垂直同步訊號;
- 設定當前的播放 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 類。
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 儲存在了 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 方法中,具體程式碼如下,這個方法做了如下幾個步驟:
- 初始化動畫,具體是設定關鍵幀集合的估值器;
- 將執行狀態置為true;
- 設定 mOverallFraction(這個屬性我們後面用到時在說明是什麼作用);
- 回撥 監聽器;
接下來我們分析第一和第四小點
// 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。
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 方法,具體程式碼如下。我們需要明確的是,傳進來的引數是當次的進度,也就是不含迴圈次數的。
我們會先進入到 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)
情況二:fraction = 1/4。進入FLAG(20)
情況三:fraction = 3/4。進入FLAG(20)
情況四:fraction = 1。進入FLAG(19)
經過上面四種情況,我們可以知道 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的地方,有兩個:
- 動畫時長為0;
- 最後一次迭代完畢,至於判斷是否完成最後一次迭代,則通過判斷當前進度是否已經大於我們迴圈的次數,並且動畫不是無限迴圈播放,判斷的程式碼可以看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、多維雷達圖
文章開頭出現的就是以下效果圖,現在我們來進行拆解實現。
效果圖
動畫分析
繪製相對應維度的雷達圖,在設定完資料後,進行設定屬性動畫,最後根據屬性動畫回撥值進行每個維度的展開。emmm,有些抽象。我們進行拆解為需要的零件:
- 每個頂點的座標;
- 維度展開的屬性動畫;
準備零件
(1)頂點座標 一圖勝千言,我們以六維雷達圖為例,以比較有代表性的A,B,C三點來計算其座標。但這裡面有一個前提是,需要將畫布的原點移至view的中心。接下來具體的計算請看圖,中間涉及到一些簡單的三角函式,這裡就不過多的說明。
根據圖片中的計算規則,我們可以得知以 畫布的負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、錶盤指示器
文章最開始出現的第二個就是以下這張效果圖,具體的操作其實和 “多維雷達圖” 沒有太多的出入,只是將維度的展開,變為 畫布的旋轉後繪製指標,達到指標旋轉的效果,再加上插值器的公式輔助,到達擺動迴盪的效果。限於文章篇幅過長這裡就不再具體闡述,有興趣的同學請入傳送門。
效果圖
五、寫在最後
屬性動畫是高階UI中又一把不可缺少的利器,使用得當,能讓整個介面富有互動感,提高使用者體驗。最後如果你從這篇文章有所收穫,請給我個贊❤️,並關注我吧。文章中如有理解錯誤或是晦澀難懂的語句,請評論區留言,我們進行討論共同進步。你的鼓勵是我前進的最大動力。
高階UI系列的Github地址:請進入傳送門,如果喜歡的話給我一個star吧?
如果需要更多的交流與探討,可以通過以下微信二維碼加小盆友好友。