Parcourir les fragments à l'aide d'animations

L'API Fragment offre deux façons d'utiliser des effets de mouvement et des transformations pour connecter visuellement les fragments pendant la navigation. L'une d'elles est le framework d'animation, qui utilise à la fois Animation et Animator. L'autre est le framework de transition, qui inclut les transitions d'éléments partagés.

Vous pouvez spécifier des effets personnalisés pour l'entrée et la sortie de fragments, ainsi que pour les transitions d'éléments partagés entre les fragments.

  • Un effet d'entrée détermine la façon dont un fragment entre dans l'écran. Par exemple, vous pouvez créer un effet pour faire glisser le fragment depuis le bord de l'écran lorsque vous y accédez.
  • Un effet de sortie détermine la manière dont un fragment quitte l'écran. Par exemple, vous pouvez créer un effet pour faire disparaître le fragment en fondu lorsque vous le quittez.
  • Une transition d'élément partagé détermine la façon dont une vue partagée entre deux fragments se déplace de l'un à l'autre. Par exemple, une image affichée dans un élément ImageView dans le fragment A passe au fragment B une fois que B devient visible.

Définir des animations

Tout d'abord, vous devez créer les animations pour les effets d'entrée et de sortie. Elles s'exécuteront à chaque accès à un nouveau fragment. Vous pouvez définir ces animations en tant que ressources d'animation d'interpolation. Ces ressources vous permettent de définir la manière dont les fragments doivent pivoter, s'étirer, se fondre et se déplacer pendant l'animation. Par exemple, vous souhaitez peut-être que le fragment actuel disparaisse en fondu et que le nouveau fragment glisse de droite à gauche, comme illustré dans la figure 1.

Animations d'entrée et de sortie. Le fragment actuel disparaît en fondu lorsque le fragment suivant glisse de droite à gauche.
Figure 1. Animations d'entrée et de sortie. Le fragment actuel disparaît lorsque le fragment suivant glisse de droite à gauche.

Ces animations peuvent être définies dans le répertoire 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%" />

Vous pouvez également spécifier des animations avec les effets d'entrée et de sortie qui s'exécutent lors de l'affichage de la pile "Retour", ce qui peut se produire lorsque l'utilisateur appuie sur le bouton "Haut" ou "Retour". C'est ce que l'on appelle des animations popEnter et popExit. Par exemple, lorsqu'un utilisateur revient à un écran précédent, vous souhaitez peut-être que le fragment actuel glisse vers la droite, puis que le fragment précédent apparaisse en fondu.

Animations popEnter et popExit. Le fragment actuel quitte l&#39;écran en glissant vers la droite tandis que le fragment précédent apparaît en fondu.
Figure 2. Animations popEnter et popExit. Le fragment actuel quitte l'écran en glissant vers la droite tandis que le fragment précédent apparaît en fondu.

Ces animations peuvent être définies comme suit :

<!-- 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" />

Pour utiliser vos animations après les avoir définies, appelez FragmentTransaction.setCustomAnimations() en transmettant vos ressources d'animation par ID de ressource, comme illustré dans l'exemple suivant :

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

Définir des transitions

Vous pouvez également utiliser des transitions pour définir des effets d'entrée et de sortie. Ces transitions peuvent être définies dans des fichiers de ressources XML. Par exemple, vous souhaitez peut-être que le fragment actuel disparaisse en fondu et que le nouveau fragment glisse à partir du bord droit de l'écran. Ces transitions peuvent être définies comme suit :

<!-- 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" />

Pour appliquer vos transitions après les avoir définies, appelez la méthode setEnterTransition() sur le fragment d'entrée et setExitTransition() sur le fragment de sortie en transmettant vos ressources de transition gonflées par leur ID de ressource, comme illustré dans l'exemple suivant :

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

Les fragments sont compatibles avec les transitions AndroidX. Bien que les fragments soient également compatibles avec les transitions du framework, nous vous recommandons vivement d'utiliser les transitions AndroidX, car elles sont compatibles avec les niveaux d'API 14 ou version ultérieure, et contiennent des corrections de bugs qui ne sont pas présentes dans les anciennes versions de transitions du framework.

Utiliser des transitions d'éléments partagés

Dans le cadre du framework de transition, les transitions d'éléments partagés déterminent la manière dont les vues correspondantes se déplacent entre deux fragments lors d'une transition de fragment. Par exemple, vous souhaitez peut-être qu'une image affichée dans un élément ImageView du fragment A passe au fragment B une fois que B est visible, comme illustré dans la figure 3.

Transition de fragment avec un élément partagé.
Figure 3 : Transition de fragment avec un élément partagé.

Dans les grandes lignes, voici comment effectuer une transition de fragment avec des éléments partagés :

  1. Attribuez un nom de transition unique à chaque vue d'élément partagé.
  2. Ajoutez des vues d'éléments partagés et des noms de transition à FragmentTransaction.
  3. Définissez une animation de transition d'élément partagé.

Tout d'abord, vous devez attribuer un nom de transition unique à chaque vue d'élément partagé pour permettre le mappage des vues d'un fragment avec le suivant. Définissez un nom de transition au niveau des éléments partagés dans chaque mise en page de fragment à l'aide de ViewCompat.setTransitionName(), qui est compatible avec les niveaux d'API 14 et supérieurs. Par exemple, le nom de transition d'un élément ImageView dans les fragments A et B peut être attribué comme suit :

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

Pour inclure vos éléments partagés dans la transition de fragments, FragmentTransaction doit savoir comment les vues de chaque élément partagé sont mappées d'un fragment au suivant. Ajoutez chacun de vos éléments partagés à FragmentTransaction. Pour ce faire, appelez FragmentTransaction.addSharedElement(), en transmettant la vue et le nom de transition de la vue correspondante dans le fragment suivant, comme illustré dans l'exemple ci-dessous :

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

Pour spécifier la manière dont vos éléments partagés passent d'un fragment au suivant, vous devez définir une transition d'entrée au niveau du fragment cible. Appelez Fragment.setSharedElementEnterTransition() dans la méthode onCreate() du fragment, comme illustré dans l'exemple suivant :

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

La transition shared_image est définie comme suit :

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

Toutes les sous-classes de Transition sont compatibles en tant que transitions d'éléments partagés. Si vous souhaitez créer une Transition personnalisée, consultez Créer une animation de transition personnalisée. changeImageTransform, utilisé dans l'exemple précédent, est l'une des translations prédéfinies disponibles. Vous trouverez d'autres sous-classes Transition dans la documentation de référence de l'API dédiée à la classe Transition.

Par défaut, la transition d'entrée de l'élément partagé est également utilisée comme transition de retour pour les éléments partagés. La transition de retour détermine la manière dont les éléments partagés reviennent au fragment précédent lorsque la transaction du fragment est déclenchée depuis la pile "Retour". Si vous souhaitez spécifier une autre transition de retour, utilisez Fragment.setSharedElementReturnTransition() dans la méthode onCreate() du fragment.

Compatibilité avec la prévisualisation du Retour

Vous pouvez utiliser la prévisualisation du Retour avec de nombreuses animations multifragments. Lorsque vous implémentez la prévisualisation du Retour, gardez à l'esprit les points suivants:

  • Importez Transitions 1.5.0 ou une version ultérieure, et Fragments 1.7.0 ou une version ultérieure.
  • La classe et les sous-classes Animator et la bibliothèque de transition AndroidX sont compatibles.
  • La classe Animation et la bibliothèque Transition du framework ne sont pas compatibles.
  • Les animations de fragment prédictive ne fonctionnent que sur les appareils équipés d'Android 14 ou version ultérieure.
  • setCustomAnimations, setEnterTransition, setExitTransition, setReenterTransition, setReturnTransition, setSharedElementEnterTransition et setSharedElementReturnTransition sont compatibles avec la prévisualisation du Retour.

Pour en savoir plus, consultez Ajout de la prise en charge des animations pour la prévisualisation du Retour.

Différer des transitions

Dans certains cas, vous devrez peut-être différer une transition de fragment pour une courte période. Par exemple, vous devrez peut-être attendre que toutes les vues du fragment entrant soient mesurées et disposées afin qu'Android puisse capturer avec précision leurs états de début et de fin pour la transition.

Il se peut aussi que votre transition doive être différée tant que certaines données nécessaires n'ont pas été chargées. Par exemple, vous devez attendre le chargement de toutes images pour les éléments partagés. Dans le cas contraire, la transition risque d'être discordante si le chargement d'une image se termine pendant ou après la transition.

Pour différer une transition, vous devez d'abord vous assurer que la transaction de fragment permet de réorganiser les changements d'état de fragment. Pour autoriser le changement d'état des fragments, appelez FragmentTransaction.setReorderingAllowed(), comme indiqué dans l'exemple suivant :

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

Pour différer la transition d'entrée, appelez Fragment.postponeEnterTransition() dans la méthode onViewCreated() du fragment entrant :

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

Une fois que vous avez chargé les données et que vous êtes prêt à démarrer la transition, appelez Fragment.startPostponedEnterTransition(). L'exemple suivant utilise la bibliothèque Glide pour charger une image dans un élément ImageView partagé. La transition correspondante est ainsi différée jusqu'à la fin du chargement de l'image.

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

Dans les cas de figure où la connexion Internet d'un utilisateur est lente, par exemple, il peut être utile de lancer la transition différée après un certain délai au lieu d'attendre le chargement de toutes les données. Si tel est le cas, vous pouvez appeler Fragment.postponeEnterTransition(long, TimeUnit) dans la méthode onViewCreated() du fragment entrant, en indiquant la durée souhaitée et l'unité de temps. La transition différée sera automatiquement lancée une fois le délai spécifié écoulé.

Utiliser des transitions d'éléments partagés avec un élément RecyclerView

Les transitions d'entrée différées ne doivent pas commencer tant que toutes les vues du fragment d'entrée n'ont pas été mesurées et disposées. Lorsque vous utilisez un objet RecyclerView, vous devez attendre que les données se chargent et que les éléments RecyclerView soient prêts à être dessinés avant de lancer la transition. Exemple :

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

Notez qu'un objet ViewTreeObserver.OnPreDrawListener est défini sur le parent de la vue du fragment. De la sorte, toutes les vues du fragment ont été mesurées et disposées, et sont donc prêtes à être dessinées, avant le début de la transition d'entrée diffusée.

Un autre point à prendre en compte lorsque vous utilisez des transitions d'éléments partagés avec un élément RecyclerView est que vous ne pouvez pas définir le nom de la transition dans la mise en page XML de l'élément RecyclerView, car un nombre arbitraire d'éléments la partage. Un nom de transition unique doit être attribué afin que l'animation de transition utilise la vue appropriée.

Vous pouvez attribuer un nom de transition unique à chaque élément partagé en lorsque ViewHolder est relié. Par exemple, si les données de chaque élément incluent un ID unique, vous pouvez les utiliser comme nom de transition, comme illustré dans l'exemple suivant :

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

Ressources supplémentaires

Pour en savoir plus sur les transitions de fragment, consultez les ressources supplémentaires suivantes.

Exemples

Articles de blog