遷移を使用してレイアウト変更をアニメーションにする

Compose を試す
Jetpack Compose は Android で推奨される UI ツールキットです。Compose でアニメーションを使用する方法を学びます。

Android の遷移フレームワークでは、開始レイアウトと終了レイアウトを提供することで、UI のあらゆる種類のモーションをアニメーション化できます。ビューをフェードイン / アウトする、ビューのサイズを変更するなど、アニメーションの種類を選択できます。遷移フレームワークにより、開始レイアウトから終了レイアウトへのアニメーション化方法が決まります。

遷移フレームワークには次の機能があります。

  • グループレベルのアニメーション: ビュー階層内のすべてのビューにアニメーション効果を適用します。
  • 組み込みのアニメーション: フェードアウトや移動などの一般的な効果に、事前定義されたアニメーションを使用します。
  • リソース ファイルのサポート: レイアウト リソース ファイルからビュー階層と組み込みアニメーションを読み込みます。
  • ライフサイクル コールバック: アニメーションと階層の変更プロセスを制御するコールバックを受け取ります。

レイアウトの変更をアニメーション化するサンプルコードについては、BasicTransition をご覧ください。

2 つのレイアウト間をアニメーション化する基本的な手順は次のとおりです。

  1. 開始レイアウトと終了レイアウトの Scene オブジェクトを作成します。ただし、多くの場合、開始レイアウトのシーンは現在のレイアウトから自動的に決定されます。
  2. Transition オブジェクトを作成して、必要なアニメーションのタイプを定義します。
  3. TransitionManager.go() を呼び出すと、アニメーションが実行され、レイアウトが入れ替えられます。

図 1 は、レイアウト、シーン、遷移、最終的なアニメーションの関係を示しています。

図 1. 遷移フレームワークがアニメーションを作成する方法を示す基本的なイラスト。

シーンを作成する

シーンには、すべてのビューとそのプロパティ値など、ビュー階層の状態が保存されます。遷移フレームワークでは、開始シーンと終了シーンの間でアニメーションを実行できます。

シーンは、レイアウト リソース ファイルまたはコード内のビューのグループから作成できます。ただし、多くの場合、遷移の開始シーンは現在の UI から自動的に決定されます。

シーンでは、シーンの変更時に実行される独自のアクションを定義することもできます。この機能は、シーンに移行した後にビュー設定をクリーンアップする場合に便利です。

レイアウト リソースからシーンを作成する

Scene インスタンスはレイアウト リソース ファイルから直接作成できます。この方法は、ファイル内のビュー階層がほぼ静的な場合に使用します。結果のシーンは、Scene インスタンスを作成したときのビュー階層の状態を表します。ビュー階層を変更した場合は、シーンを再作成します。フレームワークは、ファイル内のビュー階層全体からシーンを作成します。レイアウト ファイルの一部からシーンを作成することはできません。

レイアウト リソース ファイルから Scene インスタンスを作成するには、レイアウトからシーンルートを ViewGroup として取得します。次に、シーンのルートと、シーンのビュー階層を含むレイアウト ファイルのリソース ID を指定して、Scene.getSceneForLayout() 関数を呼び出します。

シーンのレイアウトを定義する

このセクションの残りの部分のコード スニペットでは、同じシーンルート要素を使用して 2 つの異なるシーンを作成する方法を示します。また、これらのスニペットは、互いに関連していることを示唆することなく、無関係な複数の Scene オブジェクトを読み込めることも示しています。

この例は、次のレイアウト定義で構成されています。

  • テキストラベルと子 FrameLayout を持つアクティビティのメイン レイアウト。
  • 2 つのテキスト フィールドがある最初のシーンの ConstraintLayout
  • 同じ 2 つのテキスト フィールドの順序が異なる 2 番目のシーンの 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>

2 番目のシーンのレイアウトには、同じ ID を持つ同じ 2 つのテキスト フィールドが別の順序で配置されています。次のように定義されます。

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>

レイアウトからシーンを作成する

2 つの制約レイアウトの定義を作成したら、それぞれのシーンを取得できます。これにより、2 つの UI 構成間を移行できます。シーンを取得するには、シーンルートへの参照とレイアウト リソース ID が必要です。

次のコード スニペットは、シーンルートへの参照を取得し、レイアウト ファイルから 2 つの 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);

アプリに、ビュー階層に基づく 2 つの 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 をインスタンス化することも、独自の遷移を定義することもできます。その後、終了の SceneTransitionTransitionManager.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/> 開始シーンと終了シーンのターゲット ビューの表示に関する変化を追跡し、ビューをシーンの端の 1 つから出入りさせます。

リソース ファイルから遷移インスタンスを作成する

この方法では、アクティビティのコードを変更せずに遷移の定義を変更できます。この手法は、複数の遷移を指定する方法についてのセクションで説明したように、複雑な遷移の定義をアプリコードから分離する場合にも役立ちます。

リソース ファイルに組み込みの遷移を指定する方法は次のとおりです。

  • 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);

フレームワークは、遷移インスタンスで指定されたアニメーションを実行しながら、終了シーンのビュー階層でシーンルート内のビュー階層を変更します。開始シーンは、最後の遷移の終了シーンです。前の遷移がない場合、開始シーンはユーザー インターフェースの現在の状態から自動的に決定されます。

遷移インスタンスを指定しない場合、遷移マネージャーは、ほとんどの状況で妥当な処理を行う自動遷移を適用できます。詳しくは、API リファレンスの TransitionManager クラスをご覧ください。

特定のターゲット ビューを選択する

フレームワークはデフォルトで、開始シーンと終了シーンのすべてのビューに遷移を適用します。シーン内の一部のビューにのみアニメーションを適用したい場合があります。フレームワークでは、アニメーション化する特定のビューを選択できます。たとえば、フレームワークでは ListView オブジェクトに対する変更のアニメーション化はサポートされていないため、遷移中にアニメーション化しようとしないでください。

遷移によってアニメーション化される各ビューをターゲットと呼びます。選択できるのは、シーンに関連付けられたビュー階層の一部であるターゲットのみです。

ターゲットのリストから 1 つ以上のビューを削除するには、遷移を開始する前に removeTarget() メソッドを呼び出します。指定したビューのみをターゲットのリストに追加するには、addTarget() 関数を呼び出します。詳しくは、API リファレンスの Transition クラスをご覧ください。

複数の遷移を指定する

アニメーションの効果を最大限に引き出すには、シーン間に発生する変更の種類に合わせて調整します。たとえば、シーン間で一部のビューを削除して別のビューを追加する場合、フェードアウトまたはフェードイン アニメーションによって、一部のビューが使用できなくなっていることがわかります。ビューを画面上の別のポイントに移動する場合は、移動をアニメーション化して、ユーザーがビューの新しい位置に気付けるようにすることをおすすめします。

遷移フレームワークでは、個々の組み込み遷移またはカスタム遷移のグループを含む遷移セットでアニメーション効果を組み合わせることができるため、アニメーションを 1 つだけ選択する必要はありません。

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 インスタンスと同様に遷移マネージャーで使用できます。

シーンを使用せずに遷移を適用する

ユーザー インターフェースを変更する方法は、ビュー階層を変更することだけではありません。現在の階層内で子ビューを追加、変更、削除して変更を加えることもできます。

たとえば、1 つのレイアウトで検索インタラクションを実装できます。検索入力フィールドと検索アイコンが表示されているレイアウトから始めます。結果を表示するようにユーザー インターフェースを変更するには、ユーザーがタップしたときに ViewGroup.removeView() 関数を呼び出して検索ボタンを削除し、ViewGroup.addView() 関数を呼び出して検索結果を追加します。

このアプローチは、ほぼ同一の 2 つの階層がある場合に使用できます。ユーザー インターフェースのわずかな違いのために 2 つの個別のレイアウト ファイルを作成して維持するのではなく、1 つのレイアウト ファイル(コード内で変更するビュー階層を含む)を使用できます。

この方法で現在のビュー階層内で変更する場合、シーンを作成する必要はありません。代わりに、「遅延遷移」を使用して、ビュー階層の 2 つの状態間の遷移を作成して適用できます。遷移フレームワークのこの機能は、現在のビュー階層状態から始まり、そのビューへの変更を記録して、システムがユーザー インターフェースを再描画したときに変更をアニメーション化する遷移を適用します。

1 つのビュー階層内に遅延遷移を作成する手順は次のとおりです。

  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() 関数を実装します。

詳しくは、API リファレンスの TransitionListener クラスをご覧ください。

制限事項

このセクションでは、遷移フレームワークに関する既知の制限事項をいくつか示します。

  • SurfaceView に適用されたアニメーションが正しく表示されない場合があります。SurfaceView インスタンスは UI 以外のスレッドから更新されるため、更新が他のビューのアニメーションと同期しなくなる可能性があります。
  • 特定の遷移タイプを TextureView に適用しても、期待するアニメーション効果が得られない場合があります。
  • AdapterView を拡張するクラス(ListView など)は、遷移フレームワークと互換性のない方法で子ビューを管理します。AdapterView に基づいてビューをアニメーション化しようとすると、デバイス ディスプレイが応答しなくなることがあります。
  • アニメーションで TextView のサイズを変更しようとすると、オブジェクトのサイズが完全に変更される前に、テキストが新しい位置に表示されます。この問題を回避するには、テキストを含むビューのサイズ変更をアニメーション化しないでください。