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

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.

b9fd67c205755d55.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.

76622de33a19179.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.

18a525c038443492.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.

cd10a0580a159644.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 :)

Ce que vous allez faire

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

5f7b8860db2c70e2.gif

Ce dont vous avez besoin

  • Connaissances de base en Dart et en développement avec Flutter
  • Android Studio (téléchargez-le ici si vous ne l'avez pas déjà fait)
  • Un émulateur ou un appareil Android (disponible via Android Studio)
  • 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.

Avant de commencer

Pour commencer à développer des applications mobiles avec Flutter, vous devez :

  1. télécharger et installer le SDK Flutter ;
  2. mettre à jour votre PATH avec le SDK Flutter ;
  3. installer Android Studio avec les plug-ins Flutter et Dart, ou votre éditeur préféré ;
  4. installer un émulateur Android, un simulateur iOS (nécessite un Mac avec Xcode) ou utiliser un appareil physique.

Pour en savoir plus sur l'installation de Flutter, consultez la section Premiers pas : Installation. Pour configurer un éditeur, consultez la section Premiers pas : Configurer un éditeur. Lorsque vous installez un émulateur Android, n'hésitez pas à utiliser les options par défaut, comme un téléphone Pixel 3 doté de la dernière image système. Il est conseillé, mais pas obligatoire, d'activer l'accélération de la VM. Une fois les quatre étapes ci-dessus effectuées, vous pouvez revenir à l'atelier de programmation. Pour cet atelier de programmation, vous avez seulement à installer Flutter pour une plate-forme (Android ou iOS).

Vérifier que la version de votre SDK Flutter est correcte

Avant de poursuivre cet atelier de programmation, assurez-vous que la version de votre SDK est correcte. Si le SDK Flutter a déjà été installé, utilisez flutter upgrade pour vous assurer qu'il s'agit de la dernière version.

 flutter upgrade

L'exécution de flutter upgrade lance automatiquement flutter doctor.. S'il s'agit d'une nouvelle installation de Flutter et qu'aucune mise à niveau n'est nécessaire, exécutez flutter doctor manuellement. Le système vous indique si vous devez installer des dépendances pour finaliser la configuration. Vous pouvez ignorer les options qui ne vous sont pas utiles (par exemple, Xcode si vous n'avez pas l'intention d'effectuer de développement pour iOS).

 flutter doctor

Questions fréquentes

Démarrer Android Studio

Lorsque vous ouvrez Android Studio, une fenêtre de bienvenue s'affiche. Toutefois, si vous lancez Android Studio pour la première fois, suivez les étapes de l'assistant de configuration Android Studio en conservant les valeurs par défaut. Le téléchargement et l'installation des fichiers nécessaires peuvent prendre plusieurs minutes. Par conséquent, n'hésitez pas à passer à la section suivante en laissant ces opérations s'exécuter en arrière-plan.

Option 1 : Cloner l'application de départ 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épart

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

Charger le code de départ dans Android Studio

  1. Une fois l'assistant de configuration terminé et la fenêtre de bienvenue dans Android Studio affichée, cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).

e3f200327a67a53.png

  1. Accédez au répertoire dans lequel vous avez installé l'exemple de code, puis sélectionnez l'exemple de répertoire pour ouvrir le projet.
  2. Attendez qu'Android Studio crée et synchronise le projet (voir les indicateurs d'activité situés en bas de la fenêtre Android Studio).
  3. À ce stade, Android Studio peut générer des erreurs de compilation, car vous ne disposez pas du SDK Android ni de certains outils de compilation, comme celui présenté ci-dessous. Suivez les instructions fournies dans Android Studio pour installer/mettre à jour ces éléments et synchroniser votre projet. Si vous rencontrez toujours des problèmes, consultez ce guide sur la mise à jour de vos outils avec SDK Manager.

6e026ae171f5b1eb.png

  1. Si vous y êtes invité :
  • Installez les mises à jour de la plate-forme et des plug-ins ou FlutterRunConfigurationType.
  • Si le SDK Dart ou Flutter n'est pas configuré, définissez le chemin d'accès au SDK Flutter pour le plug-in Flutter.
  • Configurez les frameworks Android.
  • Cliquez sur "Get dependencies" (Obtenir les dépendances) ou sur "Run 'flutter package gets'" (Exécuter "flutter packages get").

Redémarrez ensuite Android Studio.

53b7195f1c1deedb.png

be5ce477ba09225e.png 24810642cf859588.png

Vérifier les dépendances du projet

Le projet nécessite une dépendance au package d'animations. L'exemple de code que vous avez téléchargé devrait déjà inclure cette dépendance, mais vérifiez la configuration pour en être sûr.

Accédez au fichier pubspec.yaml du module app et vérifiez si la section dependencies inclut bien une dépendance au package d'animations :

animations: ^1.1.2

Exécuter l'application de départ

  1. Assurez-vous que la configuration de compilation indiquée à gauche du choix de l'appareil est app.
  2. Appuyez sur le bouton vert pour créer et exécuter l'application.

a34cba7fab0a2af9.png

  1. Dans le menu déroulant Flutter Device Selection (Sélection des appareils Flutter) en haut de l'écran de l'éditeur, si un de vos appareils figurent déjà dans la liste de ceux disponibles, passez à l'étape 8. Sinon, cliquez sur Create New Virtual Device (Créer un appareil virtuel).
  2. Sur l'écran Select Hardware (Sélectionner le matériel), sélectionnez un appareil (par exemple, Pixel 3), puis cliquez sur Next (Suivant).
  3. Sur l'écran System Image (Image système), sélectionnez une version récente d'Android (de préférence le niveau d'API le plus élevé). Sinon, cliquez sur le lien Télécharger qui s'affiche, puis procédez au téléchargement.
  4. Cliquez sur Next (Suivant).
  5. Sur l'écran Android Virtual Device (AVD) (Appareil virtuel Android), laissez les paramètres tels quels et cliquez sur Finish (Terminer).
  6. Sélectionnez un appareil (par exemple, iPhone SE ou SDK Android conçu pour <version> dans le menu déroulant "Sélection des appareils Flutter").
  7. Appuyez sur l'icône de lecture (b8c998094aa23ac2.png).
  8. Android Studio crée l'application, la déploie et l'ouvre automatiquement sur l'appareil cible.

Et voilà ! Le code de départ de la page d'accueil de l'application Reply devrait s'exécuter dans l'émulateur. Vous devriez voir une liste d'e-mails dans la boîte de réception.

Android

iOS

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.

Android

iOS

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

Android

iOS

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})
     : assert(replyState != null),
       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) {
   assert(configuration != null);
   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 = 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({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épart, vous pouvez implémenter la première transition.

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

Android

iOS

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,
 })  : assert(id != null),
       assert(email != null),
       assert(closedChild != null);

 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 return 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(
     color: theme.cardColor,
     child: InkWell(
       onTap: () {
         Provider.of<EmailStore>(
           context,
           listen: false,
         ).currentlySelectedEmailId = id;

         mobileMailNavKey.currentState.push(
           PageRouteBuilder(
             pageBuilder: (BuildContext context, Animation<double> animation,
                 Animation<double> secondaryAnimation) {
               return MailViewPage(id: id, email: email);
             },
           ),
         );
       },
       child: 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.of(context).scaffoldBackgroundColor,
           iconColor: currentEmailStarred
               ? colorScheme.onSecondary
               : colorScheme.onBackground,
           alignment: Alignment.centerRight,
           padding: const EdgeInsetsDirectional.only(end: 20),
         ),
         child: mailPreview,
       ),
     ),
   ),
 );
}

N'oubliez pas de supprimer InkWell du widget, car sa logique fait désormais partie de la classe _OpenContainerWrapper. Vous pouvons également supprimer le widget Material, car les propriétés de couleur du widget OpenContainer définissent la couleur du conteneur qu'il contient :

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,
   }.......

À 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

Android

iOS

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

Android

iOS

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, ajoutez l'extrait de code suivant à la définition de la classe _ReplyFabState, en veillant à importer package:animations/animations.dart en haut du fichier. Vous allez encapsuler ici le widget retour de la fonction build() de la définition de la classe _ReplyFabState 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,
     shape: circleFabBorder,
     child: Tooltip(
       message: tooltip,
       child: InkWell(
         customBorder: circleFabBorder,
         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();
               },
             ),
           );
         },
         child: SizedBox(
           height: _mobileFabDimension,
           width: _mobileFabDimension,
           child: Center(
             child: fabSwitcher,
           ),
         ),
       ),
     ),
   );
 },
);

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.

Dans la définition de la classe _ReplyFabState du fichier home.dart :

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

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

Android

iOS

À 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

Android

iOS

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})
      : assert(screen != null),
        assert(transitionKey != null),
        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. Nous allons encapsuler les écrans de widget à l'aide du wrapper, de sorte qu'un chemin d'accès soit renvoyé pour le navigateur avec la transition que nous voulons. Dans la définition de la classe ReplyRouterDelegate, sous la propriété pages, au lieu d'encapsuler l'écran de recherche avec une CustomTransitionPage, utilisez à la place le nouveau wrapper :

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(
     pageBuilder: (context, animation, secondaryAnimation) {
       return const HomePage();
     },
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('search'),
       screen: const SearchPage(),
     ),
 ],
);

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

Android

iOS

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.

Pour corriger les transitions de la page d'accueil, il vous suffit d'encapsuler la HomePage avec le SharedAxisTransitionWrapper dans router.dart :

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: const HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('search'),
       screen: const 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

Android

iOS

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

Android

iOS

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 {
  FadeThroughTransitionPageWrapper({
    @required this.mailbox,
    @required this.transitionKey,
  })  : assert(mailbox != null),
        assert(transitionKey != null),
        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. Nous allons encapsuler l'écran de la boîte aux lettres à l'aide du wrapper, de sorte qu'un chemin d'accès soit renvoyé pour le navigateur avec la transition "Fondu total". 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 le nouveau wrapper :

mail_view_router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Fade through transition between different 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

Android

iOS

À 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

Android

iOS

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,
 })  : assert(fillColor != null),
       assert(child != null);

 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 widget fabSwitcher est ce qui permet au bouton d'action flottant de changer en fonction du contexte. Le fabSwitcher vérifie si nous consultons une vue par e-mail et, si c'est le cas, une icône différente est affichée pour le bouton d'action flottant.

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.

À 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

Android

iOS

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

Android

iOS

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 SizedBox(width: 8),
const _ReplyLogo(),
const SizedBox(width: 10),
// TODO: Add Fade through transition between disappearing mailbox title (Motion)
_FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: onMailView
     ? const SizedBox(height: 0, 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. Bravo !

Après

Android

iOS

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

Android

iOS

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
         ? Row(
             mainAxisSize: MainAxisSize.max,
             mainAxisAlignment: MainAxisAlignment.end,
             children: [
               IconButton(
                 icon: ImageIcon(
                   const AssetImage(
                     '$_iconAssetLocation/twotone_star.png',
                     package: _assetsPackage,
                   ),
                   color: starIconColor,
                 ),
                 onPressed: () {
                   model.starEmail(
                     model.currentlySelectedInbox,
                     model.currentlySelectedEmailId,
                   );
                   if (model.currentlySelectedInbox == 'Starred') {
                     mobileMailNavKey.currentState.pop();
                     model.currentlySelectedEmailId = -1;
                   }
                 },
                 color: ReplyColors.white50,
               ),
               IconButton(
                 icon: const ImageIcon(
                   AssetImage(
                     '$_iconAssetLocation/twotone_delete.png',
                     package: _assetsPackage,
                   ),
                 ),
                 onPressed: () {
                   model.deleteEmail(
                     model.currentlySelectedInbox,
                     model.currentlySelectedEmailId,
                   );

                   mobileMailNavKey.currentState.pop();
                   model.currentlySelectedEmailId = -1;
                 },
                 color: ReplyColors.white50,
               ),
               IconButton(
                 icon: const Icon(Icons.more_vert),
                 onPressed: () {},
                 color: ReplyColors.white50,
               ),
             ],
           )
         : Align(
             alignment: AlignmentDirectional.bottomEnd,
             child: IconButton(
               icon: const Icon(Icons.search),
               color: ReplyColors.white50,
               onPressed: () {
                 Provider.of<RouterProvider>(
                   context,
                   listen: false,
                 ).routePath = ReplySearchPath();
               },
             ),
           ),
);

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

Android

iOS

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.

Android

iOS

É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 Sans avis 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 Sans avis 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.

52f7119a30bb8f5c.png

dd11628e4c0f3fd3.png