終于在新的一年的第一天完成了本篇文章,小盆友在此祝賀您,萬事如意,阖家幸福。?
目錄
一、前言
二、插值器與估值器
三、源碼解析
四、實戰
五、寫在最後
一、前言
對于越來越追求豐富的互動體驗的用戶端,一個帶有動态效果的界面已經是不可避免。屬性動畫就是這其中必不可少的一把利器,是以今天便來分享下 屬性動畫融合在實際場景中 的使用,以及進行 源碼分析。話不多說,先看看今天的實戰效果圖,然後開始進行分享之旅。
1、多元雷達圖
2、表盤訓示器
3、活力四射的購物車
老規矩,代碼在實戰篇會給出,透過 API 看清源碼,各種效果便是順手拈來?
二、插值器與估值器
對于屬性動畫來說,一定是繞不開 插值器 與 估值器。接下來便一一道來
文章更多的使用 我在開發過程中 自我了解 的詞語,而盡量不使用 教科書式 或 直接翻譯注釋 語句。如果有 晦澀難懂 或是 了解錯誤之處,歡迎 評論區 留言,我們進行探讨。
1、插值器(TimeInterpolator)
(1)定義
是一個控制動畫 進度 的工具。
為何如此說?咱們可以這麼了解,我們借助 SpringInterpolator 插值器的函數圖來講解。
- x軸為 時間 。0表示還未開始,1表示時間結束。時間是 不可逆的 ,是以會一直往前推進,即 隻會增加。
- y軸為 目前時間點,物體(我們的控制的動畫對象,例如View)距離目的地的進度。0表示進度還未走動,1表示進度已經走至目的地。但值得注意的是,到達目的地(A點)不代表動畫已經結束,可以 “沖破”(B點) 目的地,也可以 往回走(C點) ,結束與否是由時間軸決定。
SpringInterpolator 的動态圖如下
下面動态圖是小盆友專門為插值器更為直覺的展示而開發的小工具,如果你感興趣,可以把自己的插值器也放入這其中進行展示,友善以後開發時需要,代碼傳送門。
這個插值器,在小盆友的另一篇博文中有使用到,效果如下圖,有興趣的童鞋可以進入傳送門了解。
(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提供的插值器 是一摸一樣的,如下所示
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 的 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 的構造方法。這裡需要先說明下這幾個類的繼承關系,後面也會用到,注意看清這幾個類和接口的名字。
進入 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、第四行代碼
第四行代碼用于設定估值器,進入源碼,這裡同樣也是進入到父類 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方法中做了這幾件事:
- 初始化一些屬性,例如運作的狀态标記(注意此處 開始狀态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 類。
// 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。
接下來進行分析第四小點,折回到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中又一把不可缺少的利器,使用得當,能讓整個界面富有互動感,提高使用者體驗。最後如果你從這篇文章有所收獲,請給我個贊❤️,并關注我吧。文章中如有了解錯誤或是晦澀難懂的語句,請評論區留言,我們進行讨論共同進步。你的鼓勵是我前進的最大動力。