アニメーションを使用してフラグメント間を移動する

Fragment API には、モーション エフェクトと変換を使用して、ナビゲーション中にフラグメントを視覚的に接続する方法が 2 つあります。その 1 つがアニメーション フレームワークで、AnimationAnimator の両方を使用します。もう 1 つは、共有要素の遷移を含む遷移フレームワークです。

フラグメントの開始と終了、フラグメント間の共有要素の遷移には、カスタム エフェクトを指定できます。

  • enter エフェクトは、フラグメントがどのように画面に入るかを決定します。たとえば、画面に移動してきたときに画面の端からフラグメントをスライドインさせるためのエフェクトを作成できます。
  • exit エフェクトは、フラグメントがどのように画面から出るかを決定します。たとえば、画面から離れていくときにフラグメントをフェードアウトするためのエフェクトを作成できます。
  • 共有要素の遷移は、2 つのフラグメント間で共有されているビューが、それらのフラグメント間でどのように移動するかを決定します。たとえば、フラグメント A の ImageView に表示されている画像は、フラグメント B が表示されると B に遷移します。

アニメーションを設定する

まず、新しいフラグメントに移動したときに実行される enter エフェクトと exit エフェクトのためのアニメーションを作成する必要があります。アニメーションは、トゥイーン アニメーション リソースとして定義できます。これらのリソースを使用して、アニメーション中のフラグメントの回転、拡大、フェード、移動の方法を定義できます。たとえば、現在のフラグメントをフェードアウトして、新しいフラグメントを画面の右端からスライドインさせることができます。図 1 をご覧ください。

アニメーションの開始と終了。現在のフラグメントがフェードアウトしている間に、次のフラグメントが右からスライドインする。
図 1. アニメーションの開始と終了。現在のフラグメントがフェードアウトしている間に、次のフラグメントが右からスライドインします。

これらのアニメーションは、res/anim ディレクトリで定義できます。

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />
<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

また、バックスタックをポップすると実行される enter エフェクトと exit エフェクトのアニメーションを指定することもできます。このアニメーションは、ユーザーが [上へ] ボタンまたは [戻る] ボタンをタップしたときに表示させることができます。これらを popEnter アニメーションと popExit アニメーションと呼びます。たとえば、ユーザーが前の画面に戻るときに、現在のフラグメントを画面の右端からスライドアウトさせて前のフラグメントをフェードインさせることができます。

popEnter アニメーションと popExit アニメーション。前のフラグメントがフェードインしている間に、現在のフラグメントが画面から右にスライドアウトする。
図 2. popEnter アニメーションと popExit アニメーション。前のフラグメントがフェードインしている間に、現在のフラグメントが画面から右にスライドアウトします。

これらのアニメーションは次のように定義できます。

<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />
<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

定義したアニメーションを FragmentTransaction.setCustomAnimations() を呼び出して使用し、そのリソース ID でアニメーション リソースに渡します。次の例をご覧ください。

Kotlin

supportFragmentManager.commit {
    setCustomAnimations(
        R.anim.slide_in, // enter
        R.anim.fade_out, // exit
        R.anim.fade_in, // popEnter
        R.anim.slide_out // popExit
    )
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(
        R.anim.slide_in,  // enter
        R.anim.fade_out,  // exit
        R.anim.fade_in,   // popEnter
        R.anim.slide_out  // popExit
    )
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

遷移を設定する

遷移を使用して enter エフェクトと exit エフェクトを定義することもできます。これらの遷移は、XML リソース ファイルで定義できます。たとえば、現在のフラグメントをフェードアウトし、新しいフラグメントを画面の右端からスライドインさせるとします。これらの遷移は次のように定義できます。

<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>
<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />

遷移を定義したら、開始フラグメントで setEnterTransition() を、終了フラグメントで setExitTransition() を呼び出して適用し、そのリソース ID でインフレートされた遷移リソースを渡します。次の例をご覧ください。

Kotlin

class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setExitTransition(inflater.inflateTransition(R.transition.fade));
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setEnterTransition(inflater.inflateTransition(R.transition.slide_right));
    }
}

フラグメントは AndroidX の遷移をサポートしています。 フラグメントはフレームワークの遷移もサポートしていますが、AndroidX の遷移を使用することを強くおすすめします。AndroidX の遷移は API レベル 14 以上でサポートされており、古いバージョンのフレームワークの遷移にはないバグ修正を含んでいるためです。

共有要素の遷移を使用する

遷移フレームワークの一部である共有要素の遷移は、フラグメントの遷移中に対応するビューが 2 つのフラグメント間をどのように移動するかを決定します。たとえば、図 3 に示すように、フラグメント A の ImageView に表示される画像を、フラグメント B が表示されたときに B に遷移させることができます。

共有要素を使用したフラグメントの遷移。
図 3. 共有要素を使用したフラグメントの遷移。

共有要素を使用してフラグメントの遷移を行う手順の概要は次のとおりです。

  1. 各共有要素ビューに一意の遷移名を割り当てます。
  2. FragmentTransaction に共有要素ビューと遷移名を追加します。
  3. 共有要素の遷移アニメーションを設定します。

まず、各共有要素ビューに一意の遷移名を割り当て、ビューをあるフラグメントから次のフラグメントにマッピングできるようにする必要があります。ViewCompat.setTransitionName() を使用して、各フラグメント レイアウトの共有要素の遷移名を設定します。これにより、API レベル 14 以上で互換性が確保されます。たとえば、フラグメント A と B の ImageView の遷移名を次のように割り当てることができます。

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val itemImageView = view.findViewById<ImageView>(R.id.item_image)
        ViewCompat.setTransitionName(itemImageView, “item_image”)
    }
}

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
        ViewCompat.setTransitionName(heroImageView, “hero_image”)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView itemImageView = view.findViewById(R.id.item_image);
        ViewCompat.setTransitionName(itemImageView, “item_image”);
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView heroImageView = view.findViewById(R.id.hero_image);
        ViewCompat.setTransitionName(heroImageView, “hero_image”);
    }
}

フラグメントの遷移に共有要素を含めるには、FragmentTransaction でそれぞれの共有要素ビューがフラグメント間でどのようにマッピングされるかを把握する必要があります。FragmentTransaction.addSharedElement() を呼び出して、ビューと対応するビューの遷移名を次のフラグメントに渡すことで、各共有要素を FragmentTransaction に追加します。次の例をご覧ください。

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(...)
    addSharedElement(itemImageView, “hero_image”)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(...)
    .addSharedElement(itemImageView, “hero_image”)
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

共有要素がフラグメント間を遷移する方法を指定するには、移動先のフラグメントに enter 遷移を設定する必要があります。フラグメントの onCreate() メソッドで Fragment.setSharedElementEnterTransition() を呼び出します。次の例をご覧ください。

Kotlin

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
             .inflateTransition(R.transition.shared_image)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Transition transition = TransitionInflater.from(requireContext())
            .inflateTransition(R.transition.shared_image);
        setSharedElementEnterTransition(transition);
    }
}

shared_image 遷移は次のように定義されます。

<!-- res/transition/shared_image.xml -->
<transitionSet>
    <changeImageTransform />
</transitionSet>

共有要素の遷移として、Transition のすべてのサブクラスがサポートされています。カスタム Transition を作成する場合は、カスタムの遷移アニメーションの作成をご覧ください。先の例で使用した changeImageTransform は、利用可能な事前ビルド済み変換の 1 つです。Transition クラスの API リファレンスには、その他の Transition サブクラスもあります。

デフォルトでは、共有要素の enter 遷移は、共有要素の return 遷移としても使用されます。return 遷移は、フラグメント トランザクションがバックスタックからポップされたときに、共有要素がどのように前のフラグメントに戻るかを決定します。別の return 遷移を指定するには、フラグメントの onCreate() メソッドで Fragment.setSharedElementReturnTransition() を使用します。

予測型後方互換性

予測型「戻る」は、すべてではありませんが、多くのフラグメント間アニメーションで使用できます。予測型「戻る」を実装する場合は、次の点に注意してください。

  • Transitions 1.5.0 以降と Fragments 1.7.0 以降をインポートします。
  • Animator クラスとサブクラス、AndroidX Transition ライブラリがサポートされています。
  • Animation クラスとフレームワーク Transition ライブラリはサポートされていません。
  • 予測フラグメント アニメーションは、Android 14 以降を搭載したデバイスでのみ機能します。
  • 予測型「戻る」では、setCustomAnimationssetEnterTransitionsetExitTransitionsetReenterTransitionsetReturnTransitionsetSharedElementEnterTransitionsetSharedElementReturnTransition がサポートされています。

詳細については、予測型「戻る」アニメーションのサポートを追加するをご覧ください。

遷移を延期する

場合によっては、フラグメントの遷移を短時間延期する必要が生じることもあります。たとえば、Android が遷移の開始状態と終了状態を正確に取得できるよう、開始フラグメント内のすべてのビューが測定、配置されるまで待機しなければならない、といった場合です。

また、必要なデータが読み込まれるまで遷移を延期しなければならない、ということもあります。たとえば、共有要素の画像が読み込まれるまで待機が必要になることがあります。待機しないと、遷移中や遷移後に画像の読み込みが完了したときに遷移がゆがむ可能性があります。

遷移を延期するには、まずフラグメント トランザクションでフラグメントの状態の変化を並べ替えられるようにする必要があります。フラグメントの状態の変化を並べ替えられるようにするには、FragmentTransaction.setReorderingAllowed() を呼び出します。次の例をご覧ください。

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setReorderingAllowed(true)
    setCustomAnimation(...)
    addSharedElement(view, view.transitionName)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setReorderingAllowed(true)
    .setCustomAnimations(...)
    .addSharedElement(view, view.getTransitionName())
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

開始遷移を延期するには、開始フラグメントの onViewCreated() メソッドで Fragment.postponeEnterTransition() を呼び出します。

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        postponeEnterTransition()
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        postponeEnterTransition();
    }
}

データを読み込み、遷移を開始する準備ができたら、Fragment.startPostponedEnterTransition() を呼び出します。次の例では、Glide ライブラリを使用して画像を共有 ImageView に読み込み、画像の読み込みが完了するまで対応する遷移を延期します。

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        Glide.with(this)
            .load(url)
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(...) {
                    startPostponedEnterTransition();
                    return false;
                }

                @Override
                public boolean onResourceReady(...) {
                    startPostponedEnterTransition();
                    return false;
                }
            })
            .into(headerImage)
    }
}

ユーザーのインターネット接続が低速の場合などは、すべてのデータが読み込まれるのを待つのではなく、一定時間が経過した後に延期された遷移を開始した方がよい場合もあります。このような場合は、代わりに開始フラグメントの onViewCreated() メソッドで Fragment.postponeEnterTransition(long, TimeUnit) を呼び出して、継続時間と時間の単位を渡します。そうすると、指定された時間が経過したときに、延期された遷移が自動的に開始されます。

RecyclerView とともに共有要素の遷移を使用する

延期された開始遷移は、開始フラグメント内のすべてのビューが測定されて配置されるまで開始しないでください。RecyclerView を使用する場合は、データが読み込まれ、RecyclerView のアイテムが描画可能な状態になるまで待ってから遷移を開始する必要があります。次に例を示します。

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // Wait for the data to load
        viewModel.data.observe(viewLifecycleOwner) {
            // Set the data on the RecyclerView adapter
            adapter.setData(it)
            // Start the transition once all views have been
            // measured and laid out
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        postponeEnterTransition();

        final ViewGroup parentView = (ViewGroup) view.getParent();
        // Wait for the data to load
        viewModel.getData()
            .observe(getViewLifecycleOwner(), new Observer<List<String>>() {
                @Override
                public void onChanged(List<String> list) {
                    // Set the data on the RecyclerView adapter
                    adapter.setData(it);
                    // Start the transition once all views have been
                    // measured and laid out
                    parentView.getViewTreeObserver()
                        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                            @Override
                            public boolean onPreDraw(){
                                parentView.getViewTreeObserver()
                                        .removeOnPreDrawListener(this);
                                startPostponedEnterTransition();
                                return true;
                            }
                    });
                }
        });
    }
}

ViewTreeObserver.OnPreDrawListener がフラグメント ビューの親に設定されていることに注意してください。これは、すべてのフラグメントのビューが測定、配置され、描画可能な状態になってから、延期された開始遷移を開始するためです。

RecyclerView で共有要素の遷移を使用する場合のもう 1 つのポイントは、RecyclerView アイテムの XML レイアウトで遷移名を設定できないことです。これは、任意の数のアイテムがそのレイアウトを共有するためです。遷移アニメーションが正しいビューを使用するように、一意の遷移名を割り当てる必要があります。

各アイテムの共有要素に一意の遷移名を付けるには、ViewHolder がバインドされたときにアイテムに一意の遷移名を割り当てます。たとえば、各アイテムデータに一意の ID が含まれている場合、そのアイテムを遷移名として使用することもできます。次の例をご覧ください。

Kotlin

class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}

Java

public class ExampleViewHolder extends RecyclerView.ViewHolder {
    private final ImageView image;

    ExampleViewHolder(View itemView) {
        super(itemView);
        image = itemView.findViewById(R.id.item_image);
    }

    public void bind(String id) {
        ViewCompat.setTransitionName(image, id);
        ...
    }
}

参考情報

フラグメントの遷移について詳しくは、以下の参考情報をご覧ください。

サンプル

ブログ投稿