Creare splendide transizioni con Material Motion per Flutter

1. Introduzione

Material Design è un sistema per la creazione di prodotti digitali belli e audaci. Unendo stile, branding, interazione e movimento in un insieme coerente di principi e componenti, i team di prodotto possono realizzare il loro massimo potenziale di progettazione.

logo_components_color_2x_web_96dp.png

Material Components (MDC) consente agli sviluppatori di implementare Material Design. Creato dal team di ingegneri e designer UX di Google, MDC è dotato di decine di componenti UI belli e funzionali ed è disponibile per Android, iOS, web e Flutter.material.io/develop

Qual è il sistema di movimento di Material per Flutter?

Il sistema di movimento Material per Flutter è un insieme di modelli di transizione all'interno del pacchetto di animazioni che possono aiutare gli utenti a comprendere e navigare all'interno di un'app, come descritto nelle linee guida di Material Design.

I quattro principali modelli di transizione Material sono i seguenti:

  • Trasformazione container: transizioni tra elementi UI che includono un contenitore. crea una connessione visibile tra due elementi distinti dell'interfaccia utente trasformando in modo fluido un elemento in un altro.

11807bdf36c66657.gif

  • Asse condiviso: transizioni tra elementi UI che hanno una relazione spaziale o di navigazione. utilizza una trasformazione condivisa sugli assi x, y o z per rafforzare la relazione tra gli elementi.

71218f390abae07e.gif

  • Dissolvenza attraverso: transizioni tra elementi UI che non hanno una forte relazione tra loro. utilizza una dissolvenza in entrata e in uscita sequenziale, con una scala dell'elemento in entrata.

385ba37b8da68969.gif

  • Dissolvenza: utilizzata per gli elementi UI che entrano o escono entro i margini dello schermo.

cfc40fd6e27753b6.gif

Il pacchetto di animazioni offre widget di transizione per questi pattern, creati sopra sia la raccolta di animazioni Flutter (flutter/animation.dart) sia la raccolta di materiali Flutter (flutter/material.dart):

In questo codelab, utilizzerai le transizioni Material basate sul framework Flutter e sulla libreria Material, il che significa che dovrai gestire i widget. :)

Cosa creerai

Questo codelab ti guiderà nella creazione di alcune transizioni in un'app email di Flutter di esempio chiamata Rispondi, utilizzando Dart, per dimostrare come utilizzare le transizioni del pacchetto di animazioni per personalizzare l'aspetto e il design della tua app.

Ti verrà fornito il codice di avvio per l'app di Reply e incorporerai nell'app le seguenti transizioni di tipo Material, che puoi vedere nella GIF completa del codelab qui sotto:

  • Transizione di Container Transform dalla mailing list alla pagina dei dettagli delle email
  • Transizione di Container Transform da FAB alla pagina di scrittura delle email
  • Transizione sull'asse Z condivisa dall'icona di ricerca alla pagina di visualizzazione della ricerca
  • Transizione Dissolvenza attraverso tra le pagine delle caselle di posta
  • Transizione Fade Through tra scrittura e risposta FAB
  • Transizione Dissolvenza attraverso tra il titolo della casella di posta scomparso
  • Transizione Dissolvenza attraverso tra le azioni della barra delle app in basso

b26fe84fed12d17d.gif

Che cosa ti serve

  • Conoscenza di base dello sviluppo di Flutter e di Dart
  • Un editor di codice
  • Un emulatore o un dispositivo Android/iOS
  • Il codice di esempio (vedi il passaggio successivo)

Come giudichi il tuo livello di esperienza nella creazione di app Flutter?

Principiante Livello intermedio Eccellente

Cosa ti piacerebbe imparare da questo codelab?

Non ho mai affrontato questo argomento e vorrei una panoramica completa. So qualcosa su questo argomento, ma vorrei rinfrescarti un po'. Sto cercando un codice di esempio da utilizzare nel mio progetto. Vorrei avere una spiegazione su qualcosa di specifico.

2. Configura l'ambiente di sviluppo di Flutter

Per completare questo lab sono necessari due software: l'SDK Flutter e l'editor.

Puoi eseguire il codelab utilizzando uno di questi dispositivi:

  • Un dispositivo fisico Android o iOS connesso al computer e impostato sulla modalità sviluppatore.
  • Il simulatore iOS (richiede l'installazione degli strumenti Xcode).
  • L'emulatore Android (richiede la configurazione in Android Studio).
  • Un browser (per il debug è richiesto Chrome).
  • Come applicazione desktop Windows, Linux o macOS. Devi svilupparle sulla piattaforma in cui prevedi di eseguire il deployment. Quindi, se vuoi sviluppare un'app desktop per Windows, devi sviluppare su Windows per accedere alla catena di build appropriata. Alcuni requisiti specifici del sistema operativo sono descritti in dettaglio all'indirizzo docs.flutter.dev/desktop.

3. Scarica l'app iniziale del codelab

Opzione 1: clona l'app codelab iniziale da GitHub

Per clonare questo codelab da GitHub, esegui questi comandi:

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

Opzione 2: scarica il file ZIP dell'app codelab iniziale

L'app iniziale si trova nella directory material-components-flutter-motion-codelab-starter.

Verifica le dipendenze del progetto

Il progetto dipende dal pacchetto di animazioni. Nella sezione pubspec.yaml, noterai che la sezione dependencies include quanto segue:

animations: ^2.0.0

Apri il progetto ed esegui l'app

  1. Apri il progetto nell'editor che preferisci.
  2. Segui le istruzioni per "Esegui l'app". in Inizia: prova l'editor che hai scelto.

Operazione riuscita. Il codice di avvio per la home page di Reply dovrebbe essere eseguito sul tuo dispositivo/emulatore. Dovresti vedere la Posta in arrivo contenente un elenco di email.

Home page Rispondi

(Facoltativo) Rallentare le animazioni del dispositivo

Dal momento che questo codelab prevede transizioni rapide, ma molto raffinate, può essere utile rallentare le animazioni del dispositivo per osservare alcuni dettagli più minuti delle transizioni durante l'implementazione. A questo scopo, usa un'impostazione in-app, accessibile toccando l'icona delle impostazioni quando il riquadro a scomparsa in basso è aperto. Non preoccuparti, questo metodo di rallentamento delle animazioni dei dispositivi non influirà sulle animazioni sul dispositivo al di fuori dell'app Reply.

d23a7bfacffac509.gif

(Facoltativo) Modalità Buio

Se il tema luminoso di Rispondi ti fa male agli occhi, non cercare oltre. È inclusa un'impostazione in-app che ti consente di cambiare il tema dell'app in modalità Buio per adattarlo meglio ai tuoi occhi. Per accedere a questa impostazione, tocca l'icona delle impostazioni quando il riquadro a scomparsa in basso è aperto.

87618d8418eee19e.gif

4. Acquisisci familiarità con il codice dell'app di esempio

Diamo un'occhiata al codice. Abbiamo fornito un'app che utilizza il pacchetto di animazioni per passare da una schermata all'altra all'interno dell'applicazione.

  • Home page: visualizza la casella di posta selezionata
  • InboxPage: mostra un elenco di email
  • MailPreviewCard: mostra l'anteprima di un'email
  • MailViewPage: visualizza una singola email completa
  • ComposePage: consente di scrivere una nuova email
  • SearchPage: mostra una visualizzazione della ricerca

router.dart

Innanzitutto, per capire come è configurata la navigazione principale dell'app, apri router.dart nella directory 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);
 }
}

Si tratta del nostro navigatore principale che gestisce le schermate dell'app che utilizzano l'intero canvas, ad esempio HomePage e SearchPage. Questa funzionalità ascolta lo stato dell'app per verificare se abbiamo impostato il percorso verso ReplySearchPath. In tal caso, viene ricreato il navigatore con il SearchPage in cima allo stack. Nota che i nostri schermi sono aggregati in un elemento CustomTransitionPage senza transizioni definite. Questo ti mostra un modo per navigare tra le schermate senza alcuna transizione personalizzata.

home.dart

Abbiamo impostato il percorso verso ReplySearchPath nello stato della nostra app procedendo nel seguente modo all'interno di _BottomAppBarActionItems in 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();
   },
 ),
);

Nel parametro onPressed accediamo al nostro RouterProvider e ne impostiamo routePath su ReplySearchPath. Il nostro RouterProvider tiene traccia dello stato dei nostri navigatori principali.

mail_view_router.dart

Ora vediamo com'è configurata la navigazione interna della nostra app. Apri mail_view_router.dart nella directory lib. Verrà visualizzata una barra di navigazione simile a quella riportata sopra:

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 nostro navigatore interiore. Gestisce le schermate interne dell'app che utilizzano solo il corpo della tela, come InboxPage. L'InboxPage mostra un elenco di email, a seconda dello stato della casella di posta corrente. Il navigatore viene ricreato con il InboxPage corretto in cima allo stack, ogni volta che si verifica una modifica nella proprietà currentlySelectedInbox dello stato della nostra app.

home.dart

Abbiamo impostato la nostra casella di posta attuale nello stato dell'app procedendo nel seguente modo all'interno di _HomePageState in 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(() {});
}

Nella funzione _onDestinationSelected, accediamo al nostro EmailStore e impostiamo il relativo currentlySelectedInbox sulla destinazione selezionata. Il nostro EmailStore tiene traccia dello stato dei nostri navigatori interni.

home.dart

Infine, per vedere un esempio di routing di navigazione utilizzato, apri home.dart nella directory lib. Individua la classe _ReplyFabState, all'interno della proprietà onTap del widget InkWell, che dovrebbe avere un aspetto simile a questo:

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

Questo mostra come passare alla pagina di scrittura delle email senza alcuna transizione personalizzata. Durante questo codelab, analizzerai il codice di Reply per configurare le transizioni Material che funzionano in tandem con le varie azioni di navigazione nell'app.

Ora che hai acquisito familiarità con il codice di base, implementiamo la nostra prima transizione.

5. Aggiungi la transizione Container Transform dalla mailing list alla pagina dei dettagli dell'email

Per iniziare, aggiungerai una transizione quando fai clic su un'email. Per questa modifica alla navigazione, il pattern di trasformazione del container è adatto poiché è progettato per le transizioni tra elementi UI che includono un container. Questo pattern crea una connessione visibile tra due elementi UI.

Prima di aggiungere il codice, prova a eseguire l'app di risposta e a fare clic su un'email. Deve eseguire un semplice jump cut, che consente di sostituire lo schermo senza transizione:

Prima

48b00600f73c7778.gif

Inizia aggiungendo un'importazione per il pacchetto di animazioni nella parte superiore di mail_card_preview.dart, come mostrato nello snippet seguente:

mail_card_preview.dart

import 'package:animations/animations.dart';

Ora che hai importato il pacchetto di animazioni, possiamo iniziare ad aggiungere bellissime transizioni alla tua app. Inizia creando un corso StatelessWidget che ospiterà il nostro widget OpenContainer.

In mail_card_preview.dart, aggiungi il seguente snippet di codice dopo la definizione della classe di 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,
       );
     },
   );
 }
}

Ora usiamo il nostro nuovo wrapper. All'interno della definizione della classe MailPreviewCard aggregaremo il widget Material della funzione build() con il nuovo _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(
...

Il nostro _OpenContainerWrapper ha un widget InkWell e le proprietà colore di OpenContainer definiscono il colore del contenitore che racchiude. Possiamo quindi rimuovere i widget Material e Inkwell. Il codice risultante ha il seguente aspetto:

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

In questa fase, dovresti avere una trasformazione del container completamente funzionante. Se fai clic su un'email, l'elemento dell'elenco si espande in una schermata dei dettagli, mentre restringi l'elenco di email. Se si preme Indietro, la schermata dei dettagli dell'email torna a essere visualizzata nell'elenco mentre viene fatto lo scale up nell'elenco delle email.

Dopo

663e8594319bdee3.gif

6. Aggiungi la transizione Container Transform da FAB alla pagina di scrittura delle email

Continuiamo con la trasformazione del container e aggiungiamo una transizione dal pulsante di azione mobile a ComposePage per espandere il FAB a una nuova email che l'utente deve scrivere. Innanzitutto, esegui di nuovo l'app e fai clic sul FAB per vedere che non ci sono transizioni all'avvio della schermata di scrittura dell'email.

Prima

4aa2befdc5170c60.gif

Il modo in cui configuriamo questa transizione sarà molto simile a quello dell'ultimo passaggio, poiché stiamo utilizzando la stessa classe di widget, OpenContainer.

In home.dart, importiamo il package:animations/animations.dart all'inizio del file e modifichiamo il metodo _ReplyFabState build(). Aggrega il widget Material restituito con 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,
     ...

Oltre ai parametri utilizzati per configurare il nostro widget OpenContainer precedente, ora è in corso l'impostazione anche di onClosed. onClosed è un ClosedCallback che viene chiamato quando la route OpenContainer è stata bloccata o è tornata allo stato chiuso. Il valore restituito della transazione viene passato a questa funzione come argomento. Utilizziamo questo Callback per comunicare al fornitore dell'app che abbiamo abbandonato il percorso ComposePage, in modo che possa informare tutti gli ascoltatori.

Analogamente a quanto fatto nel nostro ultimo passaggio, rimuoveremo il widget Material dal widget, poiché il widget OpenContainer gestisce il colore del widget restituito da closedBuilder con closedColor. Rimuoveremo anche la chiamata a Navigator.push() all'interno del onTap del widget InkWell e la sostituiremo con il openContainer() Callback fornito dal closedBuilder del widget OpenContainer, poiché ora il widget OpenContainer gestisce il proprio routing.

Il codice risultante è il seguente:

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

Ora devo pulire un po' di vecchio codice. Poiché ora il nostro widget OpenContainer gestisce la notifica al fornitore dell'app che non siamo più presenti su ComposePage tramite onClosed ClosedCallback, possiamo rimuovere la nostra implementazione precedente in 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);

Questo è tutto. Dovresti avere una transizione dal FAB per creare una schermata di composizione simile alla seguente:

Dopo

5c7ad1b4b40f9f0c.gif

7. Aggiungi la transizione dell'asse Z condivisa dall'icona di ricerca alla pagina di visualizzazione della ricerca

In questo passaggio, aggiungeremo una transizione dall'icona di ricerca alla visualizzazione della ricerca a schermo intero. Poiché questa modifica alla navigazione non prevede un container permanente, possiamo utilizzare una transizione sull'asse Z condiviso per rafforzare la relazione spaziale tra le due schermate e indicare lo spostamento di un livello verso l'alto nella gerarchia dell'app.

Prima di aggiungere altro codice, prova a eseguire l'app e a toccare l'icona di ricerca nell'angolo in basso a destra dello schermo. Dovrebbe apparire la schermata di visualizzazione della ricerca senza transizione.

Prima

df7683a8ad7b920e.gif

Per iniziare, passiamo al file router.dart. Dopo la definizione della classe ReplySearchPath, aggiungi il seguente snippet:

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

Ora usiamo il nostro nuovo SharedAxisTransitionPageWrapper per realizzare la transizione che vogliamo. All'interno della definizione della classe ReplyRouterDelegate, nella proprietà pages, racchiudiamo la schermata di ricerca con un SharedAxisTransitionPageWrapper anziché un 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(),
     ),
 ],
);

Ora prova a eseguire di nuovo l'app.

81b3ea098926931.gif

Tutto inizia a essere bello! Quando fai clic sull'icona di ricerca nella barra delle app in basso, una transizione dell'asse condiviso scala la pagina di ricerca. Nota, però, che la home page non fa lo scale out e rimane statica quando la pagina di ricerca si espande. Inoltre, quando premi il pulsante Indietro, la home page non viene ridimensionata e rimane statica perché la pagina di ricerca non viene più visualizzata. Quindi non abbiamo ancora finito.

Risolviamo entrambi i problemi aggregando HomePage con SharedAxisTransitionWrapper anziché 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(),
     ),
 ],
);

È tutto. Ora prova a eseguire di nuovo l'app e a toccare l'icona di ricerca. Le schermate Home e Ricerca devono contemporaneamente sfocare e ridimensionare in profondità l'asse Z, creando un effetto omogeneo tra le due schermate.

Dopo

462d890086a3d18a.gif

8. Aggiungi una transizione Fade-through tra le pagine delle caselle di posta

In questo passaggio aggiungeremo una transizione tra diverse caselle di posta. Poiché non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza attraverso per eseguire un semplice "swap" tra gli elenchi di email.

Prima di aggiungere altro codice, prova a eseguire l'app, a toccare il logo Rispondi nella barra delle app in basso e a cambiare casella di posta. L'elenco delle email dovrebbe cambiare senza transizione.

Prima

89033988ce26b92e.gif

Per iniziare, passiamo al file mail_view_router.dart. Dopo la definizione della classe MailViewRouterDelegate, aggiungi il seguente snippet:

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

Come per l'ultimo passaggio, utilizziamo il nostro nuovo FadeThroughTransitionPageWrapper per realizzare la transizione che vogliamo. All'interno della definizione della classe MailViewRouterDelegate, nella proprietà pages, invece di racchiudere la schermata della casella di posta con un CustomTransitionPage, utilizza 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),
   ),
 ],
);

Esegui di nuovo l'app. Quando apri il riquadro di navigazione a scomparsa in basso e cambi le caselle di posta, l'elenco corrente di email dovrebbe scomparire e fare lo scale out, mentre il nuovo elenco si dissolve e viene scalato. Bene!

Dopo

8186940082b630d.gif

9. Aggiungi la transizione dissolvenza attraverso il FAB di scrittura e di risposta

In questo passaggio, aggiungeremo una transizione tra le diverse icone FAB. Poiché non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza attraverso per eseguire un semplice "swap" tra le icone nel FAB.

Prima di aggiungere altro codice, prova a eseguire l'app, a toccare un'email e ad aprire la visualizzazione dell'email. L'icona del FAB dovrebbe cambiare senza una transizione.

Prima

d8e3afa0447cfc20.gif

Lavoreremo in home.dart per il resto del codelab, quindi non preoccuparti di aggiungere l'importazione del pacchetto di animazioni, dato che l'abbiamo già fatto per home.dart nel passaggio 2.

Il modo in cui configureremo le prossime due transizioni sarà molto simile, poiché tutte utilizzeranno una classe riutilizzabile, _FadeThroughTransitionSwitcher.

In home.dart aggiungiamo il seguente snippet in _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,
   );
 }
}

Ora, nel nostro _ReplyFabState, cerca il widget fabSwitcher. fabSwitcher restituisce un'icona diversa a seconda che sia o meno nella visualizzazione email. Concludiamo la presentazione con _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,
             ),
     );
...

Ai nostri _FadeThroughTransitionSwitcher viene applicata una fillColor trasparente, pertanto non vi è alcuno sfondo tra gli elementi durante il passaggio. Creiamo anche un UniqueKey e lo assegniamo a una delle icone.

A questo punto, dovresti avere un FAB contestuale completamente animato. Quando entri nella visualizzazione di un'email, la vecchia icona FAB scompare e si ridimensiona, mentre quella nuova tende a diminuire.

Dopo

c55bacd9a144ec69.gif

10. Aggiungi la transizione Fade Through tra il titolo della casella di posta scomparso

In questo passaggio, aggiungeremo una transizione di dissolvenza attraverso il titolo della casella di posta, passando da uno stato visibile a quello invisibile quando è attiva la visualizzazione di un'email. Poiché non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza attraverso per eseguire un semplice "swap" tra il widget Text che include il titolo della casella di posta e un valore SizedBox vuoto.

Prima di aggiungere altro codice, prova a eseguire l'app, a toccare un'email e ad aprire la visualizzazione dell'email. Il titolo della casella di posta dovrebbe scomparire senza una transizione.

Prima

59eb57a6c71725c0.gif

Il resto del codelab sarà veloce, dato che abbiamo già svolto la maggior parte del lavoro in _FadeThroughTransitionSwitcher nell'ultimo passaggio.

Ora, passiamo al corso _AnimatedBottomAppBar in home.dart per aggiungere la transizione. Riutilizzeremo _FadeThroughTransitionSwitcher del nostro ultimo passaggio e racchiuseamo il condizionale onMailView, in modo da restituire un SizedBox vuoto o un titolo di casella di posta che svanisce sincronizzandosi con il riquadro a scomparsa in basso:

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
                   .bodyMedium!
                   .copyWith(
                     color: ReplyColors.white50,
                   ),
             );
           },
         ),
       ),
),

Questo è tutto. Questo passaggio è terminato.

Esegui di nuovo l'app. Quando apri un'email e si apre la visualizzazione email, il titolo della casella di posta nella barra delle app in basso dovrebbe scomparire e fare lo scale out. Ottimo!

Dopo

3f1a3db01a481124.gif

11. Aggiungi la transizione dissolvenza attraverso le azioni della barra delle app in basso

In questo passaggio, aggiungeremo una dissolvenza attraverso le azioni della barra delle app in basso in base al contesto dell'applicazione. Poiché non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza attraverso per eseguire un semplice "swap" tra le azioni della barra delle app in basso quando l'app si trova nella home page, quando il riquadro a scomparsa in basso è visibile e quando è attiva la visualizzazione email.

Prima di aggiungere altro codice, prova a eseguire l'app, a toccare un'email e ad aprire la visualizzazione dell'email. Puoi anche provare a toccare il logo Rispondi. Le azioni nella barra delle app in basso dovrebbero cambiare senza transizione.

Prima

5f662eac19fce3ed.gif

Come per l'ultimo passaggio, utilizzeremo di nuovo _FadeThroughTransitionSwitcher. Per ottenere la transizione desiderata, vai alla definizione della classe _BottomAppBarActionItems e aggrega il widget di ritorno della funzione build() con un _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
...

Proviamo! Quando apri un'email e accedi alla visualizzazione dell'email, le precedenti azioni della barra delle app in basso dovrebbero scomparire e fare lo scale out, mentre le nuove azioni dissolvenze e scale in. Ben fatto!

Dopo

cff0fa2afa1c5a7f.gif

12. Complimenti!

Utilizzando meno di 100 righe di codice Dart, il pacchetto di animazioni ti ha aiutato a creare transizioni stupende in un'app esistente, conforme alle linee guida di Material Design e avere un aspetto e un comportamento coerenti su tutti i dispositivi.

d5637de49eb64d8a.gif

Passaggi successivi

Per ulteriori informazioni sul sistema di movimento Material, assicurati di consultare le linee guida e la documentazione completa per gli sviluppatori, quindi prova ad aggiungere alcune transizioni Material alla tua app.

Grazie per aver provato Material motion. Speriamo che questo codelab ti sia piaciuto.

Ho completato questo codelab con una quantità di tempo e di sforzi ragionevoli

Totalmente d'accordo D'accordo Né chiara, né confusa In disaccordo Totalmente in disaccordo

Vorrei continuare a usare il sistema di movimento Material in futuro

Totalmente d'accordo D'accordo Né chiara, né confusa In disaccordo Totalmente in disaccordo

Per altre demo su come utilizzare i widget forniti dalla libreria Flutter Material, nonché il framework Flutter, visita la Flutter Gallery.

46ba920f17198998.png

6ae8ae284bf4f9fa.png