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

Android の遷移フレームワークでは、開始レイアウトと終了レイアウトを指定するだけで、UI のあらゆる種類のモーションをアニメーション化できます。 必要なアニメーションの種類(ビューのフェードイン / アウト、ビューのサイズ変更など)を選択できるほか、遷移フレームワークによって開始レイアウトから終了レイアウトまでのアニメーション化の方法を特定できます。

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

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

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

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

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

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

図 1. 遷移フレームワークでアニメーションが作成される仕組みを示す概略図

シーンを作成する

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

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

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

注: シーンを使用せずに遷移を適用するで説明されているように、遷移フレームワークではシーンを使用せずに単一のビュー階層内の変更をアニメーション化できます。ただし、シーンの理解は、遷移を処理するうえで不可欠です。

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

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

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

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

このセクションの残りのコード スニペットで、同じシーンルート要素を使用して 2 つの異なるシーンを作成する方法を示します。また、相互関連性を暗示せずに、関連性のない複数の Scene オブジェクトを読み込む方法も示しています。

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

  • テキストラベルと子レイアウトを持つアクティビティのメイン レイアウト。
  • 最初のシーン(2 つのテキスト フィールドを持つ)の相対レイアウト。
  • 2 番目のシーン(同じ 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>
    

このレイアウト定義には、テキスト フィールドと、シーンルートの子レイアウトが含まれています。最初のシーンのレイアウトは、メインのレイアウト ファイルに含まれます。これにより、フレームワークはレイアウト ファイル全体のみをシーンに読み込むことができるため、アプリで初期ユーザー インターフェースの一部としてレイアウトを表示し、シーンに読み込むこともできます。

最初のシーンのレイアウトは次のように定義されます。

res/layout/a_scene.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scene_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
            android:id="@+id/text_view1"
            android:text="Text Line 1" />
        <TextView
            android:id="@+id/text_view2"
            android:text="Text Line 2" />
    </RelativeLayout>
    

2 番目のシーンのレイアウトには、同じ 2 つのテキスト フィールド(同じ ID を持つ)が異なる順序で配置されます。定義は次のとおりです。

res/layout/another_scene.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scene_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
            android:id="@+id/text_view2"
            android:text="Text Line 2" />
        <TextView
            android:id="@+id/text_view1"
            android:text="Text Line 1" />
    </RelativeLayout>
    

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

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

    

このアプリには、ビュー階層に基づく Scene オブジェクトが 2 つあります。どちらのシーンも、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 オブジェクトで表します。AutoTransition および Fade などの組み込みのサブクラスを使用して Transition をインスタンス化することも、独自の遷移を定義することもできます。その後、終了 SceneTransitionTransitionManager.go() に渡すことで、シーン間のアニメーションを実行できます。

遷移ライフサイクルはアクティビティ ライフサイクルと似ており、アニメーションの開始と終了の間にフレームワークが監視する遷移状態を表します。重要なライフサイクル状態では、フレームワークはコールバック関数を呼び出すので、この関数を実装することで、遷移の各段階でユーザー インターフェースを調整できます。

遷移を作成する

前のセクションでは、さまざまなビュー階層の状態を表すシーンの作成方法について説明しました。変更する開始シーンと終了シーンを定義したら、アニメーションを定義する Transition オブジェクトを作成する必要があります。このフレームワークでは、組み込みの遷移をリソース ファイルに指定し、コードでインフレートしたり、組み込みの遷移のインスタンスを直接コードで作成したりできます。

表 1. 組み込みの遷移タイプ。

クラス タグ 属性 効果
AutoTransition <autoTransition/> - デフォルトの遷移。ビューのフェードアウト、移動、サイズ変更、フェードインをこの順序で行います。
Fade <fade/> android:fadingMode="[fade_in |
fade_out |
fade_in_out]"
fade_in: ビューのフェードインを行います。
fade_out: ビューのフェードアウトを行います。
fade_in_out: (デフォルト)fade_out に続いて fade_in を行います。
ChangeBounds <changeBounds/> - ビューの移動とサイズ変更を行います。

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

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

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

  1. res/transition/ ディレクトリをプロジェクトに追加します。
  2. このディレクトリ内に新しい XML リソース ファイルを作成します。
  3. 組み込みの遷移のいずれかに 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);

    

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

遷移インスタンスを指定しない場合、遷移マネージャーにより、ほとんどの状況に適した自動遷移を適用できます。詳細については、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

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        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" />
        ...
    </RelativeLayout>
    

次のスニペットは、テキストビューの追加をアニメーション化するコードを示しています。

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 fade: 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 will animate the addition as a fade in

    

遷移ライフサイクルのコールバックを定義する

遷移ライフサイクルは、アクティビティ ライフサイクルと似ています。これは TransitionManager.go() 関数の呼び出しからアニメーション終了までの間に、フレームワークが監視する遷移状態を表します。重要なライフサイクル状態では、フレームワークは TransitionListener インターフェースで定義されたコールバックを呼び出します。

遷移ライフサイクル コールバックは、シーンの変更中にビュー プロパティ値を開始ビュー階層から終了ビュー階層にコピーする場合などに役に立ちます。終了ビュー階層は遷移が完了するまでインフレートしないため、単純に開始ビューから終了ビュー階層のビューに値をコピーすることはできません。 代わりに、値を変数に保存し、フレームワークで遷移が完了したときに終了ビュー階層にコピーする必要があります。遷移が完了したときに通知を受け取るには、アクティビティに TransitionListener.onTransitionEnd() 関数を実装します。

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

制限事項

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

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