動畫體系知識梳理(1) 轉場動畫 ContentTransition 理論篇

澤毛發表於2017-12-21

一、概述

Android 5.0當中,Google基於Android 4.4中的Transition框架引入了轉場動畫,設計轉場動畫的目的,在於讓Activity之間或者Fragment之間的切換更加自然,其根本原因在於介面間切換時的動畫不再是以Activity或者Fragment的整個佈局作為切換時動畫的執行單元,而是將動畫的執行單元細分到了View。目前提供的轉場動畫分為兩種:

  • Content Transition:用於兩個介面之間非共享的View
  • Shared Element Transition:用於兩個介面之間需要共享的View

二、什麼是Transition

2.1 Transition的基本概念

在學習Content Transition之前,我們先對轉場動畫所依賴的Transition框架做一個簡要的介紹,這個框架是圍繞著兩個概念**Scene(場景)和Transition(變換)**來建立的,在後面我們會多次提到它:

動畫體系知識梳理(1)   轉場動畫 ContentTransition 理論篇

  • 場景(Scene):表示UI所對應的狀態,一般來說,會有兩個場景:起點場景和終點場景,在這兩個場景當中,UI有可能會有不同的狀態。在上圖當中,SceneASceneB就是兩個不同的場景,ViewA在兩個場景中對應的狀態分別為VISIBLEINVISIBLE
  • 變換(Transition):用來定義兩個場景之間切換的規則,當場景發生發生變換時,Transition需要做的有兩點:
  • 確定View在起點場景和終點場景的狀態。
  • 建立View從終點場景切換到終點場景所需的Animator

2.2 Transition的簡單例子

下面,我們通過一個簡單的例子,對上面的概念有一個直觀的感受:

public class ExampleActivity extends Activity implements View.OnClickListener {
    private ViewGroup mRootView;
    private View mRedBox, mGreenBox, mBlueBox, mBlackBox;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mRootView = (ViewGroup) findViewById(R.id.layout_root_view);
        mRootView.setOnClickListener(this);

        mRedBox = findViewById(R.id.red_box);
        mGreenBox = findViewById(R.id.green_box);
        mBlueBox = findViewById(R.id.blue_box);
        mBlackBox = findViewById(R.id.black_box);
    }

    @Override
    public void onClick(View v) {
        TransitionManager.beginDelayedTransition(mRootView, new Fade());
        toggleVisibility(mRedBox, mGreenBox, mBlueBox, mBlackBox);
    }

    private static void toggleVisibility(View... views) {
        for (View view : views) {
            boolean isVisible = view.getVisibility() == View.VISIBLE;
            view.setVisibility(isVisible ? View.INVISIBLE : View.VISIBLE);
        }
    }
}
複製程式碼
  • 第一步:通過beginDelayedTranstion傳入場景對應佈局的根節點(mRootView)以及場景變換的規則(Fade),此時系統理解呼叫TransitioncaptureStartValues方法,來確定場景當中所有子Viewvisibility
  • 第二步:當beginDeleyedTransition返回後,我們將子View設定為不可見。
  • 第三步:在下一幀,系統呼叫TranstioncaptureEndValues()方法獲取場景當中所有子View的可見性。
  • 第四步:這時候系統發現在起始場景中ViewVISIBLE的,而在終點場景中它變為了INVISIBLE,那麼Fade Transition就會根據這些資訊建立並返回AnimatorSet,用它來將那些發生變化的Viewalpha值漸變為0,而不是直接設為不可見。
  • 第五步:系統啟動這個Animator,使得這些View慢慢隱藏。

2.3 Transition小結

我們可以總結出Transition的兩個特點:

  • Animator對於開發者而言是抽象的,開發者設定View的起始值和最終值,Transition會根據這兩者的差異,自動地建立切換的Animator
  • 可以隨時通過替換Transition來改變切換的規則。

三、Content Transition基本概念

3.1 舊的介面切換動畫

回憶一下,在5.0之前:

  • Activity之間的切換新增動畫,在啟動Activity的地方加上overridePendingTransition
  • Fragment之間的切換新增動畫,通過FragmentTransationsetCustomAnimation

這兩種方式都有一個共同的特點,那就是它們都是將Activity所在的視窗或Fragment所對應的佈局作為切換動畫的執行單元

3.2 新的介面切換動畫

在新的切換方式當中,可以將佈局中的每個View作為切換的執行單元,我們以Activity之間的切換為例。

3.2.1 啟動BActivity

AActivity啟動中BActivity,這時候就會涉及到四種Scene和兩種Transition

動畫體系知識梳理(1)   轉場動畫 ContentTransition 理論篇

  • AActivity's Exit Transition:它定義了AActivity中的元素如何從VISIBLE(起點場景)變為INVISIBLE(終點場景)。
  • BActivity's Enter Transition:它定義了BActivity中的元素如果從INVISIBLE(起點場景)變為VISIBLE(終點場景)。

3.2.1.1 確定需要執行TransitionView

整個Transition的第一步,就是先要確定當前介面中需要執行Transition的動畫切換單元,這一過程是通過對整個View樹進行遞迴呼叫得到的,而遞迴的邏輯在ViewGroup當中:

public void captureTransitioningViews(List<View> transitioningViews) {
        if (getVisibility() != View.VISIBLE) {
            return;
        }
        if (isTransitionGroup()) {
            transitioningViews.add(this);
        } else {
            int count = getChildCount();
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                child.captureTransitioningViews(transitioningViews);
            }
        }
}
複製程式碼

而在View中,該方法為:

public void captureTransitioningViews(List<View> transitioningViews) {
        if (getVisibility() == View.VISIBLE) {
            transitioningViews.add(this);
        }
}
複製程式碼

由此可見,所有需要變換的ViewGroup/View都儲存在transitioningViews當中,關於這個集合的構成依據以下三點:

  • 節點不可見,那麼它以及它的所有子節點都不加入集合。
  • 節點的isTransitionGroup()標誌位為true,那麼把它和它的所有子節點當成一個變換單元加入到集合當中。
  • 除了以上兩種情況,那麼View樹的所有葉子節點都加入到集合當中。

其中isTransitionGroup()的值我們可以通過setTransitionGroup(boolean flag)來改變,如果在場景當中用到了WebView,而我們希望將它作為一個整體進行變換,那麼應當加上這個標誌位。 除了系統預設的遍歷,我們還可以通過Transitionaddedexcluded來改變這個集合。

3.2.1.2 Exit Transition的執行過程

下面,我們以AActivityExit Transition為例,描述一下它整個的執行過程:

  • 第一步:系統遍歷AActivityView樹,並決定在exit transition執行時需要變換的View,把它們放在集合當中,也就是我們在3.2.1.1中所說的transitionViews
  • 第二步:AActivityExit Transition獲取集合中View的起始狀態,呼叫的是captureStartValues方法。
  • 第三步:將集合中的View設為INVISIBLE
  • 第四步:在下一幀時,Exit Transition獲取集合中View的終點狀態,呼叫的是captureEndValues方法。
  • 第五步:Exit Transition根據第二步中的起始狀態和終點狀態,建立一個Animator,並執行這個Animator,由於是從VISIBLE變為INVISIBLE,因此,是通過onDisappear方法得到Animator

3.2.1.3 Enter Transition的執行過程。

BActivityEnter TransitionAActvityExit Transition類似,只不過第三步操作是從INVISIBLEVISIBLE

3.2.2 從BActivity返回

而當我們從BActivity返回到AActivity,那麼就會涉及到下面四種Scene和兩種Transition

動畫體系知識梳理(1)   轉場動畫 ContentTransition 理論篇

  • BActivity's Return Transition
  • AActivity's Reenter Transition

其原理和上面是相同的,就不多介紹了。

3.2.3 小結

無論是AActivity啟動BActivity,還是BActivity返回到AActivity,當View的可見性不斷切換的時候,系統能保證根據狀態資訊來建立所需的動畫。很顯然,所有的Content transition物件都需要能夠捕捉並記錄View的起始狀態和終點狀態,幸運的是,抽象類Visiblity已經幫我們做了,我們只需要實現onAppearonDisappear方法,在裡面建立一個Animator來定義進入和退出場景的View的動畫,系統預設提供了三種Transition - Fade、Slide、Explode,下面我們在分析Fade原始碼的時候,會詳細解釋這一過程。

3.3 Content TransitionShared Element Transition

在上面的討論當中,我們是從切換的角度來考慮的,而如果我們從Transition的角度來看,那麼每個Transition又可以細分為兩類:

  • content transitions:定義了Activity非共享View進入和退出場景的方式。
  • shared element transitions:定義了Acitivity共享View進入和退出場景的方法。

3.4 例子

下面,我們以一個視訊來解釋一下上面談到的四個Transition

動畫體系知識梳理(1)   轉場動畫 ContentTransition 理論篇
在這個視訊當中,我們將列表頁稱為AActivity,詳情頁稱為BActivity,此時,對應於上面提到的四種Transition

  • AActivity's Exit Transitionnull
  • AActivity's Reenter Transitionnull
  • BActivity's Enter Transition則分為三個部分:
  • 封面從小的圓形漸漸變成大的方形
  • 播放圖示的半徑漸漸變大
  • 底下的列表採用了自定義的Slide-in動畫。
  • BActivity's Exit Transition
  • 上半部分採用了Slide(TOP)的方式,而下半部分採用Slide(BOTTOM)的方式。

四、原始碼分析

系統預設自帶了三種TransitionFade、Slide、Explode,這一節,我們一起來分析一下它們的實現方式:

4.1 Fade

4.1.1 captureXXX函式

首先,我們看一下它獲取起點和終點屬性的函式:

  • public void captureStartValues(TransitionValues transitionValues)
  • public void captureEndValues(TransitionValues transitionValues)

Fade只重寫了captureStartValues,在這裡面,它把View當前的translationAlpha值儲存起來,這個值表示的是在Transition開始之前ViewtranslationAlpha的值:

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        super.captureStartValues(transitionValues);
        transitionValues.values.put(PROPNAME_TRANSITION_ALPHA, transitionValues.view.getTransitionAlpha());
    }
複製程式碼

4.1.2 onAppearonDisappear

在上面的分析當中,我們提到過,當View的可見性從INVISIBLE變為VISIBLE時會呼叫Transition中的Animator來執行這一變換的過程,例如從AActivity跳轉到BActivity,那麼BActivity中的View就會呼叫onAppear所返回的Animator

    @Override
    public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
        float startAlpha = getStartAlpha(startValues, 0);
        if (startAlpha == 1) {
            startAlpha = 0;
        }
        return createAnimation(view, startAlpha, 1);
    }
複製程式碼

這裡首先會通過getStartAlpha去獲取起始的transitionAlpha值,它是把之前儲存在PROPNAME_TRANSITION_ALPHA中的值取出來:

    private static float getStartAlpha(TransitionValues startValues, float fallbackValue) {
        float startAlpha = fallbackValue;
        if (startValues != null) {
            Float startAlphaFloat = (Float) startValues.values.get(PROPNAME_TRANSITION_ALPHA);
            if (startAlphaFloat != null) {
                startAlpha = startAlphaFloat;
            }
        }
        return startAlpha;
    }
複製程式碼

下面,我們再回到onAppear函式當中,看一下Animator的建立過程:

    private Animator createAnimation(final View view, float startAlpha, final float endAlpha) {
        if (startAlpha == endAlpha) {
            return null;
        }
        view.setTransitionAlpha(startAlpha);
        final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "transitionAlpha", endAlpha);
        final FadeAnimatorListener listener = new FadeAnimatorListener(view);
        anim.addListener(listener);
        addListener(new TransitionListenerAdapter() {
            @Override
            public void onTransitionEnd(Transition transition) {
                view.setTransitionAlpha(1);
            }
        });
        return anim;
    }
複製程式碼

從上面可以看出,它返回的是一個ObjectAnimator,這個Animator會把ViewtranslationAlphastartAlpha變為1,這也就是一個漸漸顯示的過程。 再看一下onDisappear函式,它就是onAppear的反向過程:

    @Override
    public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues,
            TransitionValues endValues) {
        float startAlpha = getStartAlpha(startValues, 1);
        return createAnimation(view, startAlpha, 0);
    }
複製程式碼

4.2 Slide

下面,我們來看一下另一種Transition - Slide的實現原理,和上面類似,我們先看一下captureXXX方都做了什麼:

4.2.1 captureXXX

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        super.captureStartValues(transitionValues);
        captureValues(transitionValues);
    }

    @Override
    public void captureEndValues(TransitionValues transitionValues) {
        super.captureEndValues(transitionValues);
        captureValues(transitionValues);
    }
複製程式碼

對於起點和終點值的獲取都是呼叫了下面這個函式,它儲存的是View在視窗中的位置:

private void captureValues(TransitionValues transitionValues) {
    View view = transitionValues.view;
    int[] position = new int[2];
    view.getLocationOnScreen(position);
    transitionValues.values.put(PROPNAME_SCREEN_POSITION, position);
}
複製程式碼

4.2.2 onAppearonDisappear

    @Override
    public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
        if (endValues == null) {
            return null;
        }
        int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION);
        //終點值是確定的
        float endX = view.getTranslationX();
        float endY = view.getTranslationY();
        //起點值則需要根據所選的模式來確定
        float startX = mSlideCalculator.getGoneX(sceneRoot, view, mSlideFraction);
        float startY = mSlideCalculator.getGoneY(sceneRoot, view, mSlideFraction);
        //根據起點值、終點值、View所處視窗的位置,來得到一個`Animator`
        return TranslationAnimationCreator.createAnimation(view, endValues, position[0], position[1], startX, startY, endX, endY, sDecelerate, this);
    }
複製程式碼

這裡面,最關鍵的是mSlideCalculator,預設情況下為:

    private static final CalculateSlide sCalculateBottom = new CalculateSlideVertical() {
        @Override
        public float getGoneY(ViewGroup sceneRoot, View view, float fraction) {
            return view.getTranslationY() + sceneRoot.getHeight() * fraction;
        }
    };
複製程式碼

用一張圖解解釋一下上面的座標:

動畫體系知識梳理(1)   轉場動畫 ContentTransition 理論篇
所以當我們採用這個Transition的時候,就可以看到它從螢幕的底端滑上來。 而onDisappear則也是一個反向的過程:

    @Override
    public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null) {
            return null;
        }
        int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION);
        //這裡的起始值和終點值正好是和onAppear相反的.
        float startX = view.getTranslationX();
        float startY = view.getTranslationY();
        float endX = mSlideCalculator.getGoneX(sceneRoot, view, mSlideFraction);
        float endY = mSlideCalculator.getGoneY(sceneRoot, view, mSlideFraction);
        return TranslationAnimationCreator.createAnimation(view, startValues, position[0], position[1], startX, startY, endX, endY, sAccelerate, this);
    }
複製程式碼

4.3 小結

通過分析FadeSlide的原始碼,它們的主要思想就是:

  • capturexxx方法中,把屬性儲存在TranslationValues中,這裡,一定要記得呼叫對應的super方法讓系統儲存一些預設的狀態。
  • onAppearonDisappear中,根據起點和終點和終點的TranslationValues,構造一個改變View屬性的Animator,同時在動畫結束之後,還原它的屬性。

五、總結

這一篇我們分析了Content Transition的設計思想和原理,下一篇文章我們將著重討論如何通過程式碼來實現上面的效果。

六、參考文獻

1.Getting Started with Activity & Fragment Transitions (part 1) 2.Content Transitions In-Depth (part 2) 3.Material-Animations

相關文章