Mit Material Motion für Flutter wunderschöne Übergänge erstellen

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.

logo_components_color_2x_web_96dp.png

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.

11807bdf36c66657.gif

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

71218f390abae07e.gif

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

385ba37b8da68969.gif

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

cfc40fd6e27753b6.gif

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

b26fe84fed12d17d.gif

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?

Anfänger Mittelstufe Fortgeschrittene

Was möchten Sie in diesem Codelab lernen?

Ich bin neu auf diesem Gebiet und möchte einen guten Überblick. Ich weiß etwas über dieses Thema, möchte aber mein Wissen auffrischen. Ich suche Beispielcode für mein Projekt. Ich suche nach einer Erklärung für etwas Bestimmtes.

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

  1. Öffnen Sie das Projekt in einem beliebigen Editor.
  2. 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.

Startseite für Antworten

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.

d23a7bfacffac509.gif

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.

87618d8418eee19e.gif

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

48b00600f73c7778.gif

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

663e8594319bdee3.gif

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

4aa2befdc5170c60.gif

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

5c7ad1b4b40f9f0c.gif

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

df7683a8ad7b920e.gif

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.

81b3ea098926931.gif

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

462d890086a3d18a.gif

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

89033988ce26b92e.gif

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

8186940082b630d.gif

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

d8e3afa0447cfc20.gif

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

c55bacd9a144ec69.gif

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

59eb57a6c71725c0.gif

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

3f1a3db01a481124.gif

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

5f662eac19fce3ed.gif

Ä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

cff0fa2afa1c5a7f.gif

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.

d5637de49eb64d8a.gif

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.

Stimme vollkommen zu Stimme zu Neutral Stimme nicht zu Stimme überhaupt nicht zu

Ich möchte das Material Motion-System auch in Zukunft verwenden.

Stimme vollkommen zu Stimme zu Neutral Stimme nicht zu Stimme überhaupt nicht zu

Weitere Demos zur Verwendung von Widgets aus der Material-Flutter-Bibliothek und des Flutter-Frameworks finden Sie in der Flutter Gallery.

46ba920f17198998.png

6ae8ae284bf4f9fa.png