Créer des transitions esthétiques avec le système de mouvement de Material Design pour Flutter

1. Introduction

Material Design est un système permettant de créer des produits numériques audacieux et esthétiques. Les équipes produit peuvent ainsi réaliser leurs meilleurs graphismes en combinant style, branding, interactions et animations selon des principes communs et des composants harmonieux.

logo_components_color_2x_web_96dp.png

Material Components (MDC) aide les développeurs à implémenter Material Design. Conçu par une équipe d'ingénieurs et de spécialistes de l'expérience utilisateur chez Google, MDC propose des dizaines de composants d'interface utilisateur élégants et fonctionnels. Il est disponible pour Android, iOS, le Web et Flutter.material.io/develop.

Qu'est-ce que le système de mouvement de Material Design pour Flutter ?

Ce système de mouvement désigne un ensemble de schémas de transition issus du package d'animations. Il permet d'aider les utilisateurs à comprendre et à parcourir une application, comme décrit dans les consignes Material Design.

Les quatre principaux schémas de transition sont les suivants :

  • Le schéma Transformation du conteneur opère une transition entre des éléments d'interface utilisateur qui incluent un conteneur. Il permet de faire visuellement le lien entre deux éléments distincts de l'interface en transformant de manière transparente un élément en un autre.

11807bdf36c66657.gif

  • Le schéma Axe partagé consiste en une transition entre des éléments d'interface utilisateur qui ont un lien de parenté dans l'espace ou au niveau de la navigation. Il utilise une transformation partagée sur l'axe x, y ou z pour renforcer le lien de parenté entre ces éléments.

71218f390abae07e.gif

  • Le schéma Fondu total effectue une transition entre des éléments d'interface utilisateur qui n'ont pas de lien de parenté fort. Il applique un effet de fondu régulier à l'ouverture et à la fermeture d'un élément.

385ba37b8da68969.gif

  • Le schéma Fondu fait apparaître ou disparaître en fondu des éléments d'interface utilisateur dans les limites de l'écran.

cfc40fd6e27753b6.gif

Pour ces différents schémas, le package d'animations propose des widgets de transition compilés en haut de la bibliothèque d'animations pour Flutter (flutter/animation.dart) et de la bibliothèque Material pour Flutter (flutter/material.dart) :

Dans cet atelier de programmation, vous allez opérer des transitions Material Design compilées en haut du framework Flutter et de la bibliothèque Material… ce qui signifie que vous allez utiliser ces widgets :)

Objectifs de l'atelier

Cet atelier de programmation explique comment créer des transitions en Dart dans un exemple d'application de messagerie Flutter appelée Reply. Vous verrez comment utiliser les transitions du package d'animations pour personnaliser l'apparence de votre application.

Le code de départ pour l'application Reply vous sera fourni. Vous devrez intégrer dans l'application les transitions Material suivantes que vous pouvez voir dans le fichier GIF ci-dessous :

  • Transformation du conteneur : transition entre la liste de diffusion et la page d'informations de l'e-mail
  • Transformation du conteneur : transition entre le bouton d'action flottant et la page de rédaction d'e-mail
  • Axe Z partagé : transition entre l'icône de recherche et la page de recherche
  • Fondu total : transition entre les pages de la boîte aux lettres
  • Fondu total : transition entre la page de rédaction d'e-mail et le bouton d'action flottant pour la réponse
  • Fontu total : transition entre le titre de la boîte aux lettres qui disparaît
  • Fondu total : transition entre les actions de la barre d'application inférieure

b26fe84fed12d17d.gif

Ce dont vous avez besoin

  • Connaissances de base en Dart et en développement avec Flutter
  • Un éditeur de code
  • Un appareil ou un émulateur Android/iOS
  • L'exemple de code (voir l'étape suivante)

Quel est votre niveau d'expérience en termes de création d'applications Flutter ?

Débutant Intermédiaire Expert

Qu'attendez-vous de cet atelier de programmation ?

Je suis novice en la matière et je voudrais avoir un bon aperçu. Je connais un peu le sujet, mais j'aimerais revoir certains points. Je recherche un exemple de code à utiliser dans mon projet. Je cherche des explications sur un point spécifique.

2. Configurer l'environnement de développement Flutter

Pour cet atelier, vous avez besoin de deux logiciels : le SDK Flutter et un éditeur.

Vous pouvez exécuter l'atelier de programmation sur l'un des appareils suivants :

  • Un appareil Android ou iOS physique connecté à votre ordinateur et réglé en mode développeur.
  • Le simulateur iOS (outils Xcode à installer).
  • L'émulateur Android (qui doit être configuré dans Android Studio).
  • Un navigateur (Chrome est requis pour le débogage).
  • En tant qu'application de bureau Windows, Linux ou macOS. Vous devez développer votre application sur la plate-forme où vous comptez la déployer. Par exemple, si vous voulez développer une application de bureau Windows, vous devez le faire sous Windows pour accéder à la chaîne de compilation appropriée. Prenez également connaissance des exigences spécifiques aux systèmes d'exploitation, détaillées sur docs.flutter.dev/desktop.

3. Télécharger l'application de démarrage de l'atelier de programmation

Option 1 : cloner l'application de démarrage de l'atelier de programmation depuis GitHub

Pour cloner cet atelier de programmation depuis GitHub, exécutez les commandes suivantes :

git clone https://github.com/material-components/material-components-flutter-motion-codelab.git
cd material-components-flutter-motion-codelab

Option 2 : Télécharger le fichier ZIP de l'application de départ de l'atelier de programmation

Télécharger l'application de démarrage

Elle se trouve dans le répertoire material-components-flutter-motion-codelab-starter.

Vérifier les dépendances du projet

Le projet dépend du package d'animations. Dans le fichier pubspec.yaml, vous remarquerez que la section dependencies comporte cette ligne :

animations: ^2.0.0

Ouvrir le projet et exécuter l'application

  1. Ouvrez le projet dans l'éditeur de votre choix.
  2. Suivez les instructions concernant l'éditeur que vous avez choisi et que vous trouverez au paragraphe "Run the app" (Exécuter l'application) sur la page Get Started > Test drive (Premiers pas > Faire un essai).

Opération réussie. Le code de démarrage de la page d'accueil de l'application Reply devrait s'exécuter dans l'émulateur/sur votre appareil. Vous devriez voir une liste d'e-mails dans la boîte de réception.

Page d'accueil de Reply

Facultatif : Ralentir les animations sur l'appareil

En raison de la rapidité des transitions soignées créées dans cet atelier de programmation, il peut être utile de ralentir les animations sur l'appareil pour observer certains détails précis de ces transitions lors de l'implémentation. Pour cela, appuyez sur l'icône des paramètres dans le panneau inférieur (comme illustré ci-dessous), puis sélectionnez le paramètre souhaité. Ne vous inquiétez pas ! Le ralentissement défini ici n'affecte pas les animations en dehors de l'application Reply.

d23a7bfacffac509.gif

Facultatif : Mode sombre

Si le thème clair de l'application Reply vous fait mal aux yeux, ne cherchez pas plus loin. Un paramètre intégré à l'application vous permet de remplacer ce thème par le mode sombre. Pour y accéder, appuyez sur l'icône des paramètres dans le panneau inférieur (comme illustré ci-dessous).

87618d8418eee19e.gif

4. Se familiariser avec l'exemple de code de l'application

Voyons à présent le code. Nous avons fourni une application qui utilise le package d'animations pour effectuer des transitions entre différents écrans de l'application.

  • HomePage : affiche la boîte aux lettres sélectionnée
  • InboxPage : affiche une liste d'e-mails
  • MailPreviewCard : affiche l'aperçu d'un e-mail
  • MailViewPage : affiche un seul e-mail complet
  • ComposerPage : permet de rédiger un nouvel e-mail
  • SearchPage : affiche une vue de recherche

router.dart

Tout d'abord, pour comprendre comment la navigation racine de l'application est configurée, ouvrez router.dart dans le répertoire lib :

class ReplyRouterDelegate extends RouterDelegate<ReplyRoutePath>
   with ChangeNotifier, PopNavigatorRouterDelegateMixin<ReplyRoutePath> {
 ReplyRouterDelegate({required this.replyState})
     : navigatorKey = GlobalObjectKey<NavigatorState>(replyState) {
   replyState.addListener(() {
     notifyListeners();
   });
 }

 @override
 final GlobalKey<NavigatorState> navigatorKey;

 RouterProvider replyState;

 @override
 void dispose() {
   replyState.removeListener(notifyListeners);
   super.dispose();
 }

 @override
 ReplyRoutePath get currentConfiguration => replyState.routePath!;

 @override
 Widget build(BuildContext context) {
   return MultiProvider(
     providers: [
       ChangeNotifierProvider<RouterProvider>.value(value: replyState),
     ],
     child: Selector<RouterProvider, ReplyRoutePath?>(
       selector: (context, routerProvider) => routerProvider.routePath,
       builder: (context, routePath, child) {
         return Navigator(
           key: navigatorKey,
           onPopPage: _handlePopPage,
           pages: [
             // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
             const CustomTransitionPage(
               transitionKey: ValueKey('Home'),
               screen: HomePage(),
             ),
             if (routePath is ReplySearchPath)
               const CustomTransitionPage(
                 transitionKey: ValueKey('Search'),
                 screen: SearchPage(),
               ),
           ],
         );
       },
     ),
   );
 }

 bool _handlePopPage(Route<dynamic> route, dynamic result) {
   // _handlePopPage should not be called on the home page because the
   // PopNavigatorRouterDelegateMixin will bubble up the pop to the
   // SystemNavigator if there is only one route in the navigator.
   assert(route.willHandlePopInternally ||
       replyState.routePath is ReplySearchPath);

   final bool didPop = route.didPop(result);
   if (didPop) replyState.routePath = const ReplyHomePath();
   return didPop;
 }

 @override
 Future<void> setNewRoutePath(ReplyRoutePath configuration) {
   replyState.routePath = configuration;
   return SynchronousFuture<void>(null);
 }
}

Il s'agit du navigateur racine. Il gère les écrans de l'application, qui utilisent l'intégralité du canevas (par exemple, les pages HomePage et SearchPage). Il écoute l'état de l'application pour vérifier si nous avons défini le chemin d'accès vers ReplySearchPath. Si c'est le cas, il recompile le navigateur avec la SearchPage en haut de la pile. Notez que les écrans sont encapsulés dans une CustomTransitionPage sans aucune transition définie. Cela illustre une façon de naviguer entre les écrans sans transition personnalisée.

home.dart

Nous avons défini le chemin d'accès vers ReplySearchPath dans l'état de l'application en effectuant ce qui suit à l'intérieur des _BottomAppBarActionItems dans home.dart :

Align(
 alignment: AlignmentDirectional.bottomEnd,
 child: IconButton(
   icon: const Icon(Icons.search),
   color: ReplyColors.white50,
   onPressed: () {
     Provider.of<RouterProvider>(
       context,
       listen: false,
     ).routePath = const ReplySearchPath();
   },
 ),
);

Dans le paramètre onPressed, nous accédons au RouterProvider et définissons son routePath sur ReplySearchPath. Le RouterProvider conserve une trace de l'état du navigateur racine.

mail_view_router.dart

Voyons maintenant comment la navigation interne de l'application est configurée. Ouvrez mail_view_router.dart dans le répertoire lib. Un navigateur semblable à celui ci-dessus s'affiche :

class MailViewRouterDelegate extends RouterDelegate<void>
   with ChangeNotifier, PopNavigatorRouterDelegateMixin {
 MailViewRouterDelegate({required this.drawerController});

 final AnimationController drawerController;

 @override
 Widget build(BuildContext context) {
   bool _handlePopPage(Route<dynamic> route, dynamic result) {
     return false;
   }

   return Selector<EmailStore, String>(
     selector: (context, emailStore) => emailStore.currentlySelectedInbox,
     builder: (context, currentlySelectedInbox, child) {
       return Navigator(
         key: navigatorKey,
         onPopPage: _handlePopPage,
         pages: [
           // TODO: Add Fade through transition between mailbox pages (Motion)
           CustomTransitionPage(
             transitionKey: ValueKey(currentlySelectedInbox),
             screen: InboxPage(
               destination: currentlySelectedInbox,
             ),
           )
         ],
       );
     },
   );
 }
...
}

Il s'agit du navigateur interne. Il gère les écrans internes de l'application, qui n'utilisent que le corps du canevas (la page InboxPage, par exemple). La page InboxPage affiche une liste d'e-mails en fonction de la boîte aux lettres actuelle dans l'état de l'application. Le navigateur est recompilé avec la page InboxPage correcte en haut de la pile, chaque fois que la propriété currentlySelectedInbox de l'état de l'application est modifiée.

home.dart

Nous avons défini la boîte aux lettres actuelle dans l'état de l'application en effectuant ce qui suit à l'intérieur de _HomePageState dans home.dart :

void _onDestinationSelected(String destination) {
 var emailStore = Provider.of<EmailStore>(
   context,
   listen: false,
 );

 if (emailStore.onMailView) {
   emailStore.currentlySelectedEmailId = -1;
 }

 if (emailStore.currentlySelectedInbox != destination) {
   emailStore.currentlySelectedInbox = destination;
 }

 setState(() {});
}

Dans la fonction _onDestinationSelected, nous accédons à notre EmailStore et définissons son currentlySelectedInbox sur la destination sélectionnée. Le EmailStore conserve une trace de l'état du navigation interne.

home.dart

Enfin, pour voir un exemple de chemin de navigation utilisé, ouvrez home.dart dans le répertoire lib. Repérez la classe _ReplyFabState, dans la propriété onTap du widget InkWell, qui doit se présenter comme suit :

onTap: () {
 Provider.of<EmailStore>(
   context,
   listen: false,
 ).onCompose = true;
 Navigator.of(context).push(
   PageRouteBuilder(
     pageBuilder: (
       BuildContext context,
       Animation<double> animation,
       Animation<double> secondaryAnimation,
     ) {
       return const ComposePage();
     },
   ),
 );
},

Cela illustre comment accéder à la page de rédaction d'e-mail, sans transition personnalisée. Au cours de cet atelier de programmation, vous allez voir en détail le code de l'application Reply pour configurer des transitions Material qui fonctionnent en tandem avec les différentes actions de navigation dans toute l'application.

Maintenant que vous connaissez le code de démarrage, vous pouvez implémenter la première transition.

5. Ajouter une transition "Transformation du conteneur" entre la liste de diffusion et la page d'informations de l'e-mail

Pour commencer, vous allez ajouter une transition lorsque vous cliquerez sur un e-mail. Pour ce changement, le schéma "Transformation du conteneur" convient parfaitement, car il est conçu pour les transitions entre des éléments d'interface utilisateur qui comportent un conteneur. Ce schéma permet de faire visuellement le lien entre deux éléments de ce type.

Avant d'ajouter un code, essayez d'exécuter l'application Reply et de cliquer sur un e-mail. Vous devriez avoir un plan sur plan, ce qui signifie que l'écran est remplacé sans transition :

Avant

48b00600f73c7778.gif

Commencez par ajouter une importation pour le package d'animations en haut de mail_card_preview.dart, comme indiqué dans l'extrait suivant :

mail_card_preview.dart

import 'package:animations/animations.dart';

Maintenant que vous avez une importation pour le package d'animations, vous pouvez ajouter de belles transitions à votre application. Commencez par créer une classe StatelessWidget qui accueillera le widget OpenContainer.

Dans mail_card_preview.dart, ajoutez l'extrait de code suivant après la définition de classe de la MailPreviewCard :

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
class _OpenContainerWrapper extends StatelessWidget {
 const _OpenContainerWrapper({
   required this.id,
   required this.email,
   required this.closedChild,
 });

 final int id;
 final Email email;
 final Widget closedChild;

 @override
 Widget build(BuildContext context) {
   final theme = Theme.of(context);
   return OpenContainer(
     openBuilder: (context, closedContainer) {
       return MailViewPage(id: id, email: email);
     },
     openColor: theme.cardColor,
     closedShape: const RoundedRectangleBorder(
       borderRadius: BorderRadius.all(Radius.circular(0)),
     ),
     closedElevation: 0,
     closedColor: theme.cardColor,
     closedBuilder: (context, openContainer) {
       return InkWell(
         onTap: () {
           Provider.of<EmailStore>(
             context,
             listen: false,
           ).currentlySelectedEmailId = id;
           openContainer();
         },
         child: closedChild,
       );
     },
   );
 }
}

Passons maintenant au nouveau wrapper. Dans la définition de la classe MailPreviewCard, vous allez encapsuler le widget Material de la fonction build() avec le nouveau _OpenContainerWrapper :

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
 id: id,
 email: email,
 closedChild: Material(
...

Le wrapper _OpenContainerWrapper comporte un widget InkWell et les propriétés de couleur de OpenContainer définissent la couleur du conteneur qu'il contient. Par conséquent, nous pouvons supprimer les widgets Material et Inkwell. Le code obtenu se présente comme suit :

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
 id: id,
 email: email,
 closedChild: Dismissible(
   key: ObjectKey(email),
   dismissThresholds: const {
     DismissDirection.startToEnd: 0.8,
     DismissDirection.endToStart: 0.4,
   },
   onDismissed: (direction) {
     switch (direction) {
       case DismissDirection.endToStart:
         if (onStarredInbox) {
           onStar();
         }
         break;
       case DismissDirection.startToEnd:
         onDelete();
         break;
       default:
     }
   },
   background: _DismissibleContainer(
     icon: 'twotone_delete',
     backgroundColor: colorScheme.primary,
     iconColor: ReplyColors.blue50,
     alignment: Alignment.centerLeft,
     padding: const EdgeInsetsDirectional.only(start: 20),
   ),
   confirmDismiss: (direction) async {
     if (direction == DismissDirection.endToStart) {
       if (onStarredInbox) {
         return true;
       }
       onStar();
       return false;
     } else {
       return true;
     }
   },
   secondaryBackground: _DismissibleContainer(
     icon: 'twotone_star',
     backgroundColor: currentEmailStarred
         ? colorScheme.secondary
         : theme.scaffoldBackgroundColor,
     iconColor: currentEmailStarred
         ? colorScheme.onSecondary
         : colorScheme.onBackground,
     alignment: Alignment.centerRight,
     padding: const EdgeInsetsDirectional.only(end: 20),
   ),
   child: mailPreview,
 ),
);

À ce stade, vous devriez avoir une transformation du conteneur entièrement opérationnelle. Si vous cliquez sur un e-mail, l'élément de liste s'étend pour afficher un écran de détails en même temps que la liste d'e-mails recule. Si vous appuyez sur la touche Retour, l'écran de détails est réduit en un élément de liste en même temps que la liste d'e-mails s'agrandit.

Après

663e8594319bdee3.gif

6. Ajouter une transition "Transformation du conteneur" entre le bouton d'action flottant et la page de rédaction d'e-mail

Poursuivez avec la transformation du conteneur, et ajoutez une transition entre le bouton d'action flottant et la page ComposePage depuis laquelle l'utilisateur pourra rédiger un nouvel e-mail. Tout d'abord, exécutez de nouveau l'application et cliquez sur le bouton d'action flottant pour voir qu'il n'y a pas de transition lors de l'ouverture de l'écran de rédaction d'e-mail.

Avant

4aa2befdc5170c60.gif

La façon de configurer cette transition sera très semblable à celle de l'étape précédente, dans la mesure où nous utilisons la même classe de widget (OpenContainer).

Dans home.dart, importez le package:animations/animations.dart en haut du fichier et modifiez la méthode _ReplyFabState build(). Encapsulez le widget Material renvoyé avec un widget OpenContainer :

home.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
 openBuilder: (context, closedContainer) {
   return const ComposePage();
 },
 openColor: theme.cardColor,
 onClosed: (success) {
   Provider.of<EmailStore>(
     context,
     listen: false,
   ).onCompose = false;
 },
 closedShape: circleFabBorder,
 closedColor: theme.colorScheme.secondary,
 closedElevation: 6,
 closedBuilder: (context, openContainer) {
   return Material(
     color: theme.colorScheme.secondary,
     ...

Outre les paramètres utilisés pour configurer le widget OpenContainer précédent, onClosed est désormais défini. onClosed est un ClosedCallback qui est appelé lorsque le chemin d'accès vers OpenContainer a été insérée ou est revenu à l'état fermé. La valeur renvoyée par cette transaction est transmise à cette fonction en tant qu'argument. Nous utilisons ce Callback pour informer le fournisseur de l'application que nous avons quitté le chemin d'accès ComposePage, afin qu'il puisse envoyer une notification à tous les écouteurs.

Comme nous l'avons fait à la dernière étape, nous allons supprimer le widget Material de notre widget, car le widget OpenContainer gère la couleur du widget renvoyé par le closedBuilder avec closedColor. Nous allons également supprimer l'appel Navigator.push() dans la propriété onTap du widget InkWell, puis le remplacer par le openContainer() Callback attribué par le closedBuilder du widget OpenContainer, car le widget OpenContainer gère maintenant son propre routage.

Le code obtenu se présente comme suit :

home.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
 openBuilder: (context, closedContainer) {
   return const ComposePage();
 },
 openColor: theme.cardColor,
 onClosed: (success) {
   Provider.of<EmailStore>(
     context,
     listen: false,
   ).onCompose = false;
 },
 closedShape: circleFabBorder,
 closedColor: theme.colorScheme.secondary,
 closedElevation: 6,
 closedBuilder: (context, openContainer) {
   return Tooltip(
     message: tooltip,
     child: InkWell(
       customBorder: circleFabBorder,
       onTap: () {
         Provider.of<EmailStore>(
           context,
           listen: false,
         ).onCompose = true;
         openContainer();
       },
       child: SizedBox(
         height: _mobileFabDimension,
         width: _mobileFabDimension,
         child: Center(
           child: fabSwitcher,
         ),
       ),
     ),
   );
 },
);

Effacez maintenant l'ancien code. Étant donné que le widget OpenContainer gère désormais la notification qui indique au fournisseur de l'application que nous ne sommes plus sur la ComposePage via le onClosed ClosedCallback, nous pouvons supprimer l'ancienne implémentation dans mail_view_router.dart :

mail_view_router.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
emailStore.onCompose = false; /// delete this line
return SynchronousFuture<bool>(true);

C'est tout pour cette étape ! Vous devriez avoir une transition entre le bouton d'action flottant et l'écran de rédaction semblable à celle ci-dessous :

Après

5c7ad1b4b40f9f0c.gif

7. Ajouter une transition "Axe Z partagé" entre l'icône de recherche et la page de recherche

À cette étape, nous allons ajouter une transition entre l'icône de recherche et la vue de recherche en plein écran. Comme ce changement n'implique aucun conteneur fixe, nous pouvons utiliser une transition basée sur l'axe Z partagé pour renforcer le lien de parenté dans l'espace entre les deux écrans et indiquer le déplacement d'un niveau vers le haut dans la hiérarchie de l'application.

Avant d'ajouter un code, exécutez l'application et appuyez sur l'icône de recherche en bas à droite de l'écran. L'écran de recherche devrait s'afficher sans transition.

Avant

df7683a8ad7b920e.gif

Pour commencer, accédez au fichier router.dart. Après la définition de la classe ReplySearchPath, ajoutez l'extrait suivant :

router.dart

// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
class SharedAxisTransitionPageWrapper extends Page {
 const SharedAxisTransitionPageWrapper(
     {required this.screen, required this.transitionKey})
     : super(key: transitionKey);

 final Widget screen;
 final ValueKey transitionKey;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder(
       settings: this,
       transitionsBuilder: (context, animation, secondaryAnimation, child) {
         return SharedAxisTransition(
           fillColor: Theme.of(context).cardColor,
           animation: animation,
           secondaryAnimation: secondaryAnimation,
           transitionType: SharedAxisTransitionType.scaled,
           child: child,
         );
       },
       pageBuilder: (context, animation, secondaryAnimation) {
         return screen;
       });
 }
}

Utilisons maintenant le nouveau SharedAxisTransitionPageWrapper pour effectuer la transition souhaitée. Dans la définition de la classe ReplyRouterDelegate, sous la propriété pages, nous allons encapsuler l'écran de recherche avec un SharedAxisTransitionPageWrapper au lieu d'utiliser une CustomTransitionPage :

router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
   const CustomTransitionPage(
     transitionKey: ValueKey('Home'),
     screen: HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('Search'),
       screen: SearchPage(),
     ),
 ],
);

Essayez maintenant d'exécuter de nouveau l'application.

81b3ea098926931.gif

Cela commence à avoir fière allure ! Lorsque vous cliquez sur l'icône de recherche dans la barre d'application inférieure, une transition "Axe partagé" ajuste la page de recherche en conséquence. Notez toutefois que la page d'accueil ne se réduit pas et reste statique lorsque la page de recherche s'agrandit par-dessus. En outre, lorsque vous appuyez sur le bouton "Retour", la page d'accueil n'est pas ajustée. Au lieu de cela, elle reste statique lorsque la page de recherche disparaît. Ce n'est donc pas encore fini.

Corrigeons ces deux problèmes en encapsulant aussi la HomePage avec le SharedAxisTransitionWrapper plutôt qu'avec une CustomTransitionPage :

router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
   const SharedAxisTransitionPageWrapper(
     transitionKey: ValueKey('home'),
     screen: HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('search'),
       screen: SearchPage(),
     ),
 ],
);

Et voilà ! Essayez de relancer l'application et d'appuyer sur l'icône de recherche. Les écrans d'accueil et de recherche s'estompent et s'ajustent simultanément sur l'axe Z, créant un effet fluide entre les deux écrans.

Après

462d890086a3d18a.gif

8. Ajouter une transition "Fondu total" entre les pages de la boîte aux lettres

Lors de cette étape, nous allons ajouter une transition entre différentes boîtes aux lettres. Comme nous ne voulons pas souligner un lien de parenté dans l'espace ou au niveau hiérarchique, nous allons effectuer un fondu pour opérer une transition simple entre les listes de diffusion.

Avant d'ajouter du code supplémentaire, exécutez l'application, appuyez sur le logo "Reply" dans la barre d'application inférieure et changez de boîte aux lettres. La liste d'e-mails devrait changer sans transition.

Avant

89033988ce26b92e.gif

Pour commencer, accédez au fichier mail_view_router.dart. Après la définition de la classe MailViewRouterDelegate, ajoutez l'extrait suivant :

mail_view_router.dart

// TODO: Add Fade through transition between mailbox pages (Motion)
class FadeThroughTransitionPageWrapper extends Page {
 const FadeThroughTransitionPageWrapper({
   required this.mailbox,
   required this.transitionKey,
 })  : super(key: transitionKey);

 final Widget mailbox;
 final ValueKey transitionKey;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder(
       settings: this,
       transitionsBuilder: (context, animation, secondaryAnimation, child) {
         return FadeThroughTransition(
           fillColor: Theme.of(context).scaffoldBackgroundColor,
           animation: animation,
           secondaryAnimation: secondaryAnimation,
           child: child,
         );
       },
       pageBuilder: (context, animation, secondaryAnimation) {
         return mailbox;
       });
 }
}

Comme pour la dernière étape, nous allons utiliser le nouveau FadeThroughTransitionPageWrapper pour effectuer la transition souhaitée. Dans la définition de la classe MailViewRouterDelegate, sous la propriété pages, au lieu d'encapsuler l'écran de la boîte aux lettres avec une CustomTransitionPage, utilisez à la place FadeThroughTransitionPageWrapper :

mail_view_router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Fade through transition between mailbox pages (Motion)
   FadeThroughTransitionPageWrapper(
     mailbox: InboxPage(destination: currentlySelectedInbox),
     transitionKey: ValueKey(currentlySelectedInbox),
   ),
 ],
);

Exécutez de nouveau l'application. Lorsque vous ouvrez le panneau de navigation inférieur et que vous changez les boîtes aux lettres, la liste actuelle d'e-mails devrait disparaître en fondu et se réduire en même temps que la nouvelle liste apparaît en fondu et s'agrandit. Bravo !

Après

8186940082b630d.gif

9. Ajouter une transition "Fondu total" entre la page de rédaction d'e-mail et le bouton d'action flottant pour la réponse

À cette étape, nous allons ajouter une transition entre différentes icônes d'un bouton d'action flottant. Comme nous ne voulons pas mettre l'accent sur un lien de parenté dans l'espace ou au niveau hiérarchique, nous allons effectuer un fondu total pour opérer une transition simple entre les icônes du bouton d'action flottant.

Avant d'ajouter du code supplémentaire, exécutez l'application, appuyez sur un e-mail et ouvrez la vue par e-mail. L'icône du bouton d'action flottant devrait changer sans transition.

Avant

d8e3afa0447cfc20.gif

Pour le reste de cet atelier de programmation, nous allons travailler dans home.dart. Si vous n'arrivez pas à ajouter l'importation pour le package d'animations comme nous l'avons déjà fait pour home.dart, retournez à l'étape 2.

La manière dont nous allons configurer les prochaines transitions sera très similaire, car elles feront toutes appel à une classe réutilisable (_FadeThroughTransitionSwitcher).

Dans home.dart, ajoutons l'extrait suivant sous _ReplyFabState :

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
class _FadeThroughTransitionSwitcher extends StatelessWidget {
 const _FadeThroughTransitionSwitcher({
   required this.fillColor,
   required this.child,
 });

 final Widget child;
 final Color fillColor;

 @override
 Widget build(BuildContext context) {
   return PageTransitionSwitcher(
     transitionBuilder: (child, animation, secondaryAnimation) {
       return FadeThroughTransition(
         fillColor: fillColor,
         child: child,
         animation: animation,
         secondaryAnimation: secondaryAnimation,
       );
     },
     child: child,
   );
 }
}

Dans _ReplyFabState, recherchez maintenant le widget fabSwitcher. Le fabSwitcher renvoie une icône différente selon qu'il se trouve ou non dans la vue par e-mail. Encapsulez-le avec _FadeThroughTransitionSwitcher :

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
static final fabKey = UniqueKey();
static const double _mobileFabDimension = 56;

@override
Widget build(BuildContext context) {
 final theme = Theme.of(context);
 final circleFabBorder = const CircleBorder();

 return Selector<EmailStore, bool>(
   selector: (context, emailStore) => emailStore.onMailView,
   builder: (context, onMailView, child) {
     // TODO: Add Fade through transition between compose and reply FAB (Motion)
     final fabSwitcher = _FadeThroughTransitionSwitcher(
       fillColor: Colors.transparent,
       child: onMailView
           ? Icon(
               Icons.reply_all,
               key: fabKey,
               color: Colors.black,
             )
           : const Icon(
               Icons.create,
               color: Colors.black,
             ),
     );
...

Nous attribuons au _FadeThroughTransitionSwitcher une fillColor transparente. Il n'y a donc pas d'arrière-plan entre les éléments lors de la transition. Nous créons également une UniqueKey que nous attribuons à l'une des icônes.

À ce stade, vous devriez avoir un bouton d'action flottant contextuel et entièrement animé. Lorsque vous accédez à une vue par e-mail, l'ancien icône du bouton d'action flottant se réduit en fondu jusqu'à disparaître, tandis que la nouvelle s'agrandit en fondu.

Après

c55bacd9a144ec69.gif

10. Ajouter une transition "Fondu total" entre le titre de la boîte aux lettres qui disparaît

Lors de cette étape, nous allons ajouter une transition "Fondu total" de sorte que le titre de la boîte aux lettres apparaisse ou disparaisse lorsque vous consultez une vue par e-mail. Comme nous ne voulons pas mettre l'accent sur un lien de parenté dans l'espace ou au niveau hiérarchique, nous allons effectuer un fondu total pour opérer une transition simple entre le widget Text qui englobe le titre de la boîte aux lettres et une SizedBox vide.

Avant d'ajouter du code supplémentaire, exécutez l'application, appuyez sur un e-mail et ouvrez la vue par e-mail. Le titre de la boîte aux lettres devrait disparaître sans transition.

Avant

59eb57a6c71725c0.gif

Le reste de cet atelier de programmation sera rapide, car nous avons déjà effectué la majeure partie du travail dans la classe _FadeThroughTransitionSwitcher lors de la dernière étape.

Passons maintenant à la classe _AnimatedBottomAppBar dans home.dart pour ajouter une transition. Nous allons réutiliser la classe _FadeThroughTransitionSwitcher de la dernière étape et encapsuler la onMailView conditionnelle qui renvoie une SizedBox vide ou un titre de boîte aux lettres qui s'agrandit de façon synchronisée avec le panneau inférieur :

home.dart

...
const _ReplyLogo(),
const SizedBox(width: 10),
// TODO: Add Fade through transition between disappearing mailbox title (Motion)
_FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: onMailView
     ? const SizedBox(width: 48)
     : FadeTransition(
         opacity: fadeOut,
         child: Selector<EmailStore, String>(
           selector: (context, emailStore) =>
               emailStore.currentlySelectedInbox,
           builder: (
             context,
             currentlySelectedInbox,
             child,
           ) {
             return Text(
               currentlySelectedInbox,
               style: Theme.of(context)
                   .textTheme
                   .bodyText1!
                   .copyWith(
                     color: ReplyColors.white50,
                   ),
             );
           },
         ),
       ),
),

Voilà ! Nous avons terminé cette étape.

Exécutez de nouveau l'application. Lorsque vous ouvrez un e-mail et que vous êtes redirigé vers la vue par e-mail, le titre de la boîte aux lettres dans la barre d'application inférieure doit se réduire en fondu et disparaître. Parfait !

Après

3f1a3db01a481124.gif

11. Ajouter une transition "Fondu total" entre les actions de la barre d'application inférieure

Lors de cette étape, nous allons ajouter une transition "Fondu total" afin qu'il y ait une transition entre les actions de la barre d'application inférieure en fonction du contexte des applications. Comme nous ne voulons pas mettre l'accent sur un lien de parenté dans l'espace ou au niveau hiérarchique, nous allons effectuer un fondu total pour opérer une transition simple entre les actions de la barre d'application inférieure lorsque l'application est sur la page d'accueil, quand le panneau inférieure est visible, et lorsque nous consultons la vue par e-mail.

Avant d'ajouter du code supplémentaire, exécutez l'application, appuyez sur un e-mail et ouvrez la vue par e-mail. Vous pouvez également appuyer sur le logo "Reply". Les actions de la barre d'application inférieure devraient changer sans transition.

Avant

5f662eac19fce3ed.gif

Comme à la dernière étape, nous allons réutiliser _FadeThroughTransitionSwitcher. Pour effectuer la transition souhaitée, accédez à la définition de la classe _BottomAppBarActionItems et encapsulez le widget de retour de la fonction build() avec _FadeThroughTransitionSwitcher :

home.dart

// TODO: Add Fade through transition between bottom app bar actions (Motion)
return _FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: drawerVisible
     ? Align(
         key: UniqueKey(),
         alignment: AlignmentDirectional.bottomEnd,
         child: IconButton(
           icon: const Icon(Icons.settings),
           color: ReplyColors.white50,
           onPressed: () async {
             drawerController.reverse();
             showModalBottomSheet(
               context: context,
               shape: RoundedRectangleBorder(
                 borderRadius: modalBorder,
               ),
               builder: (context) => const SettingsBottomSheet(),
             );
           },
         ),
       )
     : onMailView
...

Faisons maintenant un essai ! Lorsque vous ouvrez un e-mail et que vous êtes redirigé vers la vue par e-mail, les anciennes actions de la barre d'application inférieure doivent se réduire en fondu jusqu'à disparaître en même temps que les nouvelles s'agrandissent en fondu. Bravo !

Après

cff0fa2afa1c5a7f.gif

12. Félicitations !

En moins de 100 lignes de code Dart, le package d'animations vous a aidé à créer de magnifiques transitions dans une application existante, conforme aux consignes Material Design, et avec une apparence et un comportement cohérents sur tous les appareils.

d5637de49eb64d8a.gif

Étapes suivantes

Pour en savoir plus sur le système de mouvement de Material, consultez cette page et la documentation complète pour les développeurs, tout essayant d'ajouter des transitions Material à votre application.

Merci d'avoir essayé ce système. Nous espérons que cet atelier de programmation vous a plu.

La réalisation de cet atelier de programmation m'a demandé un temps et des efforts raisonnables

Tout à fait d'accord D'accord Ni d'accord, ni pas d'accord Pas d'accord Pas du tout d'accord

Je souhaite réutiliser à l'avenir le système de mouvement de Material

Tout à fait d'accord D'accord Ni d'accord, ni pas d'accord Pas d'accord Pas du tout d'accord

Pour consulter d'autres démos sur l'utilisation des widgets fournis par la bibliothèque Material Flutter, ainsi que sur le framework Flutter, accédez à la Galerie Flutter.

46ba920f17198998.png

6ae8ae284bf4f9fa.png