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 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.
- 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.
- 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.
- Dissolvenza: utilizzata per gli elementi UI che entrano o escono entro i 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 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
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?
Cosa vuoi 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 (è 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
- Apri il progetto nel tuo editor preferito.
- 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.
(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.
(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. 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
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
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
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
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
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 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
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
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
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
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
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
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
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
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
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.
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
Vorrei continuare a usare il sistema di movimento Material in futuro
Dai un'occhiata alla Flutter Gallery
Per altre dimostrazioni su come utilizzare i widget forniti dalla libreria Material Flutter e dal framework Flutter, visita la Flutter Gallery. |