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.
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.
- 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.
- 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.
- Dissolvenza: utilizzata per gli elementi UI che entrano o escono entro i margini dello schermo.
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
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?
Cosa ti piacerebbe imparare da questo codelab?
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
- Apri il progetto nell'editor che preferisci.
- 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.
(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.
(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.
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
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
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
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
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
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.
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
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
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
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
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
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
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
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
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
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.
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
Vorrei continuare a usare il sistema di movimento Material in futuro
Dai un'occhiata alla Flutter Gallery
Per altre demo su come utilizzare i widget forniti dalla libreria Flutter Material, nonché il framework Flutter, visita la Flutter Gallery. |