Animer les changements de mise en page à l'aide d'une transition

Essayer Compose
Jetpack Compose est le kit d'outils d'UI recommandé pour Android. Découvrez comment utiliser les animations dans Compose.

Le framework de transition d'Android vous permet d'animer toutes sortes de mouvements dans votre UI en fournissant les mises en page de début et de fin. Vous pouvez sélectionner le type d'animation souhaité (par exemple, pour faire apparaître ou disparaître des vues, ou pour modifier la taille des vues). Le framework de transition détermine alors comment animer la transition de la mise en page de départ à la mise en page de fin.

Le framework de transition inclut les fonctionnalités suivantes :

  • Animations au niveau du groupe : appliquez des effets d'animation à toutes les vues d'une hiérarchie de vues.
  • Animations intégrées : utilisez des animations prédéfinies pour des effets courants tels que le fondu ou le mouvement.
  • Compatibilité avec les fichiers de ressources : chargez les hiérarchies de vues et les animations intégrées à partir des fichiers de ressources de mise en page.
  • Rappels de cycle de vie : recevez des rappels qui vous permettent de contrôler le processus de modification de l'animation et de la hiérarchie.

Pour animer le passage d'une mise en page à une autre, suivez la procédure de base ci-dessous :

  1. Créez un objet Scene pour les mises en page de début et de fin. Toutefois, la scène de la mise en page de départ est souvent déterminée automatiquement à partir de la mise en page actuelle.
  2. Créez un objet Transition pour définir le type d'animation souhaité.
  3. Appelez TransitionManager.go(). Le système exécute l'animation pour inverser les mises en page.

Le diagramme de la figure 1 illustre la relation entre vos mises en page, les scènes, la transition et l'animation finale.

Figure 1. Illustration de base montrant comment le framework de transition crée une animation.

Créer une scène

Les scènes stockent l'état d'une hiérarchie de vues, y compris toutes ses vues et leurs valeurs de propriété. Le framework de transitions peut exécuter des animations entre une scène de début et une scène de fin.

Vous pouvez créer vos scènes à partir d'un fichier de ressources de mise en page ou à partir d'un groupe de vues dans votre code. Toutefois, la scène de départ de votre transition est souvent déterminée automatiquement à partir de l'UI actuelle.

Une scène peut également définir ses propres actions qui s'exécutent lorsque vous changez de scène. Cette fonctionnalité est utile pour nettoyer les paramètres de vue après votre transition vers une scène.

Créer une scène à partir d'une ressource de mise en page

Vous pouvez créer une instance Scene directement à partir d'un fichier de ressources de mise en page. Utilisez cette technique lorsque la hiérarchie des vues dans le fichier est principalement statique. La scène résultante représente l'état de la hiérarchie des vues au moment où vous avez créé l'instance Scene. Si vous modifiez la hiérarchie des vues, recréez la scène. Le framework crée la scène à partir de l'ensemble de la hiérarchie des vues du fichier. Vous ne pouvez pas créer de scène à partir d'une partie d'un fichier de mise en page.

Pour créer une instance Scene à partir d'un fichier de ressources de mise en page, récupérez la racine de la scène à partir de votre mise en page en tant que ViewGroup. Appelez ensuite la fonction Scene.getSceneForLayout() avec la racine de la scène et l'ID de ressource du fichier de mise en page qui contient la hiérarchie des vues de la scène.

Définir des mises en page pour les scènes

Les extraits de code du reste de cette section montrent comment créer deux scènes différentes avec le même élément racine de scène. Les extraits montrent également que vous pouvez charger plusieurs objets Scene sans rapport sans impliquer qu'ils sont liés les uns aux autres.

L'exemple se compose des définitions de mise en page suivantes :

  • Mise en page principale d'une activité avec un libellé de texte et un enfant FrameLayout.
  • Un ConstraintLayout pour la première scène avec deux champs de texte.
  • Un ConstraintLayout pour la deuxième scène avec les deux mêmes champs de texte dans un ordre différent.

L'exemple est conçu de sorte que toute l'animation se produise dans la mise en page enfant de la mise en page principale de l'activité. Le libellé de texte dans la mise en page principale reste statique.

La mise en page principale de l'activité est définie comme suit :

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>

Cette définition de mise en page contient un champ de texte et un FrameLayout enfant pour la racine de la scène. La mise en page de la première scène est incluse dans le fichier de mise en page principal. Cela permet à l'application de l'afficher dans l'interface utilisateur initiale et de la charger dans une scène, car le framework ne peut charger qu'un fichier de mise en page entier dans une scène.

La mise en page de la première scène est définie comme suit :

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" >
    <TextView
        android:id="@+id/text_view1"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 1"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
    <TextView
        android:id="@+id/text_view2"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 2"
        app:layout_constraintTop_toBottomOf="@id/text_view1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

La mise en page de la deuxième scène contient les deux mêmes champs de texte, avec les mêmes ID, mais placés dans un ordre différent. Elle est définie comme suit :

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" >
    <TextView
        android:id="@+id/text_view2"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 2"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
    <TextView
        android:id="@+id/text_view1"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Text Line 1"
        app:layout_constraintTop_toBottomOf="@id/text_view2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Générer des scènes à partir de mises en page

Après avoir créé des définitions pour les deux mises en page de contraintes, vous pouvez obtenir une scène pour chacune d'elles. Cela vous permet de passer d'une configuration d'UI à l'autre. Pour obtenir une scène, vous avez besoin d'une référence à la racine de la scène et à l'ID de ressource de mise en page.

L'extrait de code suivant montre comment obtenir une référence à la racine de la scène et créer deux objets Scene à partir des fichiers de mise en page :

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

Dans l'application, il existe désormais deux objets Scene basés sur des hiérarchies de vues. Les deux scènes utilisent la racine de scène définie par l'élément FrameLayout dans res/layout/activity_main.xml.

Créer une scène dans votre code

Vous pouvez également créer une instance Scene dans votre code à partir d'un objet ViewGroup. Utilisez cette technique lorsque vous modifiez les hiérarchies de vues directement dans votre code ou lorsque vous les générez de manière dynamique.

Pour créer une scène à partir d'une hiérarchie de vues dans votre code, utilisez le constructeur Scene(sceneRoot, viewHierarchy). Appeler ce constructeur équivaut à appeler la fonction Scene.getSceneForLayout() lorsque vous avez déjà gonflé un fichier de mise en page.

L'extrait de code suivant montre comment créer une instance Scene à partir de l'élément racine de la scène et de la hiérarchie des vues pour la scène dans votre code :

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

Créer des actions de scène

Le framework vous permet de définir des actions de scène personnalisées que le système exécute lorsque vous entrez dans une scène ou que vous en sortez. Dans de nombreux cas, il n'est pas nécessaire de définir des actions de scène personnalisées, car le framework anime automatiquement le changement entre les scènes.

Les actions de scène sont utiles pour gérer les cas suivants :

  • Pour animer des vues qui ne se trouvent pas dans la même hiérarchie. Vous pouvez animer des vues pour les scènes de début et de fin à l'aide d'actions de scène d'entrée et de sortie.
  • Pour animer les vues que le framework de transitions ne peut pas animer automatiquement, comme les objets ListView. Pour en savoir plus, consultez la section Limites.

Pour fournir des actions de scène personnalisées, définissez vos actions en tant qu'objets Runnable et transmettez-les aux fonctions Scene.setExitAction() ou Scene.setEnterAction(). Le framework appelle la fonction setExitAction() sur la scène de départ avant d'exécuter l'animation de transition, et la fonction setEnterAction() sur la scène de fin après l'exécution de l'animation de transition.

Appliquer une transition

Le framework de transition représente le style d'animation entre les scènes avec un objet Transition. Vous pouvez instancier un Transition à l'aide de sous-classes intégrées, telles que AutoTransition et Fade, ou définir votre propre transition. Vous pouvez ensuite exécuter l'animation entre les scènes en transmettant votre Scene de fin et le Transition à TransitionManager.go().

Le cycle de vie de la transition est semblable au cycle de vie d'une activité. Il représente les états de transition que le framework surveille entre le début et la fin d'une animation. À des états importants du cycle de vie, le framework appelle des fonctions de rappel que vous pouvez implémenter pour ajuster votre interface utilisateur à différentes phases de la transition.

Créer une transition

La section précédente montre comment créer des scènes qui représentent l'état de différentes hiérarchies de vues. Une fois que vous avez défini les scènes de début et de fin entre lesquelles vous souhaitez basculer, créez un objet Transition qui définit une animation. Le framework vous permet de spécifier une transition intégrée dans un fichier de ressources et de l'étendre dans votre code, ou de créer une instance d'une transition intégrée directement dans votre code.

Tableau 1. Types de transitions intégrés.

Classe Tag Effet
AutoTransition <autoTransition/> Transition par défaut. Les vues s'estompent, se déplacent et se redimensionnent, puis réapparaissent, dans cet ordre.
ChangeBounds <changeBounds/> Déplace et redimensionne les vues.
ChangeClipBounds <changeClipBounds/> Capture le View.getClipBounds() avant et après le changement de scène, et anime ces changements pendant la transition.
ChangeImageTransform <changeImageTransform/> Capture la matrice d'un ImageView avant et après le changement de scène, et l'anime pendant la transition.
ChangeScroll <changeScroll/> Capture les propriétés de défilement des cibles avant et après le changement de scène, et anime les éventuelles modifications.
ChangeTransform <changeTransform/> Capture l'échelle et la rotation des vues avant et après le changement de scène, et anime ces changements pendant la transition.
Explode <explode/> Suit les modifications de la visibilité des vues cibles dans les scènes de début et de fin, et déplace les vues vers l'intérieur ou l'extérieur des bords de la scène.
Fade <fade/> fade_in apparaît progressivement dans les vues.
fade_out estompe les vues.
fade_in_out (par défaut) effectue un fade_out suivi d'un fade_in.
Slide <slide/> Suit les modifications de la visibilité des vues cibles dans les scènes de début et de fin, et déplace les vues vers l'intérieur ou l'extérieur depuis l'un des bords de la scène.

Créer une instance de transition à partir d'un fichier de ressources

Cette technique vous permet de modifier la définition de votre transition sans changer le code de votre activité. Cette technique est également utile pour séparer les définitions de transitions complexes du code de votre application, comme indiqué dans la section sur la spécification de plusieurs transitions.

Pour spécifier une transition intégrée dans un fichier de ressources, procédez comme suit :

  • Ajoutez le répertoire res/transition/ à votre projet.
  • Créez un fichier de ressources XML dans ce répertoire.
  • Ajoutez un nœud XML pour l'une des transitions intégrées.

Par exemple, le fichier de ressources suivant spécifie la transition Fade :

res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

L'extrait de code suivant montre comment gonfler une instance Transition dans votre activité à partir d'un fichier de ressources :

Kotlin

var fadeTransition: Transition =
    TransitionInflater.from(this)
                      .inflateTransition(R.transition.fade_transition)

Java

Transition fadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

Créer une instance de transition dans votre code

Cette technique est utile pour créer des objets de transition de manière dynamique si vous modifiez l'interface utilisateur dans votre code, et pour créer des instances de transition intégrées simples avec peu ou pas de paramètres.

Pour créer une instance d'une transition intégrée, appelez l'un des constructeurs publics dans les sous-classes de la classe Transition. Par exemple, l'extrait de code suivant crée une instance de la transition Fade :

Kotlin

var fadeTransition: Transition = Fade()

Java

Transition fadeTransition = new Fade();

Appliquer une transition

En général, vous appliquez une transition pour passer d'une hiérarchie de vues à une autre en réponse à un événement, tel qu'une action de l'utilisateur. Par exemple, prenons une application de recherche : lorsque l'utilisateur saisit un terme de recherche et appuie sur le bouton de recherche, l'application passe à une scène qui représente la mise en page des résultats tout en appliquant une transition qui fait disparaître le bouton de recherche et apparaître les résultats de recherche.

Pour effectuer un changement de scène tout en appliquant une transition en réponse à un événement dans votre activité, appelez la fonction de classe TransitionManager.go() avec la scène de fin et l'instance de transition à utiliser pour l'animation, comme indiqué dans l'extrait suivant :

Kotlin

TransitionManager.go(endingScene, fadeTransition)

Java

TransitionManager.go(endingScene, fadeTransition);

Le framework modifie la hiérarchie des vues à l'intérieur de la racine de la scène avec la hiérarchie des vues de la scène de fin lors de l'exécution de l'animation spécifiée par l'instance de transition. La scène de départ est la scène de fin de la dernière transition. S'il n'y a pas de transition précédente, la scène de départ est déterminée automatiquement à partir de l'état actuel de l'interface utilisateur.

Si vous ne spécifiez pas d'instance de transition, le gestionnaire de transitions peut appliquer une transition automatique qui fait quelque chose de raisonnable dans la plupart des situations. Pour en savoir plus, consultez la documentation de référence de l'API pour la classe TransitionManager.

Choisir des vues cibles spécifiques

Par défaut, le framework applique des transitions à toutes les vues des scènes de début et de fin. Dans certains cas, vous ne souhaiterez peut-être appliquer une animation qu'à un sous-ensemble de vues dans une scène. Le framework vous permet de sélectionner les vues spécifiques que vous souhaitez animer. Par exemple, le framework n'est pas compatible avec l'animation des modifications apportées aux objets ListView. N'essayez donc pas de les animer lors d'une transition.

Chaque vue animée par la transition est appelée cible. Vous ne pouvez sélectionner que les cibles qui font partie de la hiérarchie des vues associée à une scène.

Pour supprimer une ou plusieurs vues de la liste des cibles, appelez la méthode removeTarget() avant de commencer la transition. Pour ajouter uniquement les vues que vous spécifiez à la liste des cibles, appelez la fonction addTarget(). Pour en savoir plus, consultez la documentation de référence de l'API pour la classe Transition.

Spécifier plusieurs transitions

Pour que l'animation ait le plus d'impact possible, faites-la correspondre au type de changements qui se produisent entre les scènes. Par exemple, si vous supprimez certaines vues et en ajoutez d'autres entre les scènes, une animation de fondu entrant ou sortant indique clairement que certaines vues ne sont plus disponibles. Si vous déplacez des vues vers différents points de l'écran, il est préférable d'animer le mouvement afin que les utilisateurs remarquent le nouvel emplacement des vues.

Vous n'avez pas à choisir une seule animation, car le framework de transitions vous permet de combiner des effets d'animation dans un ensemble de transitions qui contient un groupe de transitions individuelles intégrées ou personnalisées.

Pour définir un ensemble de transitions à partir d'une collection de transitions en XML, créez un fichier de ressources dans le répertoire res/transitions/ et listez les transitions sous l'élément TransitionSet. Par exemple, l'extrait suivant montre comment spécifier un ensemble de transitions qui a le même comportement que la classe 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>

Pour développer l'ensemble de transitions en un objet TransitionSet dans votre code, appelez la fonction TransitionInflater.from() dans votre activité. La classe TransitionSet étend la classe Transition. Vous pouvez donc l'utiliser avec un gestionnaire de transitions comme n'importe quelle autre instance Transition.

Appliquer une transition sans scènes

Modifier les hiérarchies de vues n'est pas la seule façon de modifier votre interface utilisateur. Vous pouvez également apporter des modifications en ajoutant, en modifiant et en supprimant des vues enfants dans la hiérarchie actuelle.

Par exemple, vous pouvez implémenter une interaction de recherche avec une seule mise en page. Commencez par la mise en page affichant un champ de saisie de recherche et une icône de recherche. Pour modifier l'interface utilisateur afin d'afficher les résultats, supprimez le bouton de recherche lorsque l'utilisateur appuie dessus en appelant la fonction ViewGroup.removeView() et ajoutez les résultats de recherche en appelant la fonction ViewGroup.addView().

Vous pouvez utiliser cette approche si l'alternative consiste à avoir deux hiérarchies presque identiques. Au lieu de créer et de gérer deux fichiers de mise en page distincts pour une différence mineure dans l'interface utilisateur, vous pouvez avoir un fichier de mise en page contenant une hiérarchie de vues que vous modifiez dans le code.

Si vous apportez des modifications à la hiérarchie de la vue actuelle de cette manière, vous n'avez pas besoin de créer de scène. À la place, vous pouvez créer et appliquer une transition entre deux états d'une hiérarchie de vues à l'aide d'une transition différée. Cette fonctionnalité du framework de transitions commence par l'état actuel de la hiérarchie des vues, enregistre les modifications que vous apportez à ses vues et applique une transition qui anime les modifications lorsque le système redessine l'interface utilisateur.

Pour créer une transition différée dans une même hiérarchie de vues, procédez comme suit :

  1. Lorsque l'événement qui déclenche la transition se produit, appelez la fonction TransitionManager.beginDelayedTransition() en fournissant la vue parente de toutes les vues que vous souhaitez modifier et la transition à utiliser. Le framework stocke l'état actuel des vues enfants et leurs valeurs de propriété.
  2. Modifiez les vues enfants selon les besoins de votre cas d'utilisation. Le framework enregistre les modifications que vous apportez aux vues enfants et à leurs propriétés.
  3. Lorsque le système redessine l'interface utilisateur en fonction de vos modifications, le framework anime les changements entre l'état d'origine et le nouvel état.

L'exemple suivant montre comment animer l'ajout d'une vue de texte à une hiérarchie de vues à l'aide d'une transition différée. Le premier extrait montre le fichier de définition de la mise en page :

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>

L'extrait suivant montre le code qui anime l'ajout de la vue de texte :

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.

Définir les rappels du cycle de vie de la transition

Le cycle de vie de la transition est semblable au cycle de vie d'une activité. Il représente les états de transition que le framework surveille pendant la période comprise entre un appel à la fonction TransitionManager.go() et la fin de l'animation. À des états de cycle de vie importants, le framework appelle des rappels définis par l'interface TransitionListener.

Les rappels de cycle de vie de transition sont utiles, par exemple, pour copier une valeur de propriété de vue de la hiérarchie de vues de départ vers la hiérarchie de vues de fin lors d'un changement de scène. Vous ne pouvez pas simplement copier la valeur de la vue de départ vers la vue de la hiérarchie de vues de fin, car la hiérarchie de vues de fin n'est pas gonflée tant que la transition n'est pas terminée. Au lieu de cela, vous devez stocker la valeur dans une variable, puis la copier dans la hiérarchie de vues de fin une fois que le framework a terminé la transition. Pour être averti lorsque la transition est terminée, implémentez la fonction TransitionListener.onTransitionEnd() dans votre activité.

Pour en savoir plus, consultez la documentation de référence de l'API pour la classe TransitionListener.

Limites

Cette section répertorie certaines limites connues du framework de transitions :

  • Les animations appliquées à un SurfaceView peuvent ne pas s'afficher correctement. Les instances SurfaceView sont mises à jour à partir d'un thread non UI. Les mises à jour peuvent donc être désynchronisées avec les animations des autres vues.
  • Certains types de transitions spécifiques peuvent ne pas produire l'effet d'animation souhaité lorsqu'ils sont appliqués à un TextureView.
  • Les classes qui étendent AdapterView, telles que ListView, gèrent leurs vues enfants de manière incompatible avec le framework de transitions. Si vous essayez d'animer une vue basée sur AdapterView, l'écran de l'appareil peut cesser de répondre.
  • Si vous essayez de redimensionner un TextView avec une animation, le texte se déplace vers un nouvel emplacement avant que l'objet ne soit complètement redimensionné. Pour éviter ce problème, n'animez pas le redimensionnement des vues contenant du texte.