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

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

Android 的轉換架構可提供開始和結束版面配置,為 UI 中的所有動作建立動畫。您可以選取所需的動畫類型 (例如淡入或淡出檢視畫面,或是變更檢視畫面大小),然後轉換架構會決定動畫從起始版面配置到結束版面配置的動畫方式。

轉換架構包含下列功能:

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

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

以下說明在兩個版面配置之間建立動畫的基本流程:

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

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

圖 1 轉換架構建立動畫的基本插圖。

建立場景

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

您可以透過版面配置資源檔案或程式碼中的一組檢視畫面建立場景。不過,轉換作業的起始場景通常是從目前的 UI 自動決定。

場景也可以定義自己的動作,系統會在您變更場景時執行這些動作。這項功能有助於在轉換至場景後清除檢視畫面設定。

從版面配置資源建立場景

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

如要從版面配置資源檔案建立 Scene 例項,請從版面配置擷取場景根層級做為 ViewGroup。接著,使用場景根目錄和包含場景檢視區塊階層的版面配置檔案,呼叫 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_outfade_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 的大小,系統會在物件完全調整大小前,將文字彈出到新位置。如要避免這個問題,請不要為含有文字的檢視畫面設定動畫大小。