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.
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.
- 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.
- 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.
- Le schéma Fondu fait apparaître ou disparaître en fondu des éléments d'interface utilisateur dans les limites de l'écran.
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
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 ?
Qu'attendez-vous de cet atelier de programmation ?
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
- Ouvrez le projet dans l'éditeur de votre choix.
- 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.
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.
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).
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
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
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
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
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
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.
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
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
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
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
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
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
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
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
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
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.
É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
Je souhaite réutiliser à l'avenir le système de mouvement de Material
Découvrez la galerie Flutter
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. |