1. Introduzione
Material Design è un sistema per creare prodotti digitali audaci e belli. 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.
| I componenti Material (MDC) aiutano gli sviluppatori a implementare Material Design. Creato da un team di ingegneri e progettisti UX di Google, MDC include decine di componenti UI 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 pattern di transizione all'interno del pacchetto di animazioni che possono aiutare gli utenti a comprendere e navigare in 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 UI che includono un contenitore; crea un collegamento visibile tra due elementi UI distinti trasformando senza problemi un elemento in un altro.

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

- Dissolvenza tramite:transizioni tra elementi UI che non hanno una forte correlazione; utilizza una dissolvenza in uscita e in entrata sequenziale, con una scalabilità dell'elemento in entrata.

- Dissolvenza:utilizzata per gli elementi UI che entrano o escono dai limiti dello schermo.

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 materiali Flutter (flutter/material.dart):
In questo codelab utilizzerai le transizioni Material create sulla base del framework Flutter e della libreria Material, il che significa che avrai a che fare con i widget. :)
Cosa creerai
Questo codelab ti guiderà nella creazione di alcune transizioni in un'app di posta elettronica Flutter di esempio chiamata Reply, utilizzando Dart, per dimostrare come puoi utilizzare le transizioni del pacchetto di animazioni per personalizzare l'aspetto della tua app.
Verrà fornito il codice iniziale per l'app Reply e incorporerai le seguenti transizioni Material nell'app, che possono essere visualizzate nella GIF del codelab completato di seguito:
- Transizione Trasformazione contenitore dall'elenco email alla pagina dei dettagli dell'email
- Transizione della trasformazione del contenitore dal pulsante Azione rapida alla pagina di composizione dell'email
- Transizione asse Z condiviso dall'icona di ricerca alla pagina della visualizzazione di ricerca
- Transizione Dissolvenza tra le pagine della casella di posta
- Transizione Dissolvenza tra il pulsante Azione rapida di composizione e risposta
- Transizione Dissolvenza tra il titolo della casella di posta che scompare
- Transizione Dissolvenza tra le azioni della barra delle app in basso

Che cosa ti serve
- Conoscenza di base dello sviluppo Flutter e di Dart
- Un editor di codice
- Un emulatore o un dispositivo Android/iOS
- Il codice campione (vedi il passaggio successivo)
Come valuteresti il tuo livello di esperienza nella creazione di app Flutter?
Che cosa ti piacerebbe imparare da questo codelab?
2. Configura l'ambiente di sviluppo Flutter
Per completare questo lab, hai bisogno di due software: l'SDK Flutter e un editor.
Puoi eseguire il codelab utilizzando uno qualsiasi 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 (Chrome è necessario per il debug).
- Come applicazione desktop Windows, Linux o macOS. Devi sviluppare sulla piattaforma in cui prevedi di eseguire il deployment. Pertanto, se vuoi sviluppare un'app desktop Windows, devi svilupparla su Windows per accedere alla catena di build appropriata. Esistono requisiti specifici del sistema operativo trattati in dettaglio su 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. In pubspec.yaml, nota che la sezione dependencies include quanto segue:
animations: ^2.0.0
Apri il progetto ed esegui l'app
- Apri il progetto nell'editor che preferisci.
- Segui le istruzioni per "Eseguire l'app" nella sezione Guida rapida: prova per l'editor che hai scelto.
Operazione riuscita. Il codice iniziale per la home page di Reply deve essere eseguito sul tuo dispositivo/emulatore. Dovresti visualizzare la posta in arrivo contenente un elenco di email.

(Facoltativo) Rallentare le animazioni del dispositivo
Poiché questo codelab prevede transizioni rapide ma raffinate, può essere utile rallentare le animazioni del dispositivo per osservare alcuni dettagli più fini delle transizioni durante l'implementazione. Questa operazione può essere eseguita tramite un'impostazione in-app, accessibile toccando l'icona delle impostazioni quando il riquadro inferiore è aperto. Non preoccuparti, questo metodo di rallentamento delle animazioni del dispositivo non influirà sulle animazioni sul dispositivo al di fuori dell'app Reply.

(Facoltativo) Modalità Buio
Se il tema chiaro di Reply ti affatica la vista, non cercare oltre. È disponibile un'impostazione in-app che ti consente di cambiare il tema dell'app in modalità Buio, per adattarsi meglio ai tuoi occhi. Questa impostazione è accessibile toccando l'icona delle impostazioni quando il riquadro inferiore è aperto.

4. Familiarizzare 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 dell'applicazione.
- Home page:mostra la casella selezionata
- InboxPage: mostra un elenco di email
- MailPreviewCard: mostra l'anteprima di un'email
- MailViewPage: mostra una singola email completa
- ComposePage:consente di scrivere una nuova email
- SearchPage:mostra una visualizzazione di ricerca
router.dart
Innanzitutto, per capire come è impostata 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);
}
}
Questo è il nostro navigatore principale e gestisce le schermate dell'app che occupano l'intero canvas, come HomePage e SearchPage. Ascolta lo stato della nostra app per verificare se abbiamo impostato l'itinerario per ReplySearchPath. In questo caso, il navigatore viene ricreato con SearchPage in cima alla pila. Nota che le nostre schermate sono racchiuse in un CustomTransitionPage senza transizioni definite. Questo mostra un modo per spostarsi tra le schermate senza alcuna transizione personalizzata.
home.dart
Impostiamo la nostra route su 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 a 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 come è impostata la navigazione interna della nostra app. Apri mail_view_router.dart nella directory lib. Vedrai 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,
),
)
],
);
},
);
}
...
}
Questo è il nostro navigatore interiore. Gestisce le schermate interne della nostra app che consumano solo il corpo del canvas, ad esempio InboxPage. InboxPage mostra un elenco di email a seconda della cassetta postale corrente nello stato della nostra app. Il navigatore viene ricreato con il InboxPage corretto in cima allo stack ogni volta che viene modificata la proprietà currentlySelectedInbox dello stato della nostra app.
home.dart
Impostiamo la casella di posta attuale nello stato della nostra 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 nostra funzione _onDestinationSelected, accediamo a EmailStore e impostiamo 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 in uso, apri home.dart nella directory lib. Individua la classe _ReplyFabState all'interno della proprietà onTap del widget InkWell, che dovrebbe avere questo aspetto:
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 composizione 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 familiarità con il codice iniziale, implementiamo la nostra prima transizione.
5. Aggiungere la transizione Trasformazione del contenitore dall'elenco email alla pagina dei dettagli dell'email
Per iniziare, aggiungi una transizione quando fai clic su un'email. Per questa modifica della navigazione, il pattern di trasformazione del contenitore è particolarmente 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 qualsiasi codice, prova a eseguire l'app Reply e a fare clic su un'email. Dovrebbe essere un semplice taglio netto, il che significa che lo schermo viene sostituito senza transizione:
Prima

Inizia aggiungendo un'importazione per il pacchetto di animazioni nella parte superiore di mail_card_preview.dart, come mostrato nel seguente snippet:
mail_card_preview.dart
import 'package:animations/animations.dart';
Ora che hai un'importazione per il pacchetto di animazioni, possiamo iniziare ad aggiungere transizioni accattivanti alla tua app. Iniziamo creando una classe StatelessWidget che conterrà 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 mettiamo alla prova il nostro nuovo wrapper. All'interno della definizione della classe MailPreviewCard, il widget Material della funzione build() verrà racchiuso nel 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 di OpenContainer definiscono il colore del contenitore che racchiude. Pertanto, possiamo 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,
),
);
A questo punto, 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 l'elenco delle email si ritira. Se premi Indietro, la schermata dei dettagli dell'email viene compressa in un elemento di elenco mentre viene eseguito lo zoom avanti nell'elenco delle email.
Dopo

6. Aggiungi la transizione della trasformazione del contenitore dal FAB alla pagina di composizione dell'email
Continuiamo con la trasformazione del contenitore e aggiungiamo una transizione dal pulsante di azione flottante a ComposePage, espandendo il pulsante di azione flottante in una nuova email da scrivere. Innanzitutto, esegui di nuovo l'app e fai clic sul pulsante Azione per vedere che non c'è transizione all'avvio della schermata di composizione email.
Prima

Il modo in cui configuriamo questa transizione sarà molto simile a quello dell'ultimo passaggio, poiché utilizziamo 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(). Racchiudiamo 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 widget OpenContainer precedente, ora viene impostato anche onClosed. onClosed è un ClosedCallback chiamato quando la route OpenContainer è stata estratta o è tornata allo stato chiuso. Il valore restituito di questa transazione viene passato a questa funzione come argomento. Utilizziamo questo Callback per comunicare al fornitore della nostra app che abbiamo lasciato l'itinerario ComposePage, in modo che possa avvisare 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. Rimuoveremo anche la chiamata Navigator.push() all'interno di onTap del widget InkWell e la sostituiremo con openContainer() Callback fornito da closedBuilder di 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 puliamo un po' di codice obsoleto. Poiché il nostro widget OpenContainer ora gestisce la notifica al fornitore della nostra app che non ci troviamo 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 passaggio è terminato. Dovresti avere una transizione dal FAB alla schermata di composizione simile alla seguente:
Dopo

7. Aggiungere la transizione Asse Z condiviso dall'icona di ricerca alla pagina della visualizzazione di ricerca
In questo passaggio, aggiungeremo una transizione dall'icona di ricerca alla visualizzazione di ricerca a schermo intero. Poiché non è coinvolto alcun contenitore persistente in questa modifica della navigazione, 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 essere visualizzata la schermata della visualizzazione di ricerca senza transizione.
Prima

Per iniziare, andiamo 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 utilizziamo il nuovo SharedAxisTransitionPageWrapper per ottenere la transizione che desideriamo. 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.

Le cose iniziano ad andare alla grande. Quando fai clic sull'icona di ricerca nella barra delle app inferiore, una transizione dell'asse condiviso mostra la pagina di ricerca. Tuttavia, nota come la home page non viene ridimensionata e rimane statica mentre la pagina di ricerca viene ridimensionata. Inoltre, quando si preme il pulsante Indietro, la home page non viene visualizzata in scala, ma rimane statica mentre la pagina di ricerca scompare dalla visualizzazione. Quindi non abbiamo ancora finito.
Risolviamo entrambi i problemi racchiudendo anche HomePage con il nostro SharedAxisTransitionWrapper anziché con 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 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 della visualizzazione Home e della ricerca devono sbiadire e scalare simultaneamente lungo l'asse Z in profondità, creando un effetto uniforme tra le due schermate.
Dopo

8. Aggiungere la transizione Dissolvenza tra le pagine della casella di posta
In questo passaggio, aggiungeremo una transizione tra le diverse caselle postali. Poiché non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza incrociata per eseguire un semplice "scambio" tra gli elenchi di email.
Prima di aggiungere altro codice, prova a eseguire l'app, tocca il logo Rispondi nella barra delle app inferiore e cambia casella di posta. L'elenco delle email deve cambiare senza transizione.
Prima

Per iniziare, andiamo 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 nell'ultimo passaggio, utilizziamo il nuovo FadeThroughTransitionPageWrapper per ottenere la transizione che desideriamo. All'interno della definizione della classe MailViewRouterDelegate, nella proprietà pages, anziché 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 in basso e cambi casella di posta, l'elenco attuale di email dovrebbe sbiadire e ridursi, mentre il nuovo elenco dovrebbe apparire e ingrandirsi. Bene!
Dopo

9. Aggiungere la transizione Dissolvenza tra il pulsante Azione rapida di composizione e risposta
In questo passaggio, aggiungeremo una transizione tra le diverse icone FAB. Poiché non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza incrociata per eseguire un semplice "scambio" tra le icone nel pulsante di azione rapida.
Prima di aggiungere altro codice, prova a eseguire l'app, tocca un'email e apri la visualizzazione dell'email. L'icona FAB deve cambiare senza transizione.
Prima

Lavoreremo in home.dart per il resto del codelab, quindi non preoccuparti di aggiungere l'importazione per il pacchetto di animazioni, perché l'abbiamo già fatto per home.dart nel passaggio 2.
Il modo in cui configuriamo le prossime transizioni sarà molto simile, poiché tutte utilizzeranno una classe riutilizzabile, _FadeThroughTransitionSwitcher.
In home.dart aggiungiamo il seguente snippet sotto _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, in _ReplyFabState, cerca il widget fabSwitcher. Il fabSwitcher restituisce un'icona diversa a seconda che si trovi nella visualizzazione email o meno. Concludiamo con il nostro _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,
),
);
...
Diamo ai nostri _FadeThroughTransitionSwitcher una fillColor trasparente, quindi non c'è sfondo tra gli elementi durante la transizione. Creiamo anche un UniqueKey e lo assegniamo a una delle icone.
Ora, in questo passaggio, dovresti avere un FAB contestuale completamente animato. Quando si passa alla visualizzazione di un'email, la vecchia icona FAB scompare e si riduce, mentre la nuova icona appare e si ingrandisce.
Dopo

10. Aggiungere la transizione Dissolvenza tra il titolo della casella postale che scompare
In questo passaggio, aggiungeremo una transizione di dissolvenza incrociata per far dissolvere il titolo della casella di posta tra uno stato visibile e uno invisibile quando si è in una visualizzazione email. Poiché non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza incrociata per eseguire un semplice "scambio" tra il widget Text che include il titolo della casella di posta e un widget SizedBox vuoto.
Prima di aggiungere altro codice, prova a eseguire l'app, tocca un'email e apri la visualizzazione dell'email. Il titolo della casella di posta dovrebbe scomparire senza una transizione.
Prima

Il resto di questo codelab sarà rapido, poiché abbiamo già svolto la maggior parte del lavoro nel _FadeThroughTransitionSwitcher nell'ultimo passaggio.
Ora passiamo alla classe _AnimatedBottomAppBar in home.dart per aggiungere la transizione. Riutilizzeremo _FadeThroughTransitionSwitcher dell'ultimo passaggio e inseriremo la condizione onMailView, che restituisce un SizedBox vuoto o un titolo della casella di posta che appare 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 posta, il titolo della casella di posta nella barra delle app in basso dovrebbe sbiadire e ridimensionarsi. Fantastico!
Dopo

11. Aggiungere la transizione Dissolvenza tra le azioni della barra delle app in basso
In questo passaggio, aggiungeremo una transizione di dissolvenza incrociata per dissolvere le azioni della barra delle app inferiore in base al contesto delle applicazioni. Poiché non vogliamo enfatizzare una relazione spaziale o gerarchica, utilizzeremo una dissolvenza incrociata 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 inferiore è visibile e quando ci troviamo nella visualizzazione email.
Prima di aggiungere altro codice, prova a eseguire l'app, tocca un'email e apri la visualizzazione dell'email. Puoi anche provare a toccare il logo Rispondi. Le azioni della barra delle app in basso devono cambiare senza transizione.
Prima

Come nel passaggio precedente, 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
...
Ora proviamo. Quando apri un'email e visualizzi la posta, le azioni della vecchia barra delle app in basso dovrebbero sbiadire e ridimensionarsi, mentre le nuove azioni dovrebbero apparire e ridimensionarsi. Ben fatto!
Dopo

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 e che ha un aspetto e un comportamento coerenti su tutti i dispositivi.

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 il movimento Material. Ci auguriamo che questo codelab ti sia piaciuto.
Sono riuscito a completare questo codelab con un ragionevole dispendio di tempo e impegno
Vorrei continuare a utilizzare il sistema di movimento Material in futuro
Dai un'occhiata a Flutter Gallery
| Per altre demo su come utilizzare i widget forniti dalla libreria Material Flutter e dal framework Flutter, visita la Galleria Flutter. |



