本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布
這次想來講講 View.animate(),這是一種超好用的動畫實現方式,用這種方式來實現常用的動畫效果非常方便,但在某些場景下會有一個坑,所以這次就來梳理一下它的原理。
基礎
首先,先來看一段代碼:
mView.animate().sacleX(1.2f).scaleY(1.2f).alpha(0.5f).setDuration(1000).start();
可以有些人還沒接觸過這個,但並不妨礙首次理解上述代碼。單從方法名上來看,上述代碼就是一個實現了持續 1s 的放大 & 透明度結合的動畫,是不是發現使用起來特別簡單,一行代碼就搞定。
當然,上述的動畫效果也可以通過 ValueAnimator 或 ObjectAnimator 來實現,只是可能沒法像上述一樣一行代碼就搞定。如果用 Animation 來實現,那么需要的代碼就更多了。
所以,我們的問題就來了:
Q1:動畫基本可以分為 Animator 和 Animation 兩大類,而 View.animate() 返回的是一個 ViewPropertyAnimator 類型的對象,這個類並沒有繼承自任何類,那么它實現動畫的原理又是什么呢?單從命名上看好像是通過 Animator 實現,那么真的是這樣么?
Q2:開頭說了,使用這種方式實現的動畫在某些場景下會有一個坑,這個坑又是什么,是在什么場景下的呢?
好了,下面就開始來跟着源碼一起學習吧:
源碼解析
ps:本篇閱讀的源碼版本都是 android-25,版本不一樣,源碼可能會有些許差別,大伙自己過的時候注意一下。
那么,源碼閱讀的着手點就跟之前幾篇分析動畫的一樣,從 start() 開始一步步跟蹤下去就行了。
//ViewPropertyAnimator#start()
public void start() {
mView.removeCallbacks(mAnimationStarter);
startAnimation();
}
代碼很少就兩行,第二行是調用了一個方法,看方法名可以猜測應該是去處理動畫開始的工作,那么在動畫開始前還移除了一個回調,但要搞清楚第一行的代碼是干嘛用的,我們得先知道兩個變量的含義,首先是第一個 mView:
//ViewPropertyAnimator構造函數
ViewPropertyAnimator(View view) {
mView = view;
view.ensureTransformationInfo();
}
mView 是一個成員變量,在構造函數中被賦值,還記得吧,要用這種方式實現動畫時,都得先調用 View.animate() 來創造一個 ViewPropertyAnimator 對象,所以去 View 的 animate() 方法里瞧瞧:
//View#animate()
public ViewPropertyAnimator animate() {
if (mAnimator == null) {
mAnimator = new ViewPropertyAnimator(this);
}
return mAnimator;
}
這個方法里會去創建一個 ViewPropertyAnimator 對象,並將 View 自身 this 作為參數傳遞進去,也就是說,在 ViewPropertyAnimator 里的 mView 變量其實指向的就是要進行動畫的那個 View。
知道了 mView 其實就是需要進行動畫的那個 View 后,接下去來看看另一個變量 mAnimationStarter 是什么了:
//ViewPropertyAnimator.mAnimationnStarter
private Runnable mAnimationStarter = new Runnable() {
@Override
public void run() {
startAnimation();
}
};
這個 Runnable 就是一個啟動動畫的工作,emmm,這樣就有點奇怪了,我們再回過頭來看看 start() 方法:
//ViewPropertyAnimator#start()
public void start() {
mView.removeCallbacks(mAnimationStarter);
startAnimation();
}
為什么明明方法的第二行就會去執行 startAnimation() 了,第一行卻又要去取消一個執行 startAnimation() 的 Runnable 呢?
只能說明,在我們調用 start() 之前,ViewPropertyAnimator 內部就已經預先安排了一個會執行 startAnimation() 的 Runnable 進入待執行狀態,所以在調用了 start() 之后先去取消這個 Runnable 才會有意義。
那么,又是哪里會去觸發安排一個 Runnable 呢?
回頭再看看我們使用這種方式來實現動畫效果是怎么用的:
mView.animate().sacleX(1.2f).scaleY(1.2f).alpha(0.5f).setDuration(1000).start();
首先,通過 View.animate() 先創建一個 ViewPropertyAnimator 對象,中間設置了一系列動畫行為,最后才調用了 start()。那么,有機會去觸發安排一個待執行的 Runnable 操作也只能發生在中間的這些方法里了,那么我們選擇一個跟進去看看,scaleX():
//ViewPropertyAnimator#scaleX()
public ViewPropertyAnimator scaleX(float value) {
animateProperty(SCALE_X, value);
return this;
}
繼續跟進去看看:
//ViewPropertyAnimator#animateProperty()
private void animateProperty(int constantName, float toValue) {
float fromValue = getValue(constantName);
float deltaValue = toValue - fromValue;
animatePropertyBy(constantName, fromValue, deltaValue);
}
至於各個參數是什么意思,我們后面再來分析,目前我們是想驗證是不是這些封裝好的動畫接口內部會去觸發一個待執行的 Runnable 操作,所以優先繼續跟蹤下去:
//ViewPropertyAnimator#animatePropertyBy()
private void animatePropertyBy(int constantName, float startValue, float byValue){
...
mView.removeCallbacks(mAnimationStarter);
mView.postOnAnimation(mAnimationStarter);
}
終於找到了,而且不僅僅是 scaleX() 方法,其他封裝好的動畫接口如 scaleY(),alpha(),translationX() 等等所有這一系列的方法內部最終都會走到 animatePropertyBy() 里去。而在這個方法最后都會先將待執行的 Runnable 先移除掉,再重新 post。
要理解這么做的用意,得先明白 View 的這兩個方法:removeCallbacks(),postOnAnimation() 是干嘛用的。這里我就不跟下去了,直接給大伙說下結論:
通過 postOnAnimation() 傳進去的 Runnable 並不會被馬上執行,而是要等到下一個屏幕刷新信號來的時候才會被取出來執行。
那么,將這些串起來,也就是說,僅僅只是 View.animate().scaleX() 這樣使用時,就算不主動調用 start() ,其實內部也會自動安排一個 Runnable,最遲在下一個屏幕刷新信號來的時候,就會自動去調用 startAnimation() 來啟動動畫。
但如果主動調用了 start(),內部就需要先將安排好的 Runnable 操作取消掉,然后直接調用 startAnimation() 來啟動動畫。
那么,接下去就來看看是如何啟動動畫的,startAnimation():
//ViewPropertyAnimator#startAnimation()
private void startAnimation() {
//1. 這里我還沒搞懂,也不清楚什么場景下會滿足這里的條件,直接 return。所以,本篇接下去的分析都是基於假設會直接跳過這里,后面如果搞懂了再來填坑。
if (mRTBackend != null && mRTBackend.startAnimation(this)) {
return;
}
...
//2. 創建一個 0.0-1.0 變化的 ValueAnimator
ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
//3. 將當前 mPengingAnimations 里保存的一系列動畫全都取出來,作為同一組一起執行一起結束的動畫
ArrayList<NameValuesHolder> nameValueList =
(ArrayList<NameValuesHolder>) mPendingAnimations.clone();
...
//4. 創建一個新的 PropertyBundle 來保存這一組動畫,以ValueAnimator作為key來區分
mAnimatorMap.put(animator, new PropertyBundle(propertyMask, nameValueList));
//5. 提供動畫開始前,結束后的操作回調
if (mPendingSetupAction != null) {
mAnimatorSetupMap.put(animator, mPendingSetupAction);
mPendingSetupAction = null;
}
...
//6. 對ValueAnimator進行 Listener、StartDelay、Duration、Interpolator 的設置
animator.addUpdateListener(mAnimatorEventListener);
animator.addListener(mAnimatorEventListener);
...
//7. 啟用ValueAnimator.start()
animator.start();
}
上述代碼可以先不用細看,我們稍后再來一塊一塊慢慢過,我已經將整個方法里做的事大概划分成了 7 件,首先有一點需要提一下,方法內其實是通過 ValueAnimator 來實現的。
上一篇博客屬性動畫 ValueAnimator 運行原理全解析中,我們已經將 ValueAnimator 的運行原理分析完了,感興趣的可以回去看看,這里大概提幾點結論:
ValueAnimator 內部其實並沒有進行任何 ui 操作,它只是提供了一種機制,可以根據設定的幾個數值,如 0-100,內部自己在每一幀內,根據當前時間,第一幀的時間,持續時長,以及插值器規則,估值器規則來計算出在當前幀內動畫的進度並映射到設定的數值區間,如 0-100 區間內映射之后的數值應該是多少。
既然 ValueAnimator 並沒有進行任何 ui 操作,那么要用它來實現動畫效果,只能自己在 ValueAnimator 提供的每一幀的回調里(AnimatorUpdateListener),自己取得 ValueAnimator 計算出的數值,來自行應用到需要進行動畫效果的那個 View 上。
想想自己使用 ValueAnimator 的時候是不是這樣,我們並沒有將 View 作為參數傳遞給 ValueAnimator,所以它內部也就沒有持有任何 View 的引用,自然做不了任何 ui 操作。
所以看看 startAnimation() 方法里的,我標出來的第 2、6、7點:
//ViewPropertyAnimator#startAnimation()
private void startAnimation() {
...
//2. 創建一個 0.0-1.0 變化的 ValueAnimator
ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
...
//6. 對ValueAnimator進行 Listener、StartDelay、Duration、Interpolator 的設置
animator.addUpdateListener(mAnimatorEventListener);
animator.addListener(mAnimatorEventListener);
...
//7. 啟用ValueAnimator.start()
animator.start();
}
所以,ViewPropertyAnimator 其實是通過 ValueAnimator.ofFloat(1.0f),也就是借助 ValueAnimator 的機制,來計算每一幀動畫進度在 0-1 內對應的數值。然后在它的每一幀的回調里再去進行 view 的 ui 操作來達到動畫效果,那么 ui 操作也就是在 mAnimatorEventListener 里做的事了,跟進去看看:
//ViewPropertyAnimator.mAnimatorEventListener
private AnimatorEventListener mAnimatorEventListener = new AnimatorEventListener();
private class AnimatorEventListener implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
...
@Override
public void onAnimationUpdate(ValueAnimator animation) {
...
//1. 取出 ValueAnimator 計算出的當前幀的動畫進度
float fraction = animation.getAnimatedFraction();
//2. 根據取得的動畫進度,進行一系列view的ui操作,來達到動畫效果
...
}
}
省略了絕大部分代碼,等會會再來慢慢過,這樣省略后比較容易梳理出整個流程,優先將流程梳理清楚,再來分析每個步驟具體干的活。
所以,可以看到,ViewPropertyAnimator 確實是在 ValueAnimator 的每一幀的回調中,取得 VauleAnimator 機制計算出來的動畫進度值,然后自行進行 ui 操作來達到動畫效果。
那么,到這里,整個流程就已經梳理出來了,我們先來梳理一下目前的信息:
-
通過
View.animate().scaleX(1.2f).start()實現的動畫,如果外部沒有手動調用start()方法,那么 ViewPropertyAnimator 內部最遲會在下一幀的時候自動調用startAnimation()來啟動動畫。 -
ViewPropertyAnimator 實現下一幀內自動啟動動畫是通過
View.postOnAnimation()實現,View 的這個方法會將傳遞進來的 Runnable 等到下一幀的時候再去執行。 -
如果外部手動調用了
start(),那么內部會先將第 2 步中安排的自動啟動動畫的 Runnable 取消掉,然后直接調用startAnimation()啟動動畫。 -
startAnimation()啟動動畫,實際上是借助 ValueAnimator 的機制,在onAnimationUpdate()里取得每一幀內的動畫進度時,再自行進行對應的 ui 操作來達到動畫效果。 -
ValueAnimator 只是會根據當前時間,動畫第一幀時間,持續時長,插值器規則,估值器規則等來計算每一幀內的當前動畫進度值,然后根據關鍵幀機制來映射到設定的范圍內的數值,最后通過每一幀的進度回調,供外部使用,它本身並沒有任何 ui 操作(詳情可看上一篇博客)。
好了,流程上已經梳理清理了,接下去就是細節問題了,ViewPropertyAnimator 取得了每一幀對應的動畫進度時又是如何進行的 ui 操作的呢?View.animate() 后面是支持一系列的動畫操作,如 scaleX(),alpha() 等一起執行的,那么內部又是如何區分,維護的呢?
我們還是按照流程來一步步詳細的分析,View.animate() 方式實現的動畫,流程上是設置動畫行為--啟動動畫--每一幀進度回調中進行ui操作。所以,下面就先看看第一個步驟,跟着 scaleX() 進去看看:
//ViewPropertyAnimator#scaleX()
public ViewPropertyAnimator scaleX(float value) {
//1. 第一個參數用於區分不同的動畫,第二個參數設定動畫最后一幀的值
animateProperty(SCALE_X, value);
return this;
}
//ViewPropertyAnimator#animateProperty()
private void animateProperty(int constantName, float toValue) {
//2. 第一步先取得該種動畫行為下的默認第一幀值,最后一幀值就是參數傳遞進來
float fromValue = getValue(constantName);
//3. 計算出動畫的變化數值
float deltaValue = toValue - fromValue;
animatePropertyBy(constantName, fromValue, deltaValue);
}
//ViewPropertyAnimator#getValue()
private float getValue(int propertyConstant) {
final RenderNode node = mView.mRenderNode;
switch (propertyConstant) {
...
//4. 直接通過 getScaleX() 取得當前 view 的默認屬性值
case SCALE_X:
return node.getScaleX();
...
}
return 0;
}
上述代碼作用,其實也就只是取得對應動畫行為下的第一幀的屬性值,然后根據設定的最后一幀屬性值來計算出動畫變化的數值,最終作為參數傳遞給 animatePropertyBy(),所以最關鍵的任務肯定在這個方法里,但要捋清楚這個方法里的代碼前,還需要先了解一些變量以及內部類的含義:
ViewPropertyAnimator 內部有兩個數據結構類 NameValuesHolder 和 PropertyBundle,都是用於存儲各種動畫信息的,除此之外,還有一系列成員變量的列表,如 mPendingAnimations,mAnimatorMap 等。要搞清楚這些的含義,還得先搞懂 View.animate() 是支持如何使用的。
這么說吧,還是拿開頭的示例代碼來說明:
mView.animate().scaleX(1.2f).scaleY(1.2f).alpha(0.5f).setDuration(1000).start();
ViewPropertyAnimator 亮點就是支持鏈式調用一系列不同的動畫一起執行,所以需要注意一點,一旦像上述那樣使用,那么設定的這一系列動畫就會是一起執行一起結束的。
那么,有可能存在這種場景:先設置了一系列動畫執行,如果在這一系列的動畫執行結束前,又通過 View.animate() 設置了另外一系列一起執行的動畫效果,那么這時就會有兩組動畫都在運行中,每組動畫都可能含有多種類型的動畫,所以內部就需要以每組為單位來保存信息,確保每組動畫可以互不干擾,這就是 PropertyBundle 這個類的作用了:
//ViewPropertyAnimator$PropertyBundle
private static class PropertyBundle {
int mPropertyMask;
ArrayList<NameValuesHolder> mNameValuesHolder;
...
}
這樣解釋完,再來看這個類,這樣理解兩個成員變量的含義就容易多了,首先 mNameValuesHolder 是一個 ArrayList 對象,顯然就是用來存儲這一組動畫里的那一系列不同類型的動畫;那具體存在列表里都有哪些類型的動畫呢,就是另一個成員變量 mPropertyMask 來進行標志了。
而列表里存的這一組動畫里的不同類型的動畫,所以 NamaValuesHolder 這個類的作用就是用於區分各種不同類型的動畫了:
//ViewPropertyAnimator$NameValuesHolder
static class NameValuesHolder {
int mNameConstant;
float mFromValue;
float mDeltaValue;
...
}
第一個成員變量 mNameConstant 就是用於區分不同類型的動畫,在 ViewPropertyAnimator 內部定義了一系列常用動畫的常量,mNameConstant 這個變量的取值就在這些常量中,如開頭出現 SCALE_X。而另外兩個變量表示的就是這種類型的動畫要進行變化的數值信息。
另外,ViewPropertyAnimator 支持設置一系列不同類型的動畫,那么它是以什么為依據來決定哪一系列的動畫作為第一組,哪一系列作為第二組呢?其實很簡單,就是以 startAnimation() 被調用為依據。那么,成員變量 mPendingAnimations 的作用也就出來了。
每一次調用 scaleX() 等等之類的方法時,都會創建一個 NameValuesHolder 對象來保存對應這種類型的動畫信息,然后保存在 mPendingAnimations 列表中。scaleY() 等這些方法不斷被調用,mPendingAnimations 就會保存越來越多的待執行的不同類型的動畫。而一旦 startAnimation() 方法被調用時,就會將當前 mPendingAnimations 列表里存的這一系列動畫作為同一組一起執行一起結束的動畫保存到一個新的 PropertyBundle 對象里。然后清空 mPendingAnimations,直到下一次 startAnimation() 被調用時,再次將 mPendingAnimations 中新保存的一系列動畫作為另外一組動畫保存到新的 PropertyBundle 中去。
那么,最后還需要有一個變量來保存並區分這一組一組的動畫,這就是 mAnimatorMap 變量的作用了。
private HashMap<Animator, PropertyBundle> mAnimatorMap = new HashMap<Animator, PropertyBundle>();
看一下定義,沒錯吧,PropertyBundle 保存的是一組動畫里一起開始一起結束的一系列動畫,所以 mAnimatorMap 是以 Animator 為 Key 區分每一組動畫的。
捋清楚了這些內部類和變量的作用,我們下面再來看之前分析的調用了 scaleX() 后,內部跟到了 animatePropertyBy(),那么我們繼續跟下去看看:
//ViewPropertyAnimator#animatePropertyBy()
private void animatePropertyBy(int constantName, float startValue, float byValue) {
//1. mAnimatorMap 存放着一組一組正在運行中的動畫
if (mAnimatorMap.size() > 0) {
Animator animatorToCancel = null;
Set<Animator> animatorSet = mAnimatorMap.keySet();
for (Animator runningAnim : animatorSet) {
// 2. bundle 保存着當前這一組動畫里的一系列正在運行中的不同類型的動畫
PropertyBundle bundle = mAnimatorMap.get(runningAnim);
if (bundle.cancel(constantName)) {
if (bundle.mPropertyMask == NONE) {
animatorToCancel = runningAnim;
break;
}
}
}
if (animatorToCancel != null) {
animatorToCancel.cancel();
}
}
// 3. 所以上述1 2步的工作就是要將當前constantName類型的動畫取消掉
//4. 創建一個 NameValuesHolder 對象用於保存當前constantName類型的動畫信息
NameValuesHolder nameValuePair = new NameValuesHolder(constantName, startValue, byValue);
//5. 將該類型的動畫信息保存到 mPendingAnimations 中
mPendingAnimations.add(nameValuePair);
//6. 安排一個自動開啟動畫的Runnable,最遲在下一幀觸發
mView.removeCallbacks(mAnimationStarter);
mView.postOnAnimation(mAnimationStarter);
}
上述代碼里從第 4-6 的步驟應該都清楚了吧,每次調用 scaleX() 之類的動畫,內部需要先創建一個 NameValuesHolder 對象來保存該類型的動畫行為(第4步),然后將該類型動畫添加到 mPendingAnimations 列表中(第5步)來作為組成一系列一起開始一起結束的動畫,最后會自動安排一個最遲在下一幀內自動啟動動畫的 Runnable(第6步)。
那么第 1-3 步又是干嘛的呢?
是這樣的,上面說過,可能會存在一組一組都在運行中的動畫,每一組都有一系列不同類型的動畫,那么就有可能出現同一種類型的動畫,比如 scaleX(),既在第一組里,又在第二組里。很顯然,ViewPropertyAnimator 里的所有動畫都是作用於同一個 View 上,而不同組的動畫又有可能同一時刻都在運行中,那么,一個 View 的同一種類型動畫有可能在同一時刻被執行兩次么?說得白一點,一個 View 的大小如果在同一幀內先放大 1.2 倍,同時又放大 1.5 倍,那這個 View 呈現出來的效果肯定特別錯亂。
所以,ViewPropertyAnimator 里所有的動畫,在同一時刻,同一類型的動畫只支持只有一個處於正在運行中的狀態,這也就是第 1-3 步的意義,它需要去遍歷當前每一組里的每一個動畫,如果類型跟當前設定的動畫類型一致,那么就將之前的動畫取消掉,以最近設定的這次為准。
好了,scaleX() 這些設定動畫的行為,內部實現的細節我們已經分析完了,下面就繼續看看下一個流程,啟動動畫里都干了啥,startAnimation() :
//ViewPropertyAnimator#startAnimation()
private void startAnimation() {
//1. 這里我還沒搞懂,也不清楚什么場景下會滿足這里的條件,直接 return。所以,本篇接下去的分析都是基於假設會直接跳過這里,后面如果搞懂了再來填坑。
if (mRTBackend != null && mRTBackend.startAnimation(this)) {
return;
}
mView.setHasTransientState(true);
//2. 創建一個 0.0-1.0 變化的 ValueAnimator
ValueAnimator animator = ValueAnimator.ofFloat(1.0f);
//3. 將當前 mPengingAnimations 里保存的一系列動畫全都取出來,作為同一組一起執行一起結束的動畫
ArrayList<NameValuesHolder> nameValueList =
(ArrayList<NameValuesHolder>) mPendingAnimations.clone();
mPendingAnimations.clear();
int propertyMask = 0;
int propertyCount = nameValueList.size();
//3.1 遍歷這一系列動畫,將這些動畫都有哪些類型的動畫標志出來
for (int i = 0; i < propertyCount; ++i) {
NameValuesHolder nameValuesHolder = nameValueList.get(i);
propertyMask |= nameValuesHolder.mNameConstant;
}
//4. 創建一個新的 PropertyBundle 來保存這一組動畫,以ValueAnimator作為key來區分
mAnimatorMap.put(animator, new PropertyBundle(propertyMask, nameValueList));
//5. 提供動畫開始前,結束后的操作回調
if (mPendingSetupAction != null) {
mAnimatorSetupMap.put(animator, mPendingSetupAction);
mPendingSetupAction = null;
}
if (mPendingCleanupAction != null) {
mAnimatorCleanupMap.put(animator, mPendingCleanupAction);
mPendingCleanupAction = null;
}
if (mPendingOnStartAction != null) {
mAnimatorOnStartMap.put(animator, mPendingOnStartAction);
mPendingOnStartAction = null;
}
if (mPendingOnEndAction != null) {
mAnimatorOnEndMap.put(animator, mPendingOnEndAction);
mPendingOnEndAction = null;
}
//6. 對ValueAnimator進行 Listener、StartDelay、Duration、Interpolator 的設置
animator.addUpdateListener(mAnimatorEventListener);
animator.addListener(mAnimatorEventListener);
if (mStartDelaySet) {
animator.setStartDelay(mStartDelay);
}
if (mDurationSet) {
animator.setDuration(mDuration);
}
if (mInterpolatorSet) {
animator.setInterpolator(mInterpolator);
}
//7. 啟用ValueAnimator.start()
animator.start();
}
第 1 步我還沒搞清楚,就先暫時跳過吧。
第 2-4 步就是我們上面有說過的,當 startAnimation() 被調用時,將當前保存在 mPendingAnimations 列表里所有的動畫都作為同一組一起開始一起結束的動畫,保存到一個新的 PropertyBundle 對象中,每一組動畫什么時候開始,結束,以及每一幀的進度都是借助 ValueAnimator 機制實現,所以每一組動畫就以不同的 ValueAnimator 對象作為 key 值保存到 mAnimatorMap 中相戶區分,獨立出來。
第 5 步是 ViewPropertyAnimator 支持的接口,都是供外部根據需要使用,比如 mPendingOnStartAction 就是表示會在這一組動畫開始的時候被執行,時機跟 onAnimationStart() 相同,外部使用的時候調用 withStartAction() 就可以了。那么為什么需要提供這樣的接口呢?
這是因為,如果我們想要在動畫開始或結束的時候做一些事,如果我們是這樣使用:
mView.animate().scaleX(1.2f)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
//do something
}
}).start();
沒錯,這樣寫的話,確實可以實現在動畫前去執行我們指定的工作。但這樣會有一個問題,因為 ViewPropertyAnimator 動畫是支持多組動畫同時進行中的,如果像上面這樣寫的話,那么每一組動畫在開始之前就都會去回調這個 onAnimationStart() 方法,去做相同的事。
如果我們只希望當前一組動畫去執行這些動畫開始前的工作,其他組動畫不用去執行,那么這時候就可以使用 withStartAction() 來實現。
這就是第 5 步的用意。
第 6-7 步也就是對 ValueAnimator 做各種配置,如持續時長,延遲開始時間,插值器等等,最后調用 ValueAnimator.start() 來啟動。
好,啟動動畫的具體的工作我們也分析完了,剩下最后一個流程了,在每一幀的回調中如何進行 ui 操作並且應用一系列的動畫。那么,最后就看看 AnimatorEventListener:
//ViewPropertyAnimator.mAnimatorEventListener
private class AnimatorEventListener implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
...
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//1. 取出跟當前 ValueAnimator 綁定的那一組動畫
PropertyBundle propertyBundle = mAnimatorMap.get(animation);
...
//省略一堆沒看懂的代碼,跟硬件加速有關
...
//2. 獲取 ValueAnimator 機制計算出的當前幀的動畫進度
float fraction = animation.getAnimatedFraction();
int propertyMask = propertyBundle.mPropertyMask;
...
//3. 遍歷這一組動畫里的所有動畫,分別根據不同類型的動畫進行不同的 ui 操作來實現動畫效果
ArrayList<NameValuesHolder> valueList = propertyBundle.mNameValuesHolder;
if (valueList != null) {
int count = valueList.size();
for (int i = 0; i < count; ++i) {
//3.1 取出第i個動畫
NameValuesHolder values = valueList.get(i);
//3.2 根據ValueAnimator計算的當前幀動畫進度,以及第i個動畫的第一幀的屬性值和變化的值來計算出當前幀時的屬性值是多少
float value = values.mFromValue + fraction * values.mDeltaValue;
//3.3 如果是 alpha 動畫,通過View的set方法來修改alpha值,否則調用setValue方法
if (values.mNameConstant == ALPHA) {
alphaHandled = mView.setAlphaNoInvalidation(value);
} else {
setValue(values.mNameConstant, value);
}
}
}
//省略alpha動畫的一些輔助處理
...
//4. 進度回調,通知外部
if (mUpdateListener != null) {
mUpdateListener.onAnimationUpdate(animation);
}
}
}
這個方法做的事也很明確了,上述代碼中的注釋大概也說完了。也就是說 ViewPropertyAnimator 動畫內部在 ValueAnimator 的每一幀回調中,取出跟 ValueAnimator 綁定的那一組動畫,以及當前幀的動畫進度,然后再遍歷當前組的所有動畫,分別計算出每個動畫當前幀的屬性值,如果不是 alpha 動畫的話,直接調用 setValue() 方法來進行 ui 操作達到動畫效果,如果是 alpha 動畫,則調用 view 的一個 set 方法來實現。
那么,下面再繼續看看 setValue() :
//ViewPropertyAnimator#setValue()
private void setValue(int propertyConstant, float value) {
final View.TransformationInfo info = mView.mTransformationInfo;
final RenderNode renderNode = mView.mRenderNode;
switch (propertyConstant) {
case TRANSLATION_X:
renderNode.setTranslationX(value);
break;
...
case SCALE_X:
renderNode.setScaleX(value);
break;
case SCALE_Y:
renderNode.setScaleY(value);
break;
...
}
}
省略了一堆類似的代碼,這個方法里,就全部都是根據不同類型的動畫,取得當前 View 的 mRenderNode 對象,然后分別調用相應的 setXXX 方法,如 SCALE_X 動畫,就調用 setScaleX() 方法來進行 ui 操作達到動畫效果。
以上,View.animate() 這種方式實現的動畫,也就是 ViewPropertyAnimator 動畫,的整個流程以及流程里每個步驟的工作,我們到此就全部梳理清楚了。
總結
最后,就來進行一下總結:
-
View.animate()這種方式實現的動畫其實是 ViewPropertyAnimator 動畫。 -
ViewPropertyAnimator 並不是一種動畫,它沒有繼承自 Animator 或者 Animation,它其實只是一個封裝類,將常用的動畫封裝起來,對外提供方便使用的接口,內部借助 ValueAnimator 機制。
-
ViewPropertyAnimator 動畫支持自動啟動動畫,如果外部沒有明確調用了
start(),那么內部會安排一個 Runnable 操作,最遲在下一幀內被執行,這個 Runnable 會去啟動動畫。 -
當然,如果外部手動調用了
start(),那么自動啟動動畫就沒意義了,內部會自己將其取消掉。 -
ViewPropertyAnimator 對外提供的使用動畫的接口非常方便,如
scaleX()表示 x 的縮放動畫,alpha()表示透明度動畫,而且支持鏈式調用。 -
由於支持鏈式調用,所以它支持一系列動畫一起開始,一起執行,一起結束。那么當這一系列動畫還沒執行完又重新發起了另一系列的動畫時,此時兩個系列動畫就需要分成兩組,每一組動畫互不干擾,可以同時執行。
-
但如果同一種類型的動畫,如 SCALE_X,在同一幀內分別在多組里都存在,如果都同時運行的話,View 的狀態會變得很錯亂,所以 ViewPropertyAnimator 規定,同一種類型的動畫在同一時刻只能有一個在運行。
-
也就是說,多組動畫可以處於並行狀態,但是它們內部的動畫是沒有交集的,如果有交集,比如 SCALE_X 動畫已經在運行中了,但是外部又新設置了一個新的 SCALE_X 動畫,那么之前的那個動畫就會被取消掉,新的 SCALE_X 動畫才會加入新的一組動畫中。
-
由於內部是借助 ValueAnimator 機制,所以在每一幀內都可以接收到回調,在回調中取得 ValueAnimator 計算出的當前幀的動畫進度。
-
取出當前幀的動畫進度后,就可以遍歷跟當前 ValueAnimator 綁定的那一組動畫里所有的動畫,分別根據每一個動畫保存的信息,來計算出當前幀這個動畫的屬性值,然后調用 View 的 mRenderNode 對象的 setXXX 方法來修改屬性值,達到動畫效果。
還有一些細節並沒有歸納到總結中,如果只看總結的小伙伴,有時間還是建議可以慢慢跟着本文過一遍。
遺留問題
Q1:開頭說了,使用這種方式實現的動畫在某些場景下會有一個坑,這個坑又是什么,是在什么場景下的呢?
開頭說過使用這種方式實現的動畫,在某些場景下會存在一些坑。本來以為這篇里也能順便說清楚,但單單只是原理梳理下來,篇幅就很長了,那么也當做遺留問題,留到之后的文章中來好好說下吧。可以先說下是什么坑:
如果當前界面有使用 RecyclerView 控件,然后又對它的 item 通過 View.animate() 方式實現了一些動畫效果,比如很常見的 Tv 應用的主頁,界面會有很多卡位,然后每個卡位獲得焦點時一般都需要放大的動畫,此時這個卡位就是 RecyclerView 的 item,放大動畫可以通過 View.animate() 方式來實現。
在這種場景下,可能會存在這么一種現象,當界面刷新時,如果此時有進行遙控器的方向鍵按鍵事件,那么可能會有一些卡位的縮放動畫被中斷的現象。為什么會出現這種現象,再找個時間來梳理清楚。
Q2:View 的 mRenderNode 對象又是個什么東西?它的 setXXX 方法又是如何修改 View 的屬性值來達到動畫效果的?
還有第二個遺留問題,雖然本篇梳理了 ViewPropertyAnimator 動畫的流程和原理,但到最后,我們其實只知道它內部借助了 ValueAnimator 機制來計算每一幀的動畫進度,然后在每一幀的回調中先獲取 View 的 mRenderNode 對象,再調用相應的 setXXX 方法來修改屬性值達到動畫效果。但這個 mRenderNode 是個什么東西,又是如何修改 view 的狀態來達到動畫效果的這點就還需要找個時間來梳理了。
所以到最后,ViewPropertyAnimator 內部的流程和原理雖然已經清楚了,但具體要不要將這個動畫歸納到屬性動畫中,我就不大清楚了。雖然它內部是借助了 ViewAnimator 機制,但 ValueAnimator 其實並沒有任何的 ui 操作,ObjectAnimator 才會去通過反射來調用相關的 setXXX 方法來修改屬性值,這個過程才是 ui 操作,最后才會有相應的動畫效果呈現出來。這點還有待繼續研究。

最近(2018-03)剛開通了公眾號,想激勵自己堅持寫作下去,初期主要分享原創的Android或Android-Tv方面的小知識,准備可能還有點不足,感興趣的可以先點一波關注,謝謝支持~~
