一、概述
在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
(變換)**來建立的,在後面我們會多次提到它:
- 場景(
Scene
):表示UI
所對應的狀態,一般來說,會有兩個場景:起點場景和終點場景,在這兩個場景當中,UI
有可能會有不同的狀態。在上圖當中,SceneA
和SceneB
就是兩個不同的場景,ViewA
在兩個場景中對應的狀態分別為VISIBLE
和INVISIBLE
。 - 變換(
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
),此時系統理解呼叫Transition
的captureStartValues
方法,來確定場景當中所有子View
的visibility
。 - 第二步:當
beginDeleyedTransition
返回後,我們將子View
設定為不可見。 - 第三步:在下一幀,系統呼叫
Transtion
的captureEndValues()
方法獲取場景當中所有子View
的可見性。 - 第四步:這時候系統發現在起始場景中
View
是VISIBLE
的,而在終點場景中它變為了INVISIBLE
,那麼Fade Transition
就會根據這些資訊建立並返回AnimatorSet
,用它來將那些發生變化的View
的alpha
值漸變為0
,而不是直接設為不可見。 - 第五步:系統啟動這個
Animator
,使得這些View
慢慢隱藏。
2.3 Transition
小結
我們可以總結出Transition
的兩個特點:
Animator
對於開發者而言是抽象的,開發者設定View
的起始值和最終值,Transition
會根據這兩者的差異,自動地建立切換的Animator
。- 可以隨時通過替換
Transition
來改變切換的規則。
三、Content Transition
基本概念
3.1 舊的介面切換動畫
回憶一下,在5.0
之前:
- 給
Activity
之間的切換新增動畫,在啟動Activity
的地方加上overridePendingTransition
- 給
Fragment
之間的切換新增動畫,通過FragmentTransation
的setCustomAnimation
。
這兩種方式都有一個共同的特點,那就是它們都是將Activity
所在的視窗或Fragment
所對應的佈局作為切換動畫的執行單元。
3.2 新的介面切換動畫
在新的切換方式當中,可以將佈局中的每個View
作為切換的執行單元,我們以Activity
之間的切換為例。
3.2.1 啟動BActivity
在AActivity
啟動中BActivity
,這時候就會涉及到四種Scene
和兩種Transition
:
AActivity's Exit Transition
:它定義了AActivity
中的元素如何從VISIBLE
(起點場景)變為INVISIBLE
(終點場景)。BActivity's Enter Transition
:它定義了BActivity
中的元素如果從INVISIBLE
(起點場景)變為VISIBLE
(終點場景)。
3.2.1.1 確定需要執行Transition
的View
整個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
,而我們希望將它作為一個整體進行變換,那麼應當加上這個標誌位。
除了系統預設的遍歷,我們還可以通過Transition
的added
和excluded
來改變這個集合。
3.2.1.2 Exit Transition
的執行過程
下面,我們以AActivity
的Exit Transition
為例,描述一下它整個的執行過程:
- 第一步:系統遍歷
AActivity
的View
樹,並決定在exit transition
執行時需要變換的View
,把它們放在集合當中,也就是我們在3.2.1.1
中所說的transitionViews
。 - 第二步:
AActivity
的Exit Transition
獲取集合中View
的起始狀態,呼叫的是captureStartValues
方法。 - 第三步:將集合中的
View
設為INVISIBLE
。 - 第四步:在下一幀時,
Exit Transition
獲取集合中View
的終點狀態,呼叫的是captureEndValues
方法。 - 第五步:
Exit Transition
根據第二步中的起始狀態和終點狀態,建立一個Animator
,並執行這個Animator
,由於是從VISIBLE
變為INVISIBLE
,因此,是通過onDisappear
方法得到Animator
。
3.2.1.3 Enter Transition
的執行過程。
BActivity
的Enter Transition
和AActvity
的Exit Transition
類似,只不過第三步操作是從INVISIBLE
到VISIBLE
。
3.2.2 從BActivity
返回
而當我們從BActivity
返回到AActivity
,那麼就會涉及到下面四種Scene
和兩種Transition
:
BActivity's Return Transition
AActivity's Reenter Transition
其原理和上面是相同的,就不多介紹了。
3.2.3 小結
無論是AActivity
啟動BActivity
,還是BActivity
返回到AActivity
,當View
的可見性不斷切換的時候,系統能保證根據狀態資訊來建立所需的動畫。很顯然,所有的Content transition
物件都需要能夠捕捉並記錄View
的起始狀態和終點狀態,幸運的是,抽象類Visiblity
已經幫我們做了,我們只需要實現onAppear
和onDisappear
方法,在裡面建立一個Animator
來定義進入和退出場景的View
的動畫,系統預設提供了三種Transition
- Fade、Slide、Explode
,下面我們在分析Fade
原始碼的時候,會詳細解釋這一過程。
3.3 Content Transition
和Shared Element Transition
在上面的討論當中,我們是從切換的角度來考慮的,而如果我們從Transition
的角度來看,那麼每個Transition
又可以細分為兩類:
content transitions
:定義了Activity
非共享View
進入和退出場景的方式。shared element transitions
:定義了Acitivity
共享View
進入和退出場景的方法。
3.4 例子
下面,我們以一個視訊來解釋一下上面談到的四個Transition
:
AActivity
,詳情頁稱為BActivity
,此時,對應於上面提到的四種Transition
:
AActivity's Exit Transition
為null
AActivity's Reenter Transition
為null
BActivity's Enter Transition
則分為三個部分:- 封面從小的圓形漸漸變成大的方形
- 播放圖示的半徑漸漸變大
- 底下的列表採用了自定義的
Slide-in
動畫。 BActivity's Exit Transition
:- 上半部分採用了
Slide(TOP)
的方式,而下半部分採用Slide(BOTTOM)
的方式。
四、原始碼分析
系統預設自帶了三種Transition
,Fade、Slide、Explode
,這一節,我們一起來分析一下它們的實現方式:
4.1 Fade
4.1.1 captureXXX
函式
首先,我們看一下它獲取起點和終點屬性的函式:
public void captureStartValues(TransitionValues transitionValues)
public void captureEndValues(TransitionValues transitionValues)
Fade
只重寫了captureStartValues
,在這裡面,它把View
當前的translationAlpha
值儲存起來,這個值表示的是在Transition
開始之前View
的translationAlpha
的值:
@Override
public void captureStartValues(TransitionValues transitionValues) {
super.captureStartValues(transitionValues);
transitionValues.values.put(PROPNAME_TRANSITION_ALPHA, transitionValues.view.getTransitionAlpha());
}
複製程式碼
4.1.2 onAppear
和onDisappear
在上面的分析當中,我們提到過,當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
會把View
的translationAlpha
從startAlpha
變為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 onAppear
和onDisappear
@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;
}
};
複製程式碼
用一張圖解解釋一下上面的座標:
所以當我們採用這個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 小結
通過分析Fade
和Slide
的原始碼,它們的主要思想就是:
- 在
capturexxx
方法中,把屬性儲存在TranslationValues
中,這裡,一定要記得呼叫對應的super
方法讓系統儲存一些預設的狀態。 - 在
onAppear
和onDisappear
中,根據起點和終點和終點的TranslationValues
,構造一個改變View
屬性的Animator
,同時在動畫結束之後,還原它的屬性。
五、總結
這一篇我們分析了Content Transition
的設計思想和原理,下一篇文章我們將著重討論如何通過程式碼來實現上面的效果。
六、參考文獻
1.Getting Started with Activity & Fragment Transitions (part 1) 2.Content Transitions In-Depth (part 2) 3.Material-Animations