1. Einführung
Material Design ist ein System zum Erstellen ansprechender digitaler Produkte. Wenn Stil, Branding, Interaktion und Bewegung unter einem einheitlichen Satz von Prinzipien und Komponenten vereint werden, können Produktteams ihr größtes Designpotenzial entfalten.
| Material Components (MDC) helfen Entwicklern bei der Implementierung von Material Design. MDC wurde von einem Team aus Entwicklern und UX-Designern bei Google entwickelt und bietet Dutzende von ansprechenden und funktionalen UI-Komponenten. Es ist für Android, iOS, das Web und Flutter verfügbar.material.io/develop |
Was ist das Material-Bewegungssystem für Flutter?
Das Material Motion-System für Flutter ist eine Reihe von Übergangsmustern im Animationspaket, die Nutzern helfen können, eine App zu verstehen und darin zu navigieren. Dies wird in den Material Design-Richtlinien beschrieben.
Die vier wichtigsten Muster für Materialübergänge sind:
- Container-Übergang:Übergänge zwischen UI-Elementen, die einen Container enthalten. Es wird eine sichtbare Verbindung zwischen zwei unterschiedlichen UI-Elementen hergestellt, indem ein Element nahtlos in ein anderes übergeht.

- Gemeinsame Achse:Übergänge zwischen UI-Elementen, die räumlich oder navigatorisch zueinander in einer Beziehung stehen; verwendet gemeinsame Übergänge auf der X-, Y- oder Z-Achse, um den Zusammenhang der Elemente zu verstärken.

- Durchblenden:Übergänge zwischen UI-Elementen, zwischen denen keine starke Verbindung besteht. Dabei wird ein sequenzielles Aus- und Einblenden mit einer Skalierung des eingehenden Elements verwendet.

- Überblenden:Wird für UI-Elemente verwendet, die innerhalb der Bildschirmgrenzen ein- oder ausgeblendet werden.

Das Animationspaket bietet Übergangswidgets für diese Muster, die sowohl auf der Flutter-Animationsbibliothek (flutter/animation.dart) als auch auf der Flutter-Materialbibliothek (flutter/material.dart) basieren:
In diesem Codelab verwenden Sie die Material-Übergänge, die auf dem Flutter-Framework und der Material-Bibliothek basieren. Das bedeutet, dass Sie mit Widgets arbeiten. :)
Umfang
In diesem Codelab erfahren Sie, wie Sie mit Dart einige Übergänge in eine Beispiel-E‑Mail-App namens Reply einbauen, um zu zeigen, wie Sie Übergänge aus dem Animationspaket verwenden können, um das Erscheinungsbild Ihrer App anzupassen.
Der Startcode für die Reply-App wird bereitgestellt. Sie werden die folgenden Material-Übergänge in die App einbauen, die im GIF des fertigen Codelabs unten zu sehen sind:
- Container Transform: Übergang von der E‑Mail-Liste zur E‑Mail-Detailseite
- Container Transform-Übergang von FAB zur Seite zum Verfassen von E‑Mails
- Gemeinsame Z-Achse: Übergang vom Suchsymbol zur Seite mit der Suchansicht
- Fade Through-Übergang zwischen Postfachseiten
- Fade Through-Übergang zwischen dem FAB zum Verfassen und dem FAB zum Antworten
- Übergang Fade Through zwischen verschwindendem Postfachtitel
- Fade Through-Übergang zwischen Aktionen in der unteren App-Leiste

Voraussetzungen
- Grundkenntnisse in der Flutter-Entwicklung und in Dart
- Ein Code-Editor
- Ein Android-/iOS-Emulator oder ‑Gerät
- Der Beispielcode (siehe nächsten Schritt)
Wie erfahren sind Sie im Erstellen von Flutter-Apps?
Was möchten Sie in diesem Codelab lernen?
2. Flutter-Entwicklungsumgebung einrichten
Für dieses Lab benötigen Sie zwei Softwarekomponenten: das Flutter SDK und einen Editor.
Sie können das Codelab auf einem der folgenden Geräte ausführen:
- Ein physisches Android- oder iOS-Gerät, das mit Ihrem Computer verbunden ist und auf den Entwicklermodus eingestellt ist.
- Der iOS-Simulator (erfordert die Installation von Xcode-Tools).
- Android Emulator (Einrichtung in Android Studio erforderlich)
- Ein Browser (für das Debugging ist Chrome erforderlich).
- Als Windows-, Linux- oder macOS-Desktopanwendung. Sie müssen auf der Plattform entwickeln, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktop-App entwickeln möchten, müssen Sie unter Windows entwickeln, um auf die entsprechende Build-Kette zuzugreifen. Es gibt betriebssystemspezifische Anforderungen, die auf docs.flutter.dev/desktop ausführlich beschrieben werden.
3. Starter-App für das Codelab herunterladen
Option 1: Codelab-Starter-App aus GitHub klonen
Führen Sie die folgenden Befehle aus, um dieses Codelab von GitHub zu klonen:
git clone https://github.com/material-components/material-components-flutter-motion-codelab.git cd material-components-flutter-motion-codelab
Option 2:ZIP-Datei der Starter-Codelab-App herunterladen
Die Starter-App befindet sich im Verzeichnis material-components-flutter-motion-codelab-starter.
Projektabhängigkeiten prüfen
Das Projekt hängt vom Animationspaket ab. Im pubspec.yaml enthält der Abschnitt dependencies Folgendes:
animations: ^2.0.0
Projekt öffnen und App ausführen
- Öffnen Sie das Projekt in einem beliebigen Editor.
- Folgen Sie der Anleitung unter Erste Schritte: Testlauf für den von Ihnen ausgewählten Editor.
Fertig! Der Startcode für die Startseite von Reply sollte auf Ihrem Gerät oder Emulator ausgeführt werden. Sie sollten den Posteingang mit einer Liste von E‑Mails sehen.

Optional: Geräteanimationen verlangsamen
Da in diesem Codelab schnelle, aber ausgefeilte Übergänge verwendet werden, kann es hilfreich sein, die Animationen des Geräts zu verlangsamen, um einige Feinheiten der Übergänge während der Implementierung zu beobachten. Dies kann über eine In-App-Einstellung erfolgen, auf die zugegriffen werden kann, indem bei geöffnetem unteren Bereich auf das Symbol „Einstellungen“ getippt wird. Keine Sorge, diese Methode zum Verlangsamen von Geräteanimationen wirkt sich nicht auf Animationen auf dem Gerät außerhalb der Reply-App aus.

Optional: Dunkler Modus
Wenn Sie das helle Design von Reply als unangenehm empfinden, sind Sie hier genau richtig. In der App gibt es eine Einstellung, mit der Sie das App-Design in den dunklen Modus ändern können, um die Augen zu schonen. Sie können auf diese Einstellung zugreifen, indem Sie bei geöffnetem unteren Bereich auf das Symbol „Einstellungen“ tippen.

4. Mit dem Beispiel-App-Code vertraut machen
Sehen wir uns den Code an. Wir haben eine App bereitgestellt, die das Animationspaket verwendet, um zwischen verschiedenen Bildschirmen in der Anwendung zu wechseln.
- Startseite:Das ausgewählte Postfach wird angezeigt.
- InboxPage: Hier wird eine Liste von E‑Mails angezeigt.
- MailPreviewCard: Zeigt eine Vorschau einer E‑Mail an.
- MailViewPage:Zeigt eine einzelne vollständige E‑Mail an.
- ComposePage:Ermöglicht das Verfassen einer neuen E‑Mail.
- SearchPage:Zeigt eine Suchansicht an.
router.dart
Öffnen Sie zuerst router.dart im Verzeichnis lib, um zu sehen, wie die Root-Navigation der App eingerichtet ist:
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);
}
}
Dies ist unser Root-Navigator. Er verwaltet die Bildschirme unserer App, die den gesamten Canvas einnehmen, z. B. HomePage und SearchPage. Es überwacht den Status unserer App, um zu prüfen, ob wir die Route auf ReplySearchPath festgelegt haben. Wenn ja, wird der Navigator mit SearchPage oben im Stapel neu erstellt. Beachten Sie, dass unsere Bildschirme in einem CustomTransitionPage ohne definierte Übergänge eingeschlossen sind. Hier sehen Sie eine Möglichkeit, ohne benutzerdefinierte Übergänge zwischen Bildschirmen zu wechseln.
home.dart
Wir legen den Routenstatus unserer App auf ReplySearchPath fest, indem wir Folgendes in _BottomAppBarActionItems in home.dart ausführen:
Align(
alignment: AlignmentDirectional.bottomEnd,
child: IconButton(
icon: const Icon(Icons.search),
color: ReplyColors.white50,
onPressed: () {
Provider.of<RouterProvider>(
context,
listen: false,
).routePath = const ReplySearchPath();
},
),
);
Im Parameter onPressed greifen wir auf RouterProvider zu und legen dessen routePath auf ReplySearchPath fest. Unser RouterProvider verfolgt den Status unserer Root-Navigatoren.
mail_view_router.dart
Sehen wir uns nun an, wie die interne Navigation unserer App eingerichtet ist. Öffnen Sie dazu mail_view_router.dart im Verzeichnis lib. Es wird ein Navigator ähnlich dem oben gezeigten angezeigt:
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,
),
)
],
);
},
);
}
...
}
Das ist unser innerer Navigator. Sie verarbeitet die inneren Bildschirme unserer App, die nur den Hauptteil des Canvas verwenden, z. B. InboxPage. In InboxPage wird eine Liste von E‑Mail-Adressen angezeigt, je nachdem, in welchem Zustand sich das aktuelle Postfach in unserer App befindet. Der Navigator wird mit dem richtigen InboxPage oben im Stapel neu aufgebaut, wenn sich die Eigenschaft currentlySelectedInbox des App-Status ändert.
home.dart
Wir legen das aktuelle Postfach im Status unserer App fest, indem wir Folgendes in _HomePageState in home.dart ausführen:
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(() {});
}
In unserer _onDestinationSelected-Funktion greifen wir auf EmailStore zu und legen currentlySelectedInbox auf das ausgewählte Ziel fest. Unser EmailStore verfolgt den Status unserer internen Navigatoren.
home.dart
Ein Beispiel für die Verwendung von Navigationsrouting finden Sie in der Datei home.dart im Verzeichnis lib. Suchen Sie in der onTap-Property des InkWell-Widgets nach der Klasse _ReplyFabState. Sie sollte so aussehen:
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();
},
),
);
},
Hier sehen Sie, wie Sie ohne benutzerdefinierten Übergang zur Seite zum Verfassen von E-Mails gelangen. In diesem Codelab sehen Sie sich den Code von Reply an, um Material-Übergänge einzurichten, die in der gesamten App mit den verschiedenen Navigationsaktionen zusammenarbeiten.
Nachdem Sie sich mit dem Startcode vertraut gemacht haben, implementieren wir unsere erste Übergangsanimation.
5. Container-Übergangsmuster von der E‑Mail-Liste zur E‑Mail-Detailseite hinzufügen
Zuerst fügen Sie eine Übergangsanimation hinzu, die ausgelöst wird, wenn Sie auf eine E‑Mail klicken. Für diese Navigationsänderung eignet sich das Container-Übergangsmuster gut, da es für Übergänge zwischen UI-Elementen konzipiert wurde, die einen Container enthalten. Dieses Muster erzeugt eine sichtbare Verbindung zwischen zwei UI-Elementen.
Bevor Sie Code hinzufügen, sollten Sie die Reply App ausführen und auf eine E‑Mail klicken. Es sollte ein einfacher Jump-Cut erfolgen, d. h. der Bildschirm wird ohne Übergang ersetzt:
Vorher

Fügen Sie zuerst oben in mail_card_preview.dart einen Import für das Animationspaket ein, wie im folgenden Snippet gezeigt:
mail_card_preview.dart
import 'package:animations/animations.dart';
Nachdem Sie jetzt einen Import für das Animationspaket haben, können Sie Ihrer App ansprechende Übergänge hinzufügen. Beginnen wir mit dem Erstellen einer StatelessWidget-Klasse, in der sich unser OpenContainer-Widget befindet.
Fügen Sie in mail_card_preview.dart das folgende Code-Snippet nach der Klassendefinition von MailPreviewCard ein:
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,
);
},
);
}
}
Jetzt verwenden wir den neuen Wrapper. Innerhalb der Klassendefinition MailPreviewCard umschließen wir das Material-Widget aus unserer build()-Funktion mit unserem neuen _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(
...
Unser _OpenContainerWrapper hat ein InkWell-Widget und die Farbeigenschaften von OpenContainer definieren die Farbe des Containers, in dem es enthalten ist. Daher können wir die Material- und Inkwell-Widgets entfernen. Der resultierende Code sieht so aus:
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,
),
);
An diesem Punkt sollten Sie eine voll funktionsfähige Container-Transformation haben. Wenn Sie auf eine E‑Mail klicken, wird das Listenelement zu einem Detailbildschirm maximiert und die Liste der E‑Mails wird zurückgezogen. Wenn Sie auf „Zurück“ tippen, wird der Bildschirm mit den E‑Mail-Details wieder in ein Listenelement minimiert und gleichzeitig in der Liste der E‑Mails vergrößert.
Nachher

6. Container-Transform-Übergang vom FAB zur Seite zum Verfassen von E‑Mails hinzufügen
Fahren wir mit der Container-Transformation fort und fügen wir einen Übergang vom Floating Action Button zu ComposePage hinzu, indem wir den FAB in eine neue E-Mail erweitern, die vom Nutzer verfasst werden soll. Führen Sie die App zuerst noch einmal aus und klicken Sie auf den schwebenden Aktionsbutton, um zu sehen, dass beim Starten des Bildschirms zum Verfassen von E-Mails kein Übergang erfolgt.
Vorher

Die Konfiguration dieses Übergangs ähnelt sehr der im letzten Schritt, da wir dieselbe Widget-Klasse verwenden, nämlich OpenContainer.
Importieren wir in home.dart oben in der Datei package:animations/animations.dart und ändern wir die Methode _ReplyFabState build(). Wir umschließen das zurückgegebene Material-Widget mit einem OpenContainer-Widget:
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,
...
Zusätzlich zu den Parametern, die zum Konfigurieren des vorherigen OpenContainer-Widgets verwendet wurden, wird jetzt auch onClosed festgelegt. onClosed ist ein ClosedCallback, das aufgerufen wird, wenn die OpenContainer-Route entfernt wurde oder in den geschlossenen Zustand zurückgekehrt ist. Der Rückgabewert dieser Transaktion wird als Argument an diese Funktion übergeben. Wir verwenden diese Callback, um den Anbieter unserer App darüber zu informieren, dass wir die ComposePage-Route verlassen haben, damit er alle Listener benachrichtigen kann.
Ähnlich wie im letzten Schritt entfernen wir das Material-Widget aus unserem Widget, da das OpenContainer-Widget die Farbe des von closedBuilder mit closedColor zurückgegebenen Widgets verarbeitet. Außerdem entfernen wir den Navigator.push()-Aufruf im onTap unseres InkWell-Widgets und ersetzen ihn durch das openContainer() Callback des closedBuilder des OpenContainer-Widgets, da das OpenContainer-Widget jetzt sein eigenes Routing übernimmt.
Der resultierende Code sieht so aus:
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,
),
),
),
);
},
);
Jetzt müssen wir noch etwas alten Code bereinigen. Da unser OpenContainer-Widget den Anbieter unserer App jetzt über die onClosed ClosedCallback benachrichtigt, dass wir nicht mehr auf dem ComposePage sind, können wir unsere vorherige Implementierung in mail_view_router.dart entfernen:
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);
Das war's für diesen Schritt. Der Übergang vom FAB zum Compose-Bildschirm sollte so aussehen:
Nachher

7. Übergang mit gemeinsamer Z-Achse vom Suchsymbol zur Suchansichtsseite hinzufügen
In diesem Schritt fügen wir einen Übergang vom Suchsymbol zur Vollbild-Suchansicht hinzu. Da bei dieser Navigationsänderung kein persistenter Container beteiligt ist, können wir einen Übergang mit gemeinsamer Z-Achse verwenden, um die räumliche Beziehung zwischen den beiden Bildschirmen zu verdeutlichen und anzuzeigen, dass in der Hierarchie der App eine Ebene nach oben verschoben wird.
Bevor Sie zusätzlichen Code hinzufügen, sollten Sie die App ausführen und rechts unten auf dem Bildschirm auf das Suchsymbol tippen. Dadurch sollte der Bildschirm für die Suche ohne Übergang angezeigt werden.
Vorher

Rufen wir zuerst die Datei router.dart auf. Fügen Sie nach der Klassendefinition ReplySearchPath das folgende Snippet hinzu:
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;
});
}
}
Jetzt nutzen wir SharedAxisTransitionPageWrapper, um den gewünschten Übergang zu erzielen. In der Klassendefinition ReplyRouterDelegate umschließen wir den Suchbildschirm unter dem Attribut pages mit einem SharedAxisTransitionPageWrapper anstelle eines 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(),
),
],
);
Versuchen Sie nun, die App noch einmal auszuführen.

Es sieht schon richtig gut aus! Wenn Sie in der unteren App-Leiste auf das Suchsymbol klicken, wird die Suchseite durch eine Übergangsanimation mit gemeinsamer Achse eingeblendet. Beachten Sie jedoch, dass die Startseite nicht skaliert wird, sondern statisch bleibt, während die Suchseite darüber skaliert wird. Außerdem wird beim Drücken der Zurück-Schaltfläche die Startseite nicht skaliert, sondern bleibt statisch, während die Suchseite skaliert wird. Wir sind also noch nicht fertig.
Wir beheben beide Probleme, indem wir auch HomePage mit unserem SharedAxisTransitionWrapper anstelle von CustomTransitionPage umschließen:
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(),
),
],
);
Geschafft! Versuchen Sie nun, die App noch einmal auszuführen und auf das Suchsymbol zu tippen. Die Startseite und die Suchansicht sollten gleichzeitig ein- und ausgeblendet und entlang der Z-Achse skaliert werden, um einen nahtlosen Übergang zwischen den beiden Bildschirmen zu schaffen.
Nachher

8. „Fade Through“-Übergang zwischen Postfachseiten hinzufügen
In diesem Schritt fügen wir einen Übergang zwischen verschiedenen Postfächern hinzu. Da wir keine räumliche oder hierarchische Beziehung hervorheben möchten, verwenden wir eine Überblendung, um einen einfachen „Tausch“ zwischen Listen von E-Mails durchzuführen.
Bevor Sie zusätzlichen Code hinzufügen, führen Sie die App aus, tippen Sie in der unteren App-Leiste auf das Antwortlogo und wechseln Sie das Postfach. Die Liste der E-Mail-Adressen sollte ohne Übergang geändert werden.
Vorher

Rufen wir zuerst die Datei mail_view_router.dart auf. Fügen Sie nach der Klassendefinition MailViewRouterDelegate das folgende Snippet hinzu:
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;
});
}
}
Ähnlich wie im vorherigen Schritt verwenden wir die neue FadeThroughTransitionPageWrapper, um den gewünschten Übergang zu erzielen. Verwenden Sie in der Klassendefinition MailViewRouterDelegate unter dem Attribut pages anstelle von CustomTransitionPage die Funktion FadeThroughTransitionPageWrapper, um den Posteingangsbildschirm zu umschließen:
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),
),
],
);
Führen Sie die App noch einmal aus. Wenn Sie die untere Navigationsleiste öffnen und das Postfach wechseln, sollte die aktuelle Liste der E‑Mails ausgeblendet und verkleinert werden, während die neue Liste eingeblendet und vergrößert wird. Sehr gut!
Nachher

9. „Fade Through“-Übergang zwischen dem Compose- und dem Reply-Schaltfläche hinzufügen
In diesem Schritt fügen wir einen Übergang zwischen verschiedenen UAS-Symbolen hinzu. Da wir keine räumliche oder hierarchische Beziehung betonen möchten, verwenden wir eine Überblendung, um die Symbole im schwebenden Aktionsschaltfläche einfach zu tauschen.
Bevor Sie zusätzlichen Code hinzufügen, sollten Sie die App ausführen, auf eine E‑Mail tippen und die E‑Mail-Ansicht öffnen. Das UAS-Symbol sollte sich ohne Übergang ändern.
Vorher

Wir arbeiten für den Rest des Codelabs in home.dart. Sie müssen also den Import für das Animationspaket nicht hinzufügen, da wir das bereits in Schritt 2 für home.dart erledigt haben.
Die nächsten Übergänge werden sehr ähnlich konfiguriert, da sie alle eine wiederverwendbare Klasse, _FadeThroughTransitionSwitcher, verwenden.
Fügen Sie in home.dart das folgende Snippet unter _ReplyFabState ein:
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,
);
}
}
Suchen Sie nun in unserem _ReplyFabState nach dem Widget fabSwitcher. Die fabSwitcher gibt je nachdem, ob sie in der E‑Mail-Ansicht angezeigt wird oder nicht, ein anderes Symbol zurück. So sieht das Ganze mit unserem _FadeThroughTransitionSwitcher aus:
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,
),
);
...
Wir geben unserem _FadeThroughTransitionSwitcher einen transparenten fillColor, sodass beim Übergang kein Hintergrund zwischen den Elementen zu sehen ist. Außerdem erstellen wir eine UniqueKey und weisen sie einem der Symbole zu.
Jetzt sollten Sie einen vollständig animierten kontextbezogenen schwebenden Aktionsschaltfläche haben. Wenn Sie eine E‑Mail-Ansicht aufrufen, wird das alte Symbol für die Schaltfläche zum Erstellen einer neuen E‑Mail ausgeblendet und verkleinert, während das neue Symbol eingeblendet und vergrößert wird.
Nachher

10. Zwischen dem verschwindenden Postfach-Titel einen Übergang mit Ein- und Ausblenden einfügen
In diesem Schritt fügen wir einen Übergang mit Ein- und Ausblenden hinzu, um den Titel des Postfachs in der E‑Mail-Ansicht ein- und auszublenden. Da wir keine räumliche oder hierarchische Beziehung hervorheben möchten, verwenden wir eine Überblendung, um einen einfachen „Tausch“ zwischen dem Text-Widget, das den Postfachtitel umfasst, und einem leeren SizedBox durchzuführen.
Bevor Sie zusätzlichen Code hinzufügen, sollten Sie die App ausführen, auf eine E‑Mail tippen und die E‑Mail-Ansicht öffnen. Der Titel des Postfachs sollte ohne Übergang verschwinden.
Vorher

Der Rest dieses Codelabs ist schnell erledigt, da wir im letzten Schritt bereits den Großteil der Arbeit erledigt haben._FadeThroughTransitionSwitcher
Wechseln wir nun zur Klasse _AnimatedBottomAppBar in home.dart, um den Übergang hinzuzufügen. Wir verwenden _FadeThroughTransitionSwitcher aus dem letzten Schritt wieder und umschließen unsere onMailView-Bedingung, die entweder ein leeres SizedBox oder einen E‑Mail-Postfachtitel zurückgibt, der synchron mit der unteren Schublade eingeblendet wird:
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,
),
);
},
),
),
),
Das war alles. Wir sind mit diesem Schritt fertig.
Führen Sie die App noch einmal aus. Wenn Sie eine E‑Mail öffnen und zur E‑Mail-Ansicht weitergeleitet werden, sollte der Titel des Postfachs in der unteren App-Leiste ausgeblendet und verkleinert werden. Sehr gut!
Nachher

11. Einblenden-Übergang zwischen Aktionen in der unteren App-Leiste hinzufügen
In diesem Schritt fügen wir einen Übergang hinzu, um die Aktionen in der unteren App-Leiste basierend auf dem Kontext der Anwendung ein- und auszublenden. Da wir keine räumliche oder hierarchische Beziehung betonen möchten, verwenden wir eine Überblendung, um einen einfachen „Tausch“ zwischen den Aktionen der unteren App-Leiste durchzuführen, wenn sich die App auf der Startseite befindet, wenn das untere Drawer sichtbar ist und wenn wir uns in der E‑Mail-Ansicht befinden.
Bevor Sie zusätzlichen Code hinzufügen, sollten Sie die App ausführen, auf eine E‑Mail tippen und die E‑Mail-Ansicht öffnen. Sie können auch auf das Antwortlogo tippen. Die Aktionen in der unteren App-Leiste sollten ohne Übergang geändert werden.
Vorher

Ähnlich wie im vorherigen Schritt verwenden wir wieder _FadeThroughTransitionSwitcher. Um den gewünschten Übergang zu erzielen, gehen Sie zur Klassendefinition _BottomAppBarActionItems und umschließen Sie das zurückgegebene Widget der Funktion build() mit einem _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
...
Probieren wir es jetzt aus. Wenn Sie eine E‑Mail öffnen und zur E‑Mail-Ansicht weitergeleitet werden, sollten die alten Aktionen in der unteren App-Leiste ausgeblendet und verkleinert werden, während die neuen Aktionen eingeblendet und vergrößert werden. Gut gemacht!
Nachher

12. Glückwunsch!
Mit weniger als 100 Zeilen Dart-Code haben Sie mit dem Animationspaket ansprechende Übergänge in einer vorhandenen App erstellt, die den Material Design-Richtlinien entspricht und auf allen Geräten einheitlich aussieht und sich einheitlich verhält.

Weiteres Vorgehen
Weitere Informationen zum Material-Motion-System finden Sie in den Richtlinien und der vollständigen Entwicklerdokumentation. Probieren Sie aus, Ihrer App einige Material-Übergänge hinzuzufügen.
Vielen Dank, dass Sie Material Motion ausprobiert haben. Wir hoffen, dieses Codelab hat Ihnen gefallen.
Ich konnte dieses Codelab in angemessener Zeit und mit angemessenem Aufwand durcharbeiten.
Ich möchte das Material Motion-System auch in Zukunft verwenden.
Flutter Gallery ansehen
| Weitere Demos zur Verwendung von Widgets aus der Material-Flutter-Bibliothek und des Flutter-Frameworks finden Sie in der Flutter Gallery. |



