補間動畫源碼中分析機制原理
前言
補間動畫移動后,點擊事件的響應為什么還在原來的位置?
那今天我們就來從源碼解析原理
一、補間動畫
補間動畫可以在一個視圖容器內執行一系列簡單變換(具體的變換步驟有:位置、大小、旋轉、透明度);
我們可以通過平移、旋轉、縮放、透明度等API進行具體的操作;
補間動畫的實現方式可以通過 XML或通過Android代碼兩種方式 去定義;
1、xml方式實現
文件名:animator_translate.xml
- <?xml version="1.0" encoding="utf-8"?>
- <translate
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:fromXDelta="0"
- android:fromYDelta="0"
- android:toYDelta="0"
- android:toXDelta="200"
- android:duration="500"
- android:fillAfter="true">
- </translate>
代碼加載xml文件獲取動畫
- //加載動畫
- Animation animation = AnimationUtils.loadAnimation(this, R.anim.animator_translate);
- //執行動畫
- testBtn.startAnimation(animation);
2、代碼方式實現
- TranslateAnimation translateAnimation = new TranslateAnimation(0,200,0,0);
- translateAnimation.setDuration(500);//動畫執行時間
- translateAnimation.setFillAfter(true);//動畫執行完成后保持狀態
- //執行動畫
- testBtn.startAnimation(translateAnimation);
二、補間動畫原理解
1、startAnimation
startAnimation(rotateAnimation)方法進入源碼;
- //View.java
- public void startAnimation(Animation animation) {
- animation.setStartTime(Animation.START_ON_FIRST_FRAME);
- setAnimation(animation);
- invalidateParentCaches();
- invalidate(true);
- }
首先是通過setStartTime()設置了動畫的開始時間;
- //View.java
- public void setStartTime(long startTimeMillis) {
- mStartTime = startTimeMillis;
- mStarted = mEnded = false;
- mCycleFlip = false;
- mRepeated = 0;
- mMore = true;
- }
這里只是對一些變量進行賦值,再來看看下一個方法;
設置動畫setAnimation(animation):
- //View.java
- public void setAnimation(Animation animation) {
- mCurrentAnimation = animation;
- if (animation != null) {
- if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
- && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
- animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
- }
- animation.reset();
- }
- }
這里面也是將動畫實例賦值給當前的成員變量;
分析startAnimation()方法里的invalidateParentCaches();
- //View.java
- protected void invalidateParentCaches()
- if (mParent instanceof View) {
- ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
- }
- }
可以看到這里僅僅是設置動畫標記,在視圖構建或者屬性改變時是必要的;
再回到startAnimation()方法里面invalidate(true);
2、invalidate
- //View.java
- public void invalidate(boolean invalidateCache) {
- invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
- }
- void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
- boolean fullInvalidate) {
- if (mGhostView != null) {
- mGhostView.invalidate(true);
- return;
- }
- .................
- // Propagate the damage rectangle to the parent view.
- final AttachInfo ai = mAttachInfo;
- final ViewParent p = mParent;
- if (p != null && ai != null && l < r && t < b) {
- final Rect damage = ai.mTmpInvalRect;
- damage.set(l, t, r, b);
- p.invalidateChild(this, damage);
- }
- }
- }
這里著重看p.invalidateChild(this, damage);
- //ViewGroup.java
- @Deprecated
- @Override
- public final void invalidateChild(View child, final Rect dirty) {
- final AttachInfo attachInfo = mAttachInfo;
- if (attachInfo != null && attachInfo.mHardwareAccelerated) {
- // HW accelerated fast path
- onDescendantInvalidated(child, child);
- return;
- }
- ViewParent parent = this;
- .........
- do {
- View view = null;
- if (parent instanceof View) {
- view = (View) parent;
- }
- .........
- parent = parent.invalidateChildInParent(location, dirty);
- } while (parent != null);
- }
- }
- 因為ViewParent p = mParent,this是View的子類ViewGroup;
- 所以p.invalidateChild(this, damage)里面其實是調用了ViewGroup的invalidateChild();
- 這里有一個do{}while()循環,第一次的時候parent = this即ViewGroup,然后調用parent.invalidateChildInParent(location, dirty)方法,當parent == null的時候結束循環;
- invalidateChildInParent方法中,只要條件成立就會返回mParent;
- //ViewGroup.java
- @Deprecated
- @Override
- public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
- if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
- .......
- return mParent;
- }
- return null;
- }
- ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0)是保持成立的,所以會一直返回mParent,那么說明View的mParent是ViewGroup;
- ViewGroup的mParent也是ViewGroup,而do{}while()循環一直找mParent,而一個View最頂端的mParent是ViewRootImpl,所以最后走到ViewRootImpl的invalidateChildInParent()里面;
- 在onCreate()方法里面通過setContentView()將布局添加到以DecorView為根布局的一個ViewGroup里面,因為在onResume()執行完成后,WindowManager會執行addView()方法,然后會創建一個ViewRootImpl對象,與DecorView綁定起來,DecorView的mParent設置成ViewRootImpl,ViewRootImpl實現了ViewParent接口,所以ViewRootImpl雖然沒有繼承View或者ViewGroup;
- ViewRootImpl的invalidateChildInParent()方法中;
- //ViewRootImpl.java
- @Override
- public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
- checkThread();
- if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
- if (dirty == null) {
- invalidate();
- return null;
- } else if (dirty.isEmpty() && !mIsAnimating) {
- return null;
- }
- .......
- invalidateRectOnScreen(dirty);
- return null;
- }
這里所有的返回值都變為null了,之前執行的do{}while()循壞也會停止。
3、scheduleTraversals()
- 接著分析invalidateRectOnScreen(dirty)方法;
- 進入 scheduleTraversals()方法;
- //ViewRootImpl.java
- private void invalidateRectOnScreen(Rect dirty) {
- ......
- if (!mWillDrawSoon && (intersected || mIsAnimating)) {
- scheduleTraversals();
- }
- }
- //ViewRootImpl.java
- void scheduleTraversals() {
- if (!mTraversalScheduled) {
- mTraversalScheduled = true;
- mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
- mChoreographer.postCallback(
- Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
- if (!mUnbufferedInputDispatch) {
- scheduleConsumeBatchedInput();
- }
- notifyRendererOfFramePending();
- pokeDrawLockIfNeeded();
- }
- }
主要看mTraversalRunnable,我們找到mTraversalRunnable這個類;
- //ViewRootImpl.java
- final class TraversalRunnable implements Runnable {
- @Override
- public void run() {
- doTraversal();
- }
- }
- final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
4、doTraversal()
doTraversal()方法;
- //ViewRootImpl.java
- void doTraversal() {
- .......
- performTraversals();
- .......
- }
- }
- scheduleTraversals()是將 performTraversals()放到一個Runnable里面;
- 在Choreographer的帶執行對列里面,這些待執行的Runable會在最近的一個16.6ms屏幕刷新信號到來的時候執行;
- 而performTraversals()是View的三大操作:測量、布局、繪制的發起者;
- 在Android屏幕刷新機制里,View樹里面不管哪個View發起的繪制請求或者布局請求都會走到ViewRootImpl的scheduleTraversals()里面,然后在最新的一個屏幕刷新信號來到時,再通過ViewRootImpl的performTraversals()從根布局DecorView依次遍歷View樹去執行測量、布局、繪制三大操作
- 每一次的刷新都會走到ViewRootImpl里面,然后在層層遍歷到發生改變的VIew里去執行相應的布局和繪制操作;
- 所以在調用View.startAnimation(rotateAnimation)后,并沒有立即執行動畫,而是做了一下變量初始化操作,將View和Animation綁定起來,調用重繪操作,內部層層尋找mPartent,最終在ViewRootImpl的scheduleTraversals()發起一個遍歷View樹的請求,在最近一個屏幕信號刷新到來時執行這個請求,調用performTraversals()從根布局去遍歷View樹。
5、draw
- 我們繼續分析draw方法是怎樣將動畫繪制的且動畫是怎樣動起來的呢?
- invalidate最終會調用到ViewRootImpl 注冊Choreographer的回調;
- 在下一次VSYN信號到來時 會調用ViewRootImpl performTraversals 最終會調用到View的draw方法。
- boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
- ...
- //清除上次動畫保存的Transformation
- if ((parentFlags & ViewGroup.FLAG_CLEAR_TRANSFORMATION) != 0) {
- parent.getChildTransformation().clear();
- parent.mGroupFlags &= ~ViewGroup.FLAG_CLEAR_TRANSFORMATION;
- }
- ......
- final Animation a = getAnimation();
- if (a != null) {
- //根據當前時間計算當前幀的動畫,more表示是否需要執行更多幀的動畫
- more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
- concatMatrix = a.willChangeTransformationMatrix();
- if (concatMatrix) {
- mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
- }
- //拿到當前幀需要的變換 ,這個值會在applyLegacyAnimation中進行設置
- transformToApply = parent.getChildTransformation();
- }
- ....
- if (transformToApply != null) {
- if (concatMatrix) {
- if (drawingWithRenderNode) {
- renderNode.setAnimationMatrix(transformToApply.getMatrix());
- } else {
- // Undo the scroll translation, apply the transformation matrix,
- // then redo the scroll translate to get the correct result.
- canvas.translate(-transX, -transY);
- canvas.concat(transformToApply.getMatrix());//在這里調用canvas的concat方法,實現最終的平移效果 (做矩陣相乘)
- canvas.translate(transX, transY);
- }
- //標記需要清除Tranformation
- parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
- }
- float transformAlpha = transformToApply.getAlpha();
- if (transformAlpha < 1) {
- alpha *= transformAlpha;
- parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
- }
- }
- ...
- }
- 調用applyLegacyAnimation根據當前時間來計算當前幀的動畫同時返回值表示動畫是否還沒播放完成;
- 拿到當前幀需要的變換transformToApply;
- 調用canvas.concat 方法 實現最終的平移效果 (做矩陣相乘) 這樣我們就將最終的平移想過畫到canvas上面了解決了如何完成繪制的問題;
- 接下來繼續分析applyLegacyAnimation是如何計算當前的幀的動畫的。
6、applyLegacyAnimation
- private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
- Animation a, boolean scalingRequired) {
- ...
- //獲取Transformation 每個ViewGroup中的子View共同使用一個Transformation 為了多個View有動畫時頻繁創建多個Transformation
- //這個和在draw方法中取出的transformToApply是一個對象 就是最終應用到Canvas上的Transform
- final Transformation t = parent.getChildTransformation();
- //調用Animation的getTransformation方法來根據當前時間計算Transformation 這個對象的值最終會由getTransformation方法中進行賦值
- boolean more = a.getTransformation(drawingTime, t, 1f);
- invalidationTransform = t;
- ...
- //如果動畫還沒有播放完成 需要讓動畫循環起來 實際上是繼續調用invalidate
- if (more) {
- if (parent.mInvalidateRegion == null) {
- parent.mInvalidateRegion = new RectF();
- }
- final RectF region = parent.mInvalidateRegion;
- //調用Animation 的getInvalidateRegion來根據invalidationTransform計算 parent的invalidateRegion
- a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
- invalidationTransform);
- // The child need to draw an animation, potentially offscreen, so
- // make sure we do not cancel invalidate requests
- parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
- final int left = mLeft + (int) region.left;
- final int top = mTop + (int) region.top;
- //調用invalidate執行下一次繪制請求,這樣動畫就動起來了
- parent.invalidate(left, top, left + (int) (region.width() + .5f),
- top + (int) (region.height() + .5f));
- }
- }
- 在applyLegacyAnimation方法中會再次調用parent.invalidate注冊一個Choreographer回調,下一次VSYN后又會調用draw方法.這樣就循環起來了;
- 只有當more為false時 表示動畫播放完成了 這時候就不會invalidate了;
- 繼續看getTransformation是如何計算Transformation的。
- //Animation.java
- //返回值表示動畫是否沒有播放完成 并且需要計算outTransformation 也就是動畫需要做的變化
- public boolean getTransformation(long currentTime, Transformation outTransformation) {
- if (mStartTime == -1) {
- mStartTime = currentTime;//記錄第一幀的時間
- }
- if (duration != 0) {
- normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) / //計算運行的進度(0-1) (當前時間-開始時間+偏移量)/動畫總時長
- (float) duration;
- }
- final boolean expired = normalizedTime >= 1.0f || isCanceled(); //判斷動畫是否播放完成 或者被取消
- mMore = !expired;
- if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f); //處理最大值
- final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);//根據插值器計算的當前動畫運行進度
- applyTransformation(interpolatedTime, outTransformation);//根據動畫進度 計算最終的outTransformation
- return mMore;
- }
- 記錄動畫第一幀的時間;
- 根據當前時間到動畫第一幀的時間這之間的時長和動畫應持續的時長來計算動畫的進度;
- 把動畫進度控制在 0-1 之間,超過 1 的表示動畫已經結束,重新賦值為 1 即可;
- 根據插值器來計算動畫的實際進度;
- 調用 applyTransformation() 應用動畫效果。
- //applyTransformation每種類型的動畫都有自己的實現 這里以位移動畫為例
- //TranslateAnimation.java
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- //Transformation可以理解成 存儲View的一些變換信息,將變化信息保存到成員變量matrix中
- float dx = mFromXDelta;
- float dy = mFromYDelta;
- if (mFromXDelta != mToXDelta) {
- dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);//計算X方向需要移動的距離
- }
- if (mFromYDelta != mToYDelta) {
- dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);//計算Y方向需要移動的距離
- }
- t.getMatrix().setTranslate(dx, dy); //將最終的結果設置到Matrix上面去
- }
- 至此計算完最終的變化然后應用到了Transformation的Matix上,會在draw方法中拿到該Transformation并應用到Canvas上,調用canvas.concat,進行平移動畫;
- 當動畫如果還沒執行完,就會再調用 invalidate() 方法,層層通知到 ViewRootImpl 再次發起一次遍歷請求,當下一幀屏幕刷新信號來的時候;
- 再通過 performTraversals() 遍歷 View 樹繪制時,該 View 的 draw 收到通知被調用時;
- 會再次去調用 applyLegacyAnimation() 方法去執行動畫相關操作,包括調用 getTransformation() 計算動畫進度,調用 applyTransformation() 應用動畫。
7、動畫總結
- 當調用View.startAnimation(Animation)時,并沒有立即執行動畫,而是通過invalidate()層層通過到ViewRootImpl發起一次遍歷View樹的請求,在接收到下一個(16ms)屏幕信號刷新時才發起遍歷View樹的繪制操作,從DecorView開始遍歷,繪制時會調用draw()方法,如果View有綁定動畫則執行applyLegacyAnimation()方法處理相關動畫邏輯;
- 在applyLegacyAnimation()里面,先執行初始化initialize(),再通知動畫開始onAnimationStart(),然后通過getTransformation()計算動畫進度,并且它的返回值和動畫是否結束決定是否繼續通知ViewRootImpl發起遍歷請求,view樹繪制,如此重復這個步驟,并且調用applyTransformation()方法執行動畫的邏輯,直到動畫結束。
總結
至于未來會怎樣,要走下去才知道,反正路還很長,天總會亮;
加油老鐵們!
本文轉載自微信公眾號「Android開發編程」