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

試用 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 大小,在物件完全調整大小之前,文字會彈出至新的位置。為避免這個問題,請勿以動畫方式調整內含文字的檢視畫面大小。