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

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

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

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

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

この方法は、ほぼ同一の 2 つの階層が存在する場合に使用できます。ユーザー インターフェースのわずかな違いのために 2 つの個別のレイアウト ファイルを作成して維持するのではなく、1 つのレイアウト ファイルにビュー階層を 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 のサイズを変更しようとすると、オブジェクトのサイズが完全に変更される前にテキストが新しい位置にポップされます。この問題を回避するには、テキストを含むビューのサイズ変更をアニメーション化しないでください。