Android動畫篇(七)—— 補間動畫(Tween Animation)的運行原理

前言:不要在努力拼搏的年紀去選擇安逸享樂,爲了不讓生活留下遺憾和後悔,我們要抓住一切改變生活的機會,生命不止,奮鬥不息。

一、概述

在前面的文章中我們詳細講解了補間動畫(Tween Animation)的使用,系統爲我們封裝了幾個基本的動畫,也就是ScaleAnimation(縮放)、AlphaAnimation(透明)、RotateAnimation(旋轉)、TranslateAnimation(平移),它們都是繼承了Animation類,然後實現了applyTransformation()方法,然後通過Transformation轉換類和Matrix矩形類實現了各種各樣的動畫效果。

那麼補間動畫執行的原理是怎麼樣的呢?我們從源碼的角度查看這個問題,查看源碼我們帶着問題去看去查找,避免大海撈針被轉暈。我們先來看看動畫的使用,這裏以RotateAnimation(旋轉)爲例:

//創建RotateAnimation實例
RotateAnimation rotateAnimation = new RotateAnimation(0f, -650f,
                        Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
//設置動畫時間2000毫秒
rotateAnimation.setDuration(2000);
//設置保持動畫結束時的狀態
rotateAnimation.setFillAfter(true);
//控件綁定動畫
mTextView.startAnimation(rotateAnimation);

效果如下:

這裏圍繞自身旋轉逆時針方向650度,從代碼看到是通過mTextView.startAnimation(rotateAnimation)將動畫實例通過參數傳給控件,然後控件就可以動畫了。

二、動畫執行源碼分析

1、View樹遍歷(動畫執行前)

我們順藤摸瓜點擊mTextView.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)

    //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的時候結束循環。

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {}方法中,只要條件成立就會返回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,但是DecorView的mParent。我們找到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()循壞也會停止,我們點擊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();

我們打開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裏去執行相應的佈局和繪製操作。

所以在mTextView.startAnimation(rotateAnimation)源碼分析的這個過程中,執行動畫會內部會調用View的重繪操作invalidate(),最終走到ViewRootImpl的scheduleTraversals(),在下一個屏幕刷新信號到來的時候遍歷View樹刷新屏幕。得出結論:

在調用View.startAnimation(rotateAnimation)後,並沒有立即執行動畫,而是做了一下變量初始化操作,將View和Animation綁定起來,調用重繪操作,內部層層尋找mPartent,最終在ViewRootImpl的scheduleTraversals()發起一個遍歷View樹的請求,在最近一個屏幕信號刷新到來時執行這個請求,調用performTraversals()從根佈局去遍歷View樹。

2、動畫的執行

那麼真正動畫的執行是在ViewRootImpl發起的遍歷View樹的過程中,View顯示在屏幕的過程中的測量、佈局、繪製都是有ViewRootImpl的performTraversals()控制,這個View樹的三個操作,只能通過層層遍歷,所以基本操作的執行都會是一次遍歷股過程。

 //View.java
 public void draw(Canvas canvas) {
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
}

View遍歷的實現是在View#Draw()方法裏面執行的,這個方法大體上就做了六件事,當前View需要繪製,就會去調用自己的onDraw()方法,如果有子View就會去調用dispatchDraw()將繪製事件通知子View,ViewGroup重寫了dispatchDraw(),調用了drawChild(),然後drawChild()調用子View的draw(Canvas, ViewGroup, long),而這個方法又會調用到draw(Canvas)方法,所以達到了遍歷的效果。在測量和佈局的方法中都沒有動畫的相關代碼,但是在draw(Canvas, ViewGroup, long)發現了動畫的相關代碼:

    //View.java
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
        ......
        boolean more = false;
        ......
        Transformation transformToApply = null;
        ......
        final Animation a = getAnimation();
        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
      ......
     return more;
    }

這裏的getAnimation()其實就是上面View.startAnimation(Animation)時傳進來的成員變量,

    //View.java
    public Animation getAnimation() {
        return mCurrentAnimation;
    }

我們點進去applyLegacyAnimation()方法中,

    //View.java
    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }

        final Transformation t = parent.getChildTransformation();
        boolean more = a.getTransformation(drawingTime, t, 1f);
        if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
            if (parent.mInvalidationTransformation == null) {
                parent.mInvalidationTransformation = new Transformation();
            }
            invalidationTransform = parent.mInvalidationTransformation;
            a.getTransformation(drawingTime, invalidationTransform, 1f);
        } else {
            invalidationTransform = t;
        }

       .....
        return more;
    }

從上面看到a.initialize()onAnimationStart()有動畫的初始化和開始,我們跟着getTransformation()方法看到動畫的核心就在這裏,

View.java

這個方法主要做了記錄動畫第一幀的時間,計算動畫進度值,控制動畫進度在0-1之間,根據插值器計算動畫的實際進度(可以參考《Android動畫篇(四)—— 屬性動畫ValueAnimator的高級進階》),最後纔在applyTransformation()執行動畫的具體邏輯,在子類的具體實現相應的動畫的邏輯。我們來看看ScaleAnimation的applyTransformation()具體做了什麼。

ScaleAnimation.java

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float sx = 1.0f;
        float sy = 1.0f;
        float scale = getScaleFactor();
        if (mFromX != 1.0f || mToX != 1.0f) {
            sx = mFromX + ((mToX - mFromX) * interpolatedTime);
        }
        if (mFromY != 1.0f || mToY != 1.0f) {
            sy = mFromY + ((mToY - mFromY) * interpolatedTime);
        }
        if (mPivotX == 0 && mPivotY == 0) {
            t.getMatrix().setScale(sx, sy);
        } else {
            t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
        }
    }

applyTransformation()根據傳入的比例參數計算需要操作的動畫參數,然後由Transformation 做動畫操作。其他的子類AlphaAnimation、RotateAnimation、TranslateAnimation都是會重寫這個方法並實現具體的動畫邏輯,這裏就不一一舉例了。

但是問題來了,applyTransformation()是動畫不斷回調的地方,根據傳入的參數實現動畫在運行中,而applyTransformation()被調用的地方沒有循環,那麼一次View樹的遍歷繪製也就執行一次動畫?它是怎麼被多次回調的?

在上面知道applyTransformation()getTransformation()裏面調用,而這個方法有一個boolean返回值,我們看看返回的邏輯是什麼,

View.java

這裏的mMore其實是動畫的完成情況,true表示動畫未完成,false表示動畫已完成或者已取消。我們去看看那裏使用了這個方法的返回值,在applyLegacyAnimation()中找到mMore的使用,applyLegacyAnimation()中返回的more決定了動畫是否執行完,如果返回爲false,那麼將會結束動畫。

View.java

當動畫還沒執行完,就會在調用invalidate()方法,層層通知ViewRootImpl在發起一次遍歷請求,當下一個屏幕刷新信號的時候,再通過performTraversals()遍歷View樹繪製,view的draw()方法被執行,會再次調用applyLegacyAnimation()方法執行相關動畫操作,包括getTransformation()計算動畫進度,applyTransformation()執行動畫邏輯等。

動畫的執行流程這裏就基本完了,文章篇幅過長,我們來總結一下:

1、當調用View.startAnimation(Animation)時,並沒有立即執行動畫,而是通過invalidate()層層通過到ViewRootImpl發起一次遍歷View樹的請求,在接收到下一個(16ms)屏幕信號刷新時才發起遍歷View樹的繪製操作,從DecorView開始遍歷,繪製時會調用draw()方法,如果View有綁定動畫則執行applyLegacyAnimation()方法處理相關動畫邏輯。

2、在applyLegacyAnimation()裏面,先執行初始化initialize(),再通知動畫開始onAnimationStart(),然後通過getTransformation()計算動畫進度,並且它的返回值和動畫是否結束決定是否繼續通知ViewRootImpl發起遍歷請求,view樹繪製,如此重複這個步驟,並且調用applyTransformation()方法執行動畫的邏輯,直到動畫結束。

其實補間動畫僅僅改變的是控件的顯示位置,並沒有改變控件本身的值,View Animation動畫是通過Parent View實現的,在view被draw時Parent View改變它的繪製參數,這樣view的大小位置角度等雖然變化了,但是view的實際屬性並沒有改變。可參考《Android動畫篇(三)—— 屬性動畫ValueAnimator的使用

至此,本文結束!

請尊重原創者版權,轉載請標明出處: https://blog.csdn.net/m0_37796683/article/details/90904394 謝謝!

動畫系列文章:

1、 Android動畫篇(一)—— alpha、scale、translate、rotate、set的xml屬性及用法

  • 補間動畫的XML用法以及屬性詳解

2、Android動畫篇(二)—— 代碼實現alpha、scale、translate、rotate、set及插值器動畫

  • 代碼動態實現補間動畫以及屬性詳解

3、 Android動畫篇(三)—— 屬性動畫ValueAnimator的使用

  • ValueAnimator的基本使用

4、 Android動畫篇(四)—— 屬性動畫ValueAnimator的高級進階

  • 插值器(Interpolator)、計算器(Evaluator)、ValueAnimator的ofObject用法等相關知識

5、 Android動畫篇(五)—— 屬性動畫ObjectAnimator基本使用

  • ObjectAnomator的基本使用以及屬性詳解

6、 Android動畫篇(六)—— 組合動畫AnimatorSet和PropertyValuesHolder的使用

  • AnimatorSet動畫集合和PropertyValuesHolder的使用

以上幾篇動畫文章是一定要掌握的,寫的不好請多多指出!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章