使用轉場功能以動畫呈現版面配置變更

試用 Compose 方法
Jetpack Compose 是 Android 推薦的 UI 工具包。瞭解如何在 Compose 中使用動畫。

透過 Android 的轉換架構,您可以提供起始和結束版面配置,為 UI 中的所有動態效果建立動畫。您可以選擇想要的動畫類型,例如讓檢視畫面淡入/淡出,或是變更檢視畫面大小,而轉換架構會決定從起始版面配置到結束版面配置的動畫方式。

轉換架構包含下列功能:

  • 群組層級動畫:將動畫效果套用至檢視區塊階層中的所有檢視畫面。
  • 內建動畫:使用預先定義的動畫做為常見效果,例如淡出或移動。
  • 資源檔案支援:從版面配置資源檔案載入檢視區塊階層和內建動畫。
  • 生命週期回呼:接收可提供動畫和階層變更程序的回呼。

如需在版面配置變更之間建立動畫效果的程式碼範例,請參閱 BasicTransition

在兩個版面配置之間建立動畫的基本程序如下:

  1. 為起始和結束版面配置建立 Scene 物件。不過,起始版面配置的場景通常會從目前的版面配置自動決定。
  2. 建立 Transition 物件來定義所需的動畫類型。
  3. 呼叫 TransitionManager.go(),系統會執行動畫來切換版面配置。

圖 1 中的圖表說明版面配置、場景、轉場效果與最終動畫之間的關係。

圖 1 基本插圖:轉換架構如何建立動畫。

建立場景

場景會儲存檢視區塊階層的狀態,包括所有檢視畫面及其屬性值。轉換架構可以在起始和結束場景之間執行動畫。

您可以使用版面配置資源檔案或程式碼中的一組檢視畫面建立場景。不過,系統通常會根據目前的 UI 自動決定轉場效果的起始場景。

場景也可以定義自己的動作,在變更場景時執行。轉換到場景後,這項功能可用來清理檢視畫面設定。

從版面配置資源建立場景

您可以直接從版面配置資源檔案建立 Scene 執行個體。如果檔案中的檢視區塊階層大部分都是靜態的,請使用這項技巧。產生的場景代表建立 Scene 執行個體時的檢視區塊階層狀態。如果變更檢視區塊階層,請重建場景。架構會從檔案中的整個檢視區塊階層建立場景。您無法從版面配置檔案的一部分建立場景。

如要從版面配置資源檔案建立 Scene 執行個體,請將版面配置中的場景根目錄擷取為 ViewGroup。接著,使用場景根目錄和包含場景檢視區塊階層的版面配置檔案資源 ID 呼叫 Scene.getSceneForLayout() 函式。

定義場景的版面配置

本節其餘部分使用的程式碼片段將說明如何同一個場景根元素建立兩個不同的場景。程式碼片段也示範如何載入多個不相關的 Scene 物件,而不會暗示這些物件彼此相互關聯。

本範例包含下列版面配置定義:

  • 含有文字標籤和子項 FrameLayout 的活動的主要版面配置。
  • 包含兩個文字欄位的第一個場景的 ConstraintLayout
  • 第二個場景的 ConstraintLayout,具有相同順序的兩個文字欄位。

這個範例的設計可讓所有動畫在活動主要版面配置的子版面配置中進行。主要版面配置中的文字標籤會保持靜態。

活動的主要版面配置定義如下:

res/layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/master_layout">
    <TextView
        android:id="@+id/title"
        ...
        android:text="Title"/>
    <FrameLayout
        android:id="@+id/scene_root">
        <include layout="@layout/a_scene" />
    </FrameLayout>
</LinearLayout>

這個版面配置定義包含文字欄位和子根子項 FrameLayout。主要版面配置檔案中會納入第一個場景的版面配置。這可讓應用程式將其顯示為初始使用者介面的一部分,同時將其載入場景中,因為架構只能載入整個版面配置檔案。

第一個場景的版面配置定義如下:

res/layout/a_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    
</androidx.constraintlayout.widget.ConstraintLayout>

第二個場景的版面配置包含相同的兩個文字欄位 (具有相同 ID),並以不同順序排列。定義如下:

res/layout/another_scene.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    
    
</androidx.constraintlayout.widget.ConstraintLayout>

從版面配置產生場景

建立兩個限製版面配置的定義後,您可以取得每個限製版面配置的場景。這樣您就能在兩種 UI 設定之間進行轉換。如要取得場景,您必須參照場景根目錄和版面配置資源 ID。

下列程式碼片段說明如何取得場景根目錄的參照,並從版面配置檔案建立兩個 Scene 物件:

Kotlin

val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this)
val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this)

Java

Scene aScene;
Scene anotherScene;

// Create the scene root for the scenes in this app.
sceneRoot = (ViewGroup) findViewById(R.id.scene_root);

// Create the scenes.
aScene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this);
anotherScene =
    Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this);

在應用程式中,現在有兩個根據檢視區塊階層為基礎的 Scene 物件。這兩個場景都使用 res/layout/activity_main.xmlFrameLayout 元素定義的場景根。

在程式碼中建立場景

您也可以透過 ViewGroup 物件,在程式碼中建立 Scene 執行個體。如要直接在程式碼中修改檢視區塊階層,或以動態方式產生檢視區塊階層,請使用這項技巧。

如要從程式碼中的檢視區塊階層建立場景,請使用 Scene(sceneRoot, viewHierarchy) 建構函式。在加載版面配置檔案時,呼叫此建構函式相當於呼叫 Scene.getSceneForLayout() 函式。

下列程式碼片段說明如何從程式碼中場景根元素和場景的檢視區塊階層建立 Scene 例項:

Kotlin

val sceneRoot = someLayoutElement as ViewGroup
val viewHierarchy = someOtherLayoutElement as ViewGroup
val scene: Scene = Scene(sceneRoot, viewHierarchy)

Java

Scene mScene;

// Obtain the scene root element.
sceneRoot = (ViewGroup) someLayoutElement;

// Obtain the view hierarchy to add as a child of
// the scene root when this scene is entered.
viewHierarchy = (ViewGroup) someOtherLayoutElement;

// Create a scene.
mScene = new Scene(sceneRoot, mViewHierarchy);

建立場景動作

這個架構可讓您定義系統在進入或離開場景時要執行的自訂場景動作。在許多情況下,您不一定要定義自訂場景動作,因為架構會自動在場景之間建立動畫動畫。

場景動作適用於處理以下情況:

  • 為不屬於同一階層的檢視畫面建立動畫效果。您可以使用「結束」和「進入場景」動作,為開始和結束場景的檢視畫面建立動畫。
  • 為轉換架構無法自動建立動畫的檢視畫面 (例如 ListView 物件) 建立動畫。詳情請參閱「限制」一節。

如要提供自訂場景動作,請將動作定義為 Runnable 物件,並傳遞至 Scene.setExitAction()Scene.setEnterAction() 函式。架構會先在開始的場景上呼叫 setExitAction() 函式,然後再執行轉場動畫,並在執行轉場動畫後在結束場景上呼叫 setEnterAction() 函式。

套用轉場效果

轉換架構代表使用 Transition 物件呈現場景之間的動畫樣式。您可以使用內建子類別 (例如 AutoTransitionFade) 對 Transition 執行個體化,或是定義自己的轉場效果。接著,您可以將結束的 SceneTransition 傳遞至 TransitionManager.go(),以便在場景之間執行動畫。

轉換生命週期與活動生命週期相似,代表架構在動畫開始和結束之間監控的轉換狀態。在重要的生命週期狀態中,架構會叫用回呼函式,您可以實作這些函式,在轉換的不同階段調整使用者介面。

建立轉場效果

上一節說明如何建立代表不同檢視區塊階層狀態的場景。定義要切換的開始和結束場景後,請建立可定義動畫的 Transition 物件。這個架構可讓您在資源檔案中指定內建轉換功能,並在程式碼中加載該轉換,或直接在程式碼中建立內建轉換功能的執行個體。

表 1. 內建轉換類型。

類別 標記 效果
AutoTransition <autoTransition/> 預設轉換。以順序淡出、移動、調整大小,以及淡入檢視畫面。
ChangeBounds <changeBounds/> 移動及調整檢視畫面大小。
ChangeClipBounds <changeClipBounds/> 擷取場景變更前後的 View.getClipBounds(),並在轉換期間為相關變化加入動畫效果。
ChangeImageTransform <changeImageTransform/> 擷取場景變更前後的 ImageView 矩陣,並在轉換期間加入動畫效果。
ChangeScroll <changeScroll/> 擷取場景變更前後的目標捲動屬性,並將任何變更加入動畫效果。
ChangeTransform <changeTransform/> 擷取場景變更前後的縮放比例和旋轉,並在轉換期間為這些變化加上動畫效果。
Explode <explode/> 追蹤在開始和結束場景中的目標檢視畫面瀏覽權限變更,並將檢視畫面移入或移出場景邊緣。
Fade <fade/> fade_in 會淡入檢視畫面。
fade_out 淡出檢視畫面。
fade_in_out (預設) 會執行 fade_out,後面接著 fade_in
Slide <slide/> 追蹤在開始和結束場景中的目標檢視畫面瀏覽權限變更,並將檢視畫面移入或移出場景的任一邊緣。

透過資源檔案建立轉換執行個體

這項技巧可讓您在不變更活動程式碼的情況下修改轉換定義。這項技巧也有助於將複雜的轉換定義與應用程式程式碼分開,詳情請參閱指定多個轉場效果一節。

如要在資源檔案中指定內建轉場效果,請按照下列步驟操作:

  • res/transition/ 目錄新增至專案。
  • 在這個目錄中建立新的 XML 資源檔案。
  • 為其中一個內建轉場效果新增 XML 節點。

舉例來說,下列資源檔案指定了 Fade 轉換效果:

res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

下列程式碼片段說明如何從資源檔案的活動中加載 Transition 例項:

Kotlin

var fadeTransition: Transition =
    TransitionInflater.from(this)
                      .inflateTransition(R.transition.fade_transition)

Java

Transition fadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

在程式碼中建立轉換執行個體

如果您在程式碼中修改使用者介面,並建立不含參數 (或不含參數) 的簡易內建轉換執行個體,這項技巧就非常實用。

如要建立內建轉換的執行個體,請叫用 Transition 類別子類別中的其中一個公開建構函式。例如,下列程式碼片段會建立 Fade 轉換的執行個體:

Kotlin

var fadeTransition: Transition = Fade()

Java

Transition fadeTransition = new Fade();

套用轉場效果

您通常會套用轉換來切換不同的檢視區塊階層,以回應使用者動作等事件。以搜尋應用程式為例:當使用者輸入搜尋字詞並輕觸搜尋按鈕時,應用程式會變更為代表結果版面配置的場景,同時套用淡出搜尋按鈕並淡化搜尋結果中的轉場效果。

如要在回應活動中的事件時套用轉場效果,請在 TransitionManager.go() 類別函式中加入結尾畫面和用於動畫的轉換例項,藉此變更場景,如以下程式碼片段所示:

Kotlin

TransitionManager.go(endingScene, fadeTransition)

Java

TransitionManager.go(endingScene, fadeTransition);

架構會在執行轉換執行個體指定的動畫時,從結束場景變更場景根目錄中的檢視區塊階層和檢視區塊階層。起始場景是最近一次轉場後的結束場景。如果沒有先前的轉場效果,則系統會從使用者介面的目前狀態自動決定起始場景。

如果您未指定轉換執行個體,轉換管理員可套用在大多數情況下合理的自動轉換效果。詳情請參閱 TransitionManager 類別的 API 參考資料。

選擇特定目標檢視畫面

根據預設,架構會套用開始和結束場景中的所有檢視畫面轉換。在某些情況下,您可能只想將動畫套用至場景中的部分檢視畫面。架構可讓您選取要建立動畫的特定檢視畫面。例如,該架構不支援為 ListView 物件變更動畫效果,因此請勿在轉換期間嘗試為這些物件建立動畫。

轉換動畫的每個檢視畫面都稱為「目標」。您只能選取屬於與場景相關聯的檢視區塊階層中的目標。

如要從目標清單中移除一或多個檢視畫面,請先呼叫 removeTarget() 方法,再開始轉換。如果只想將指定的檢視畫面加入目標清單,請呼叫 addTarget() 函式。詳情請參閱 Transition 類別的 API 參考資料。

指定多個轉場效果

為了充分發揮動畫的效益,請將動畫和場景之間出現的變更類型配對。舉例來說,如果您要移除部分檢視畫面,並在場景間加入其他檢視畫面,請以淡出或淡入的動畫效果表示,部分檢視畫面無法再使用。如果要將檢視畫面移至螢幕上的不同點,建議您為動作加上動畫效果,讓使用者註意到檢視畫面的新位置。

您不需要只選擇單一動畫,因為轉換架構可讓您在包含一組個別內建或自訂轉場效果的轉換集中結合使用動畫效果。

如要從 XML 中的轉換集合定義轉換集,請在 res/transitions/ 目錄中建立資源檔案,並在 TransitionSet 元素下方列出轉場效果。例如,下列程式碼片段說明如何指定與 AutoTransition 類別具有相同行為的轉換集:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="sequential">
    <fade android:fadingMode="fade_out" />
    <changeBounds />
    <fade android:fadingMode="fade_in" />
</transitionSet>

如要在程式碼中將轉場效果加載至 TransitionSet 物件,請在活動中呼叫 TransitionInflater.from() 函式。TransitionSet 類別來自 Transition 類別,因此您可以將其與轉換管理員搭配使用,就像其他 Transition 執行個體一樣。

套用無場景的轉場效果

變更檢視區塊階層不是修改使用者介面的唯一方式,您也可以在目前階層內新增、修改及移除子項檢視畫面,藉此進行變更。

舉例來說,您可以使用單一版面配置實作搜尋互動。從顯示搜尋輸入欄位和搜尋圖示的版面配置開始。如要變更使用者介面以顯示結果,請在使用者輕觸搜尋按鈕時呼叫 ViewGroup.removeView() 函式,然後呼叫 ViewGroup.addView() 函式來新增搜尋結果。

另一種情況是如果兩個階層幾乎相同,就可以使用這個方法。您可以有一個版面配置檔案,其中包含您在程式碼中修改的檢視區塊階層,而不是建立及維護兩個獨立的版面配置檔案。

如果您以這種方式在目前的檢視區塊階層內進行變更,就不需要建立場景。您可以改用「延遲轉換」,在檢視區塊階層的兩個狀態之間建立及套用轉換。轉換架構的這項功能會從目前的檢視區塊階層狀態開始,記錄您對檢視畫面所做的變更,並套用在系統重新繪製使用者介面時為變更的動畫效果。

如要在單一檢視區塊階層中建立延遲轉換,請按照下列步驟操作:

  1. 觸發轉換的事件時,請呼叫 TransitionManager.beginDelayedTransition() 函式,提供您要變更的所有檢視畫面的上層檢視畫面和轉場效果。架構會儲存子檢視畫面的目前狀態及其屬性值。
  2. 根據您的用途要求變更子項檢視畫面。架構會記錄您對子檢視畫面及其屬性所做的變更。
  3. 系統根據變更內容重新繪製使用者介面時,架構會將原始狀態和新狀態之間的變化加入動畫效果。

以下範例說明如何使用延遲轉換功能,為新增文字檢視區塊至檢視區塊階層的行為建立動畫效果。第一個程式碼片段顯示版面配置定義檔案:

res/layout/activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/mainLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <EditText
        android:id="@+id/inputText"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    ...
</androidx.constraintlayout.widget.ConstraintLayout>

下一個程式碼片段顯示新增文字檢視區塊的動畫程式碼:

MainActivity

Kotlin

setContentView(R.layout.activity_main)
val labelText = TextView(this).apply {
    text = "Label"
    id = R.id.text
}
val rootView: ViewGroup = findViewById(R.id.mainLayout)
val mFade: Fade = Fade(Fade.IN)
TransitionManager.beginDelayedTransition(rootView, mFade)
rootView.addView(labelText)

Java

private TextView labelText;
private Fade mFade;
private ViewGroup rootView;
...
// Load the layout.
setContentView(R.layout.activity_main);
...
// Create a new TextView and set some View properties.
labelText = new TextView(this);
labelText.setText("Label");
labelText.setId(R.id.text);

// Get the root view and create a transition.
rootView = (ViewGroup) findViewById(R.id.mainLayout);
mFade = new Fade(Fade.IN);

// Start recording changes to the view hierarchy.
TransitionManager.beginDelayedTransition(rootView, mFade);

// Add the new TextView to the view hierarchy.
rootView.addView(labelText);

// When the system redraws the screen to show this update,
// the framework animates the addition as a fade in.

定義轉換生命週期回呼

轉換生命週期與活動生命週期相似。這代表架構在呼叫 TransitionManager.go() 函式到動畫結束之間所監控的轉場狀態。在重要的生命週期狀態下,架構會叫用 TransitionListener 介面定義的回呼。

轉換生命週期回呼非常實用,例如在場景變更期間,將檢視畫面階層中的檢視畫面屬性值從起始檢視區塊階層複製到結束檢視區塊階層。您無法直接將初始檢視畫面的值複製到結束檢視區塊階層的檢視區塊,因為結束的檢視區塊階層在轉換完成前不會加載。相反地,您需要將值儲存在變數中,然後在架構完成轉換後,將該值複製到結尾檢視區塊階層。如要在轉場完成後收到通知,請在活動中實作 TransitionListener.onTransitionEnd() 函式。

詳情請參閱 TransitionListener 類別的 API 參考資料。

限制

本節列出轉換架構的一些已知限制:

  • 套用至 SurfaceView 的動畫可能不會正確顯示。SurfaceView 執行個體是從非 UI 執行緒更新,因此更新內容可能會與其他檢視畫面的動畫不同步。
  • 將某些轉換類型套用至 TextureView 時,可能無法產生想要的動畫效果。
  • 擴充 AdapterView 的類別 (例如 ListView) 會以與轉換架構不相容的方式管理子檢視畫面。如果您嘗試根據 AdapterView 為檢視畫面建立動畫,裝置螢幕可能會停止回應。
  • 如果您嘗試調整具有動畫的 TextView 大小,系統會在物件徹底調整大小前,將文字彈出新的位置。為避免發生這個問題,請勿為含有文字的檢視畫面調整大小動畫。