Android 動畫其實也可以通過 View.animate() 來實作常用的動畫效果,而且使用非常友善,一行代碼搞定。但它的内部原理是什麼,你清楚麼,來跟着學習一下吧
本篇文章已授權微信公衆号 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()
方法,那麼 ViewPropertyAnimator 内部最遲會在下一幀的時候自動調用start()
startAnimation()
- ViewPropertyAnimator 實作下一幀内自動啟動動畫是通過
實作,View 的這個方法會将傳遞進來的 Runnable 等到下一幀的時候再去執行。View.postOnAnimation()
- 如果外部手動調用了
,那麼内部會先将第 2 步中安排的自動啟動動畫的 Runnable 取消掉,然後直接調用start()
啟動動畫。startAnimation()
-
啟動動畫,實際上是借助 ValueAnimator 的機制,在startAnimation()
裡取得每一幀内的動畫進度時,再自行進行對應的 ui 操作來達到動畫效果。onAnimationUpdate()
- 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 動畫,的整個流程以及流程裡每個步驟的工作,我們到此就全部梳理清楚了。
總結
最後,就來進行一下總結:
-
這種方式實作的動畫其實是 ViewPropertyAnimator 動畫。View.animate()
- ViewPropertyAnimator 并不是一種動畫,它沒有繼承自 Animator 或者 Animation,它其實隻是一個封裝類,将常用的動畫封裝起來,對外提供友善使用的接口,内部借助 ValueAnimator 機制。
- ViewPropertyAnimator 動畫支援自動啟動動畫,如果外部沒有明确調用了
,那麼内部會安排一個 Runnable 操作,最遲在下一幀内被執行,這個 Runnable 會去啟動動畫。start()
- 當然,如果外部手動調用了
,那麼自動啟動動畫就沒意義了,内部會自己将其取消掉。start()
- ViewPropertyAnimator 對外提供的使用動畫的接口非常友善,如
表示 x 的縮放動畫,scaleX()
表示透明度動畫,而且支援鍊式調用。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方面的小知識,準備可能還有點不足,感興趣的可以先點一波關注,謝謝支援~~