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

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/> 開始シーンと終了シーンにおけるターゲット ビューの表示状態の変化を追跡し、シーンの一方の端からビューを出入りします。

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

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

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

  • 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. 遷移をトリガーするイベントが発生したら、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 のサイズを変更しようとすると、オブジェクトのサイズが完全に変更される前に、テキストが新しい位置にポップされます。この問題を回避するには、テキストを含むビューのサイズ変更をアニメーション化しないでください。