終於在新的一年的第一天完成了本篇文章,小盆友在此祝賀您,萬事如意,闔家幸福。😄java
目錄android
1、前言git
2、插值器與估值器程序員
3、源碼解析github
4、實戰canvas
5、寫在最後設計模式
對於愈來愈追求豐富的交互體驗的客戶端,一個帶有動態效果的界面已是不可避免。屬性動畫就是這其中必不可少的一把利器,因此今天便來分享下 屬性動畫融合在實際場景中 的使用,以及進行 源碼分析。話很少說,先看看今天的實戰效果圖,而後開始進行分享之旅。數組
一、多維雷達圖 瀏覽器
二、錶盤指示器 微信
三、活力四射的購物車老規矩,代碼在實戰篇會給出,透過 API 看清源碼,各類效果即是順手拈來😄
對於屬性動畫來講,必定是繞不開 插值器 與 估值器。接下來便一一道來
文章更多的使用 我在開發過程當中 自我理解 的詞語,而儘可能不使用 教科書式 或 直接翻譯註釋 語句。若是有 晦澀難懂 或是 理解錯誤之處,歡迎 評論區 留言,咱們進行探討。
是一個控制動畫 進度 的工具。
爲什麼如此說?我們能夠這麼理解,咱們藉助 SpringInterpolator 插值器的函數圖來說解。
這個插值器,在小盆友的另外一篇博文中有使用到,效果以下圖,有興趣的童鞋能夠進入 傳送門瞭解。下面動態圖是小盆友專門爲插值器更爲直觀的展現而開發的小工具,若是你感興趣,能夠把本身的插值器也放入這其中進行展現,方便之後開發時須要,代碼傳送門。
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());
複製代碼
將 插值器 中的 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 系統中提供了一些 經常使用的估值器
然鵝,在某些狀況下(產品大大搞事),咱們須要實現一個相似以下的添加到購物車的效果,商品是以 貝塞爾曲線 的路徑 「投到」 購物車中的,這時咱們就須要 自定義估值器,由於 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));
}
}
複製代碼
購物車的動畫思路以下:
若是對這一動畫感興趣,能夠查看具體代碼,請進入傳送門。
第二種狀況,大多數狀況下,咱們不會進行設置估值器,由於源碼中已經幫咱們作了這一步的轉換。因此當咱們沒有設置時,系統會以如下的公式進行轉換(咱們這裏以 浮點數 爲具體場景)
// 這段代碼在 FloatKeyframeSet 的 getFloatValue 方法中
prevValue + intervalFraction * (nextValue - prevValue);
複製代碼
這裏再賣個關子,公式的各個參數的意義先不給出,在源碼解析一節中一塊兒講解。
值得注意的是,若是屬性動畫中須要使用的是本身定義的類型,則必需要使用第一種狀況自行定義估值器,不然會crash。
如何使用,在上一小節其實已經給出,這裏給一個完整的代碼
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));
}
}
複製代碼
進入源碼解析前,有必要先跟各位同窗 確認一件事 和 創建一個場景,不然源碼解析過程會 沒有目標 ,而迷路。
你已經會使用屬性動畫,會的意思是你已經能在 不借助文檔 或是 「借鑑」別人的代碼 的狀況下,寫出一個屬性動畫,並讓他按照你所須要的效果 正常的運行 起來,效果難易程度不限定。
源碼的閱讀在一個具體的場景中更容易理解,雖然會稍微片面些,可是漏掉的情景在懂得了一個場景後,後續使用過程當中便會慢慢的補充,並且主線已懂,支線也就不難了。話很少說,咱們來看看這個使用場景。
咱們針對 ZincView 的 setZinc 方法進行值變更:
/** * @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,接下來就開始咱們的每行分析。
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 的構造方法走完,咱們先來小結一下,作了兩件事:
看完構造方法中的祕密,回到 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 的代碼內容就走完了,咱們再來小結一下
咱們須要再折到 FLAG(3),進行查看 setValues 這一句是如何保存剛剛建立的 PropertyValuesHolder 對象。進入setValues,能夠看到下面👇這段代碼,
/** * 將 PropertyValuesHolder組 進行保存。分別存於: * 一、mValues ---------- PropertyValuesHolder組 * 二、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;
}
複製代碼
至此,第一行代碼就已經解析完畢,主要是進行 目標對象,屬性方法名 和 關鍵幀 的包裝和保存。已經在每小段代碼後進行小結,這裏就再也不作冗餘操做。
// 時長
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毫秒。
// 插值器
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 插值器,該插值器的走勢以下動態圖。
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。
// 更新回調
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(下一小節講)各自只能設置一個,若是屢次設置是會覆蓋。看了源碼才得知是有序列表持有。只怪本身以前太單純😂。
// 生命週期監聽器
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);
}
複製代碼
// 開啓
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方法中作了這幾件事:
// 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 方法中,具體代碼以下,這個方法作了以下幾個步驟:
接下來咱們分析第一和第四小點
// 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的地方,有兩個:
若是 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();
}
複製代碼
至此,屬性動畫的源碼分析便完成了。
文章開頭出現的就是如下效果圖,如今咱們來進行拆解實現。
繪製相對應維度的雷達圖,在設置完數據後,進行設置屬性動畫,最後根據屬性動畫回調值進行每一個維度的展開。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;
複製代碼
組裝零件
零件都已經備好了,組裝起來就是咱們看到的效果。由於代碼稍微較長,但主要點咱們已經攻破了,而且代碼註釋也比較多,這裏就再也不貼出來了,須要的請進傳送門。
文章最開始出現的第二個就是如下這張效果圖,具體的操做其實和 「多維雷達圖」 沒有太多的出入,只是將維度的展開,變爲 畫布的旋轉後繪製指針,達到指針旋轉的效果,再加上插值器的公式輔助,到達擺動迴盪的效果。限於文章篇幅過長這裏就再也不具體闡述,有興趣的同窗請入傳送門。
屬性動畫是高級UI中又一把不可缺乏的利器,使用得當,能讓整個界面富有交互感,提升用戶體驗。最後若是你從這篇文章有所收穫,請給我個贊❤️,並關注我吧。文章中若有理解錯誤或是晦澀難懂的語句,請評論區留言,咱們進行討論共同進步。你的鼓勵是我前進的最大動力。
高級UI系列的Github地址:請進入傳送門,若是喜歡的話給我一個star吧😄
若是須要更多的交流與探討,能夠經過如下微信二維碼加小盆友好友。