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

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 番目のシーンの ConstraintLayout(同じ 2 つのテキスト フィールドを持つが、配置順が異なる)。

この例では、アクティビティのメイン レイアウトの子レイアウト内で、すべてのアニメーションを生成しています。メイン レイアウトのテキストラベルは静的なままです。

アクティビティのメイン レイアウトは次のように定義されます。

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 番目のシーンのレイアウトには、同じ 2 つのテキスト フィールド(同じ 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>

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

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.xml 内の FrameLayout 要素で定義されたシーンルートを使用します。

コードでシーンを作成する

また、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/> 開始シーンと終了シーンのターゲット ビューの可視性の変化をトラッキングし、シーンの端からビューを移動したり、端へ移動したりします。

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

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

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

  • 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 クラスのサブクラスでパブリック コンストラクタの 1 つを呼び出します。たとえば、次のコード スニペットでは 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 オブジェクトに対する変更のアニメーション化はサポートされていないため、遷移中にアニメーション化しようとしないでください。

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

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

複数の遷移を指定する

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

遷移フレームワークを使用すると、個々の組み込み遷移やカスタム遷移のグループを含む遷移セットでアニメーション効果を組み合わせることができるため、アニメーションを 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 インスタンスと同様に遷移マネージャーで使用できます。

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

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

たとえば、単一のレイアウトで検索インタラクションを実装できます。検索入力フィールドと検索アイコンを表示するレイアウトから始めます。結果を表示するようにユーザー インターフェースを変更するには、ユーザーが検索ボタンをタップしたときに 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() 関数を実装します。

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

制限事項

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

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