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 da un team di ingegneri e designer UX di Google, MDC offre dozzine di componenti dell'interfaccia utente belli e funzionali ed è disponibile per Android, iOS, web e Flutter.material.io/develop

Che cos'è 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 pattern di transizione di Material sono i seguenti:

  • Trasformazione del contenitore: transizioni tra elementi dell'interfaccia utente che includono un contenitore. Crea un collegamento visibile tra due elementi dell'interfaccia utente distinti trasformando facilmente 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 sull'asse 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 uscita e in entrata sequenziale, con una scala dell'elemento in entrata.

385ba37b8da68969.gif

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

cfc40fd6e27753b6.gif

Il pacchetto di animazioni offre widget di transizione per questi pattern, basati sia sulla libreria di animazioni Flutter (flutter/animation.dart) sia sulla libreria di materiale Flutter (flutter/material.dart):

In questo codelab utilizzerai le transizioni Material create sulla base del framework Flutter e della raccolta 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 dal pacchetto di animazioni per personalizzare l'aspetto e il design della tua app.

Ti verrà fornito il codice di avvio dell'app Reply e dovrai incorporare le seguenti transizioni Material nell'app, che puoi vedere nella GIF del codelab completato di seguito:

  • Transizione di Container Transform dall'elenco email alla pagina dei dettagli dell'email
  • Transizione di Container Transform da FAB alla pagina di scrittura delle email
  • Transizione dell'asse Z condivisa dall'icona di ricerca alla pagina di visualizzazione della ricerca
  • Transizione Dissolvenza tra le pagine della casella di posta
  • Transizione Fade Through tra scrittura e risposta FAB
  • Transizione Dissolvenza tra il titolo della casella di posta che scompare
  • 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 valuteresti il tuo livello di esperienza nella creazione di app Flutter?

Principiante Intermedio Proficiente

Cosa vuoi 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'. Cerco codice di esempio da utilizzare nel mio progetto. Sto cercando una spiegazione di 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 (è richiesta 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 sviluppare 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. Esistono requisiti specifici per il sistema operativo che sono descritti in dettaglio su docs.flutter.dev/desktop.

3. Scarica l'app di avvio del codelab

Opzione 1: clona l'app del codelab di avvio da GitHub

Per clonare questo codelab da GitHub, esegui i seguenti 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.

Verificare le dipendenze del progetto

Il progetto dipende dal pacchetto di animazioni. In pubspec.yaml, tieni presente che la sezione dependencies include quanto segue:

animations: ^2.0.0

Apri il progetto ed esegui l'app

  1. Apri il progetto nel tuo editor preferito.
  2. Segui le istruzioni "Esegui l'app" in Inizia: prova per 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 eleganti, può essere utile rallentare le animazioni del dispositivo per osservare alcuni dettagli più minuti delle transizioni durante l'implementazione. Questo può essere fatto tramite un'impostazione in-app, accessibile toccando l'icona delle impostazioni quando il riquadro inferiore è 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. Acquisire 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.

  • HomePage: mostra 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 di 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 e gestisce le schermate della nostra app che occupano l'intero canvas, come HomePage e SearchPage. Ascolta lo stato della nostra app per verificare se abbiamo impostato il percorso per 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

Impostiamo il percorso su ReplySearchPath nello stato della nostra app eseguendo i seguenti passaggi 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 impostiamo il relativo 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. Viene visualizzato un navigatore simile a quello 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 ricostruito con il InboxPage corretto sopra la pila ogni volta che viene modificata la proprietà currentlySelectedInbox dello stato della nostra app.

home.ARROW

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

Infine, per vedere un esempio di routing di navigazione in uso, 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, esaminerai 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, aggiungi una transizione facendo clic su un'email. Per questa modifica alla navigazione, il pattern di trasformazione del contenitore è molto adatto, in quanto progettato per le transizioni tra elementi UI che includono un contenitore. Questo pattern crea un collegamento visibile tra due elementi UI.

Prima di aggiungere il codice, prova a eseguire l'app Reply 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 un'importazione per il pacchetto di animazioni, possiamo iniziare ad aggiungere bellissime transizioni alla tua app. Iniziamo creando una classe 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à di colore del OpenContainer definiscono il colore del contenitore che racchiude. Possiamo quindi rimuovere i widget Material e Inkwell. Il codice risultante è il seguente:

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 e l'elenco delle email si riduce. Se premi Indietro, la schermata dei dettagli dell'email viene compressa in un elemento dell'elenco e l'elenco delle email viene visualizzato in un formato più grande.

Dopo

663e8594319bdee3.gif

6. Aggiungere la transizione di Trasformazione del contenitore dal FAB alla pagina di composizione dell'email

Continuiamo con la trasformazione del container e aggiungiamo una transizione dal pulsante di azione mobile a ComposePage, espandendo 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 package:animations/animations.dart nella parte superiore del file e modifichiamo il metodo _ReplyFabState build(). Involgiamo il widget Material restituito con un widget OpenContainer:

home.ARROW

// 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 widget OpenContainer precedente, ora viene impostato anche 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.

Come abbiamo fatto per l'ultimo passaggio, rimuoveremo il widget Material dal nostro widget, poiché il widget OpenContainer gestisce il colore del widget restituito da closedBuilder con closedColor. Inoltre, rimuoveremo la chiamata Navigator.push() all'interno di onTap del widget InkWell e la sostituiremo con openContainer() Callback fornito da closedBuilder del widget OpenContainer, poiché ora il widget OpenContainer gestisce il proprio routing.

Il codice risultante è il seguente:

home.ARROW

// 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 dobbiamo ripulire un po' di codice vecchio. Poiché il nostro widget OpenContainer ora gestisce l'invio di una notifica al fornitore della nostra app che non siamo più 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 per questo passaggio. 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 tocca 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 sull'asse condiviso ridimensiona la pagina di ricerca in modo che sia visibile. Nota, però, che la home page non esegue lo scale out e rimane statica quando la pagina di ricerca si espande. Inoltre, quando si preme il pulsante Indietro, la home page non viene visualizzata, ma rimane statica mentre la pagina di ricerca viene rimossa dalla visualizzazione. 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. Aggiungere la transizione con dissolvenza tra le pagine della cassetta postale

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 "scambio" tra elenchi di email.

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

Prima

89033988ce26b92e.gif

Per iniziare, apriamo il 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 nell'ultimo passaggio, utilizziamo il nuovo FadeThroughTransitionPageWrapper per completare la transizione che vogliamo. All'interno della definizione della classe MailViewRouterDelegate, nella proprietà pages, anziché racchiudere la schermata della cassetta postale 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 in basso e cambi casella di posta, l'elenco corrente delle email dovrebbe svanire e rimpicciolirsi, mentre il nuovo elenco dovrebbe svanire e ingrandirsi. 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 sottolineare una relazione spaziale o gerarchica, utilizzeremo una transizione graduale per eseguire un semplice "scambio" 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 pulsante flottante deve cambiare senza 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.ARROW

// 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, nella nostra _ReplyFabState, cerca il widget fabSwitcher. fabSwitcher restituisce un'icona diversa a seconda che sia nella visualizzazione email o meno. Concludiamo la presentazione con _FadeThroughTransitionSwitcher:

home.ARROW

// 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 apri la visualizzazione di un'email, l'icona del menu a discesa flottante precedente si attenua e si riduce di dimensioni, mentre quella nuova si attenua e aumenta di dimensioni.

Dopo

c55bacd9a144ec69.gif

10. Aggiungere la transizione con dissolvenza tra il titolo della cassetta postale che scompare

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 sottolineare una relazione spaziale o gerarchica, utilizzeremo una transizione graduale per eseguire un semplice "scambio" tra il widget Text che include il titolo della cassetta postale e un SizedBox vuoto.

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

Prima

59eb57a6c71725c0.gif

Il resto di questo codelab sarà veloce perché abbiamo già svolto la maggior parte del lavoro in _FadeThroughTransitionSwitcher nell'ultimo passaggio.

Ora, passiamo al nostro corso _AnimatedBottomAppBar in home.dart per aggiungere la transizione. Riutilizzeremo _FadeThroughTransitionSwitcher dell'ultimo passaggio e racchiuderemo il nostro onMailView condizionale, che restituisce un SizedBox vuoto o un titolo della cassetta postale che si attenua in sincronia con il riquadro inferiore:

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

È tutto, abbiamo completato questo passaggio.

Esegui di nuovo l'app. Quando apri un'email e viene visualizzata la relativa visualizzazione, il titolo della cassetta postale nella barra delle app in basso dovrebbe svanire e rimpicciolirsi. Ottimo!

Dopo

3f1a3db01a481124.gif

11. Aggiungere la transizione Svanisci tra le azioni della barra delle app in basso

In questo passaggio aggiungeremo una transizione di dissolvenza per far scomparire le azioni della barra delle app in basso in base al contesto delle applicazioni. Dal momento che non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza attraverso per eseguire un semplice "scambio" 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, toccando un'email e aprendo 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 nell'ultimo passaggio, utilizzeremo di nuovo _FadeThroughTransitionSwitcher. Per ottenere la transizione desiderata, vai alla definizione della classe _BottomAppBarActionItems e racchiudi 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 viene visualizzata la relativa visualizzazione, le azioni della barra delle app in basso precedenti dovrebbero svanire e rimpicciolirsi, mentre le nuove azioni dovrebbero svanire e ingrandirsi. Ben fatto!

Dopo

cff0fa2afa1c5a7f.gif

12. Complimenti!

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

d5637de49eb64d8a.gif

Passaggi successivi

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

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

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

Molto d'accordo D'accordo Né d'accordo né in disaccordo In disaccordo Molto in disaccordo

Vorrei continuare a usare il sistema di movimento Material in futuro

Molto d'accordo D'accordo Né d'accordo né in disaccordo In disaccordo Molto in disaccordo

Per altre dimostrazioni su come utilizzare i widget forniti dalla libreria Material Flutter e dal framework Flutter, visita la Flutter Gallery.

46ba920f17198998.png

6ae8ae284bf4f9fa.png