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

試試 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 的大小,文字會在物件完全調整大小前彈出至新位置。為避免這個問題,請勿為包含文字的檢視區塊設定大小調整動畫。