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

1. Einführung

Material Design ist ein System zum Erstellen ausdrucksstarker und ansprechender digitaler Produkte. Indem sie Stil, Branding, Interaktion und Bewegung unter einem einheitlichen Satz von Prinzipien und Komponenten vereinen, können Produktteams ihr größtes Designpotenzial ausschöpfen.

logo_components_color_2x_web_96dp.png

Material Components (MDC) unterstützen Entwickler bei der Implementierung von Material Design. MDC wurde von einem Team von Entwicklern und UX-Designern bei Google entwickelt und bietet Dutzende ansprechender und funktionaler UI-Komponenten. Es ist für Android, iOS, Web und Flutter verfügbar.material.io/develop

Was ist das Material Motion-System 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 zu bedienen, wie in den Material Design-Richtlinien beschrieben.

Die vier wichtigsten Materialübergangsmuster sind folgende:

  • Container Transform:Übergänge zwischen UI-Elementen, die einen Container enthalten; stellt eine sichtbare Verbindung zwischen zwei verschiedenen UI-Elementen her, indem ein Element nahtlos in ein anderes umgewandelt wird.

11807bdf36c66657.gif

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

71218f390abae07e.gif

  • Durchblenden: Übergänge zwischen UI-Elementen, die nicht stark miteinander verbunden sind. Es wird ein sequenzielles Aus- und Einblenden mit einer Skalierung des eingehenden Elements verwendet.

385ba37b8da68969.gif

  • Ausblenden:wird für UI-Elemente verwendet, die innerhalb des Bildschirms ein- oder ausgeblendet werden.

cfc40fd6e27753b6.gif

Das Animationspaket bietet Übergangs-Widgets für diese Muster, die auf der Flutter-Animationsbibliothek (flutter/animation.dart) und 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. Sie werden also mit Widgets arbeiten. :)

Umfang

In diesem Codelab erfahren Sie, wie Sie mit Dart einige Übergänge in einer Beispiel-Flutter-E-Mail-App namens Reply erstellen. Dabei wird gezeigt, wie Sie mithilfe von Übergängen aus dem Animationspaket das Erscheinungsbild Ihrer App anpassen können.

Der Code für den Einstieg in die Reply App wird bereitgestellt. Sie fügen der App die folgenden Material-Übergänge hinzu, die im GIF unten im abgeschlossenen Codelab zu sehen sind:

  • Umstellung von Container Transform von der E-Mail-Liste auf die Seite mit E-Mail-Details
  • Umstellung von Container Transform von der UAS-Seite zur Seite zum Schreiben von E-Mails
  • Gemeinsam genutzte Z-Achse: Übergang vom Suchsymbol zur Suchansicht
  • Überblenden mit dem Übergang zwischen den Postfachseiten
  • Übergang Überblenden zwischen Schreiben und Antworten FAB
  • Wechsel per Überblendung zwischen verschwindendem Postfachtitel
  • Übergang Überblenden zwischen Aktionen in der App-Leiste am unteren Rand

b26fe84fed12d17d.gif

Voraussetzungen

  • Grundkenntnisse in Flutter-Entwicklung und Dart
  • Code-Editor
  • Einen Android/iOS-Emulator oder ein Gerät
  • Beispielcode (siehe nächster Schritt)

Wie würden Sie Ihre Erfahrung mit der Entwicklung von Flutter-Apps bewerten?

Neuling Mittel Kompetent

Was möchten Sie in diesem Codelab lernen?

Ich kenne dieses Thema noch nicht und möchte eine gute Übersicht erhalten. Ich weiß etwas über dieses Thema, möchte aber mein Wissen auffrischen. Ich suche nach Beispielcode für mein Projekt. Ich suche nach einer Erklärung zu etwas Bestimmtem.

2. Flutter-Entwicklungsumgebung einrichten

Für dieses Lab benötigen Sie zwei Softwareprogramme: das Flutter SDK und einen Editor.

Sie können das Codelab auf jedem dieser Geräte ausführen:

  • Ein physisches Android- oder iOS-Gerät, das mit Ihrem Computer verbunden und auf den Entwicklermodus gesetzt ist.
  • Der iOS-Simulator (erfordert die Installation von Xcode-Tools).
  • Android-Emulator (Einrichtung in Android Studio erforderlich)
  • Ein Browser (zur Fehlerbehebung wird Chrome benötigt)
  • Als Windows-, Linux- oder macOS-Desktopanwendung Sie müssen die Entwicklung auf der Plattform durchführen, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktop-App entwickeln möchten, müssen Sie die Entwicklung unter Windows ausführen, damit Sie auf die entsprechende Build-Kette zugreifen können. Es gibt betriebssystemspezifische Anforderungen, die unter docs.flutter.dev/desktop ausführlich beschrieben werden.

3. Codelab-Starter-App herunterladen

Option 1: Start-Codelab-App von GitHub klonen

Führen Sie die folgenden Befehle aus, um dieses Codelab aus GitHub zu klonen:

git clone https://github.com/material-components/material-components-flutter-motion-codelab.git
cd material-components-flutter-motion-codelab

Option 2: Lade die Start-Codelab-App als ZIP-Datei herunter.

Die Start-App befindet sich im Verzeichnis material-components-flutter-motion-codelab-starter.

Projektabhängigkeiten prüfen

Das Projekt hängt vom animations-Paket ab. Beachten Sie in pubspec.yaml, dass der Abschnitt dependencies Folgendes enthält:

animations: ^2.0.0

Projekt öffnen und App ausführen

  1. Öffnen Sie das Projekt in einem beliebigen Editor.
  2. Folgen Sie der Anleitung zum Ausführen der App unter Erste Schritte: Testlauf für den ausgewählten Editor.

Fertig! Der Startcode für die Startseite von Antworten sollte auf deinem Gerät/deinem Emulator ausgeführt werden. Sie sollten den Posteingang mit einer Liste von E-Mails sehen.

Startseite von Reply

Optional: Geräteanimationen verlangsamen

Da dieses Codelab schnelle, aber ausgefeilte Übergänge enthält, kann es hilfreich sein, die Animationen des Geräts zu verlangsamen, um bei der Implementierung einige Details der Übergänge zu beobachten. Das ist über eine In-App-Einstellung möglich, auf die Sie zugreifen können, indem Sie auf das Einstellungssymbol tippen, wenn die untere Leiste geöffnet ist. Keine Sorge, diese Methode zum Verlangsamen der Geräteanimationen hat keine Auswirkungen auf Animationen auf dem Gerät außerhalb der Reply App.

d23a7bfacffac509.gif

Optional: Dunkler Modus

Wenn das helle Design von Reply Ihre Augen anstrengt, haben wir genau das Richtige für Sie. Die App hat eine Einstellung, mit der du das App-Design in den dunklen Modus ändern kannst. Wenn die untere Schublade geöffnet ist, können Sie auf das Symbol „Einstellungen“ tippen, um diese Einstellung aufzurufen.

87618d8418eee19e.gif

4. Mit dem Beispielcode der App vertraut machen

Sehen wir uns den Code an. Wir haben eine App bereitgestellt, die das Animationspaket für den Wechsel zwischen verschiedenen Bildschirmen innerhalb der App verwendet.

  • Startseite: das ausgewählte Postfach
  • InboxPage Eine Liste von E-Mails wird angezeigt.
  • MailPreviewCard: zeigt die Vorschau einer E-Mail an.
  • MailViewPage: Zeigt eine einzelne, vollständige E-Mail an.
  • ComposePage:Hiermit können Sie eine neue E-Mail verfassen.
  • SearchPage:Zeigt eine Suchansicht an.

router.dart

Wenn Sie wissen möchten, wie die Root-Navigation der App eingerichtet ist, öffnen Sie router.dart im Verzeichnis 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);
 }
}

Dies ist unser Stammnavigationselement, das die Bildschirme unserer App verwaltet, die den gesamten Canvas belegen, z. B. HomePage und SearchPage. Es überwacht den Status unserer App, um zu prüfen, ob wir die Route zum ReplySearchPath festgelegt haben. Ist dies der Fall, wird der Navigator mit SearchPage oben im Stack neu erstellt. Beachten Sie, dass unsere Bildschirme in einem CustomTransitionPage-Element ohne definierte Übergänge verpackt sind. Hier sehen Sie eine Möglichkeit, zwischen Bildschirmen zu wechseln, ohne einen benutzerdefinierten Übergang zu verwenden.

home.dart

Wir legen unsere Route im Status unserer App auf ReplySearchPath fest, indem wir in _BottomAppBarActionItems in home.dart Folgendes tun:

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. RouterProvider erfasst den Status des Root-Navigators.

mail_view_router.dart

Sehen wir uns nun an, wie das innere Navigationsmenü der App eingerichtet ist. Öffnen Sie mail_view_router.dart im Verzeichnis lib. Es wird ein Navigator angezeigt, der dem obigen ähnelt:

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. Er verarbeitet die inneren Bildschirme unserer App, die nur den Canvas-Textkörper belegen, z. B. die InboxPage. In der InboxPage wird eine Liste mit E-Mail-Adressen angezeigt, je nachdem, welchen Status die App aktuell hat. Wenn sich die currentlySelectedInbox-Eigenschaft des App-Status ändert, wird der Navigator neu erstellt, wobei die richtige InboxPage oben im Stapel angezeigt wird.

home.dart

Wir setzen unser aktuelles Postfach auf den Status unserer App, indem wir innerhalb von _HomePageState in home.dart Folgendes tun:

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 der Funktion _onDestinationSelected greifen wir auf unsere EmailStore zu und legen ihre currentlySelectedInbox auf das ausgewählte Ziel fest. Unser EmailStore überwacht den Status des internen Navigationssystems.

home.dart

Wenn Sie sich ein Beispiel für ein verwendetes Navigations-Routing ansehen möchten, öffnen Sie home.dart im Verzeichnis lib. Suchen Sie in der Property onTap 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 benutzerdefinierte Übergänge zur Seite zum Verfassen von E-Mails gelangen. In diesem Codelab befassen wir uns mit dem Code von Reply, um Materialübergänge einzurichten, die zusammen mit den verschiedenen Navigationsaktionen in der App funktionieren.

Nachdem Sie sich mit dem Startcode vertraut gemacht haben, implementieren wir jetzt unsere erste Überleitung.

5. Container-Übergangsmuster von der E-Mail-Liste zur E-Mail-Detailseite hinzufügen

Als Erstes fügen Sie einen Übergang hinzu, wenn Sie auf eine E-Mail klicken. Für diese Navigationsänderung eignet sich das Containertransformationsmuster gut, da es für Übergänge zwischen UI-Elementen vorgesehen ist, die einen Container enthalten. Dieses Muster schafft eine sichtbare Verbindung zwischen zwei UI-Elementen.

Bevor Sie Code hinzufügen, versuchen Sie, die Antwort-App auszuführen und auf eine E-Mail zu klicken. Es sollte ein einfacher Jump-Cut verwendet werden, 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 hinzu, wie im folgenden Snippet gezeigt:

mail_card_preview.dart

import 'package:animations/animations.dart';

Nachdem Sie nun einen Import für das Animationspaket erstellt haben, können wir Ihrer App ansprechende Übergänge hinzufügen. Erstellen Sie als Erstes eine StatelessWidget-Klasse, in der unser OpenContainer-Widget gespeichert wird.

Fügen Sie in mail_card_preview.dart nach der Klassendefinition der MailPreviewCard das folgende Code-Snippet hinzu:

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 wenden wir unseren neuen Wrapper an. Innerhalb der Klassendefinition MailPreviewCard umschließen wir das Material-Widget aus der build()-Funktion mit der 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(
...

Die _OpenContainerWrapper hat ein InkWell-Widget und die Farbeigenschaften von OpenContainer definieren die Farbe des eingeschlossenen Containers. 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,
 ),
);

In dieser Phase sollten Sie über eine voll funktionsfähige Containertransformation verfügen. Durch Klicken auf eine E-Mail-Adresse wird das Listenelement zu einem Detailbildschirm erweitert, während die Liste der E-Mail-Adressen zurückgeht. Wenn Sie auf „Zurück“ klicken, wird der Bildschirm mit den E-Mail-Details wieder zu einem Listenelement minimiert und die Liste der E-Mails wird maximiert.

Nachher

663e8594319bdee3.gif

6. Container Transform-Umstellung von FAB zum Schreiben von E-Mails hinzufügen

Fahren wir mit der Containertransformation fort und fügen Sie einen Übergang von der unverankerten Aktionsschaltfläche zu ComposePage hinzu, um die UAS auf eine neue E-Mail zu erweitern, die vom Nutzer geschrieben werden soll. Führen Sie die App zuerst noch einmal aus und klicken Sie auf die FAB. Sie sollten feststellen, dass beim Aufrufen des Bildschirms zum Verfassen von E-Mails kein Übergang erfolgt.

Vorher

4aa2befdc5170c60.gif

Die Konfiguration dieses Übergangs ähnelt der im letzten Schritt, da wir dieselbe Widget-Klasse verwenden, den OpenContainer.

Importieren Sie in home.dart die package:animations/animations.dart am Anfang der Datei und ändern Sie die _ReplyFabState build()-Methode. Verpacken wir 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 zur Konfiguration des vorherigen OpenContainer-Widgets verwendet wurden, wird jetzt auch onClosed festgelegt. onClosed ist ein ClosedCallback, der aufgerufen wird, wenn die OpenContainer-Route entfernt wurde oder in den geschlossenen Status zurückgekehrt ist. Der Rückgabewert dieser Transaktion wird als Argument an diese Funktion übergeben. Mit dieser Callback benachrichtigen wir den Anbieter unserer App, dass wir die ComposePage-Route verlassen haben, damit er alle Listener benachrichtigen kann.

Ähnlich wie beim letzten Schritt entfernen wir das Material-Widget aus unserem Widget, da das OpenContainer-Widget die Farbe des Widgets verarbeitet, das vom closedBuilder mit closedColor zurückgegeben wird. Außerdem entfernen wir den Navigator.push()-Aufruf in der onTap des InkWell-Widgets und ersetzen ihn durch die openContainer() Callback, die von der closedBuilder des OpenContainer-Widgets angegeben wird, da das OpenContainer-Widget jetzt seine eigene Weiterleitung verarbeitet.

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 bereinigen wir etwas alten Code. Da unser OpenContainer-Widget jetzt über die onClosed ClosedCallback den Anbieter unserer App darüber informiert, dass wir nicht mehr auf der 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 Floating Action Button zum Bildschirm zum Verfassen einer Nachricht sollte so aussehen:

Nachher

5c7ad1b4b40f9f0c.gif

7. Gemeinsamen Z‑Achsen-Übergang vom Suchsymbol zur Suchansicht hinzufügen

In diesem Schritt fügen wir einen Übergang vom Suchsymbol zur Suchansicht im Vollbildmodus hinzu. Da bei dieser Navigationsänderung kein persistenter Container verwendet wird, können wir einen gemeinsamen Z‑Achsenübergang verwenden, um die räumliche Beziehung zwischen den beiden Bildschirmen zu verstärken und anzuzeigen, dass man sich in der Hierarchie der App um eine Ebene nach oben bewegt.

Bevor Sie zusätzlichen Code hinzufügen, starten Sie die App und tippen Sie rechts unten auf dem Bildschirm auf das Suchsymbol. Daraufhin sollte der Bildschirm für die Suchansicht ohne Übergang angezeigt werden.

Vorher

df7683a8ad7b920e.gif

Öffnen Sie zuerst die Datei router.dart. Fügen Sie nach der ReplySearchPath-Klassendefinition 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;
       });
 }
}

Nutzen wir jetzt unsere neue SharedAxisTransitionPageWrapper, um die gewünschte Umstellung zu erreichen. In der ReplyRouterDelegate-Klassendefinition unter der Eigenschaft pages umschließen wir den Suchbildschirm 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 jetzt alles super aus! Wenn Sie auf das Suchsymbol in der unteren App-Leiste klicken, wird die Suchseite durch einen gemeinsamen Achsenübergang eingeblendet. Beachten Sie jedoch, dass die Startseite nicht hochskaliert wird, sondern statisch bleibt, wenn die Suchseite darüber skaliert wird. Wenn Sie außerdem auf die Schaltfläche „Zurück“ klicken, wird die Startseite nicht maximiert, sondern bleibt statisch, während die Suchseite maximiert wird. Wir sind also noch nicht fertig.

Wir beheben beide Probleme, indem wir das HomePage-Element auch in unseren SharedAxisTransitionWrapper statt in CustomTransitionPage setzen:

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(),
     ),
 ],
);

Fertig! Starten Sie die App jetzt noch einmal und tippen Sie auf das Suchsymbol. Die Startbildschirme und Suchansichtsbildschirme sollten gleichzeitig entlang der Z-Achse in der Tiefe verblassen und skaliert werden, um einen nahtlosen Übergang zwischen den beiden Bildschirmen zu schaffen.

Nachher

462d890086a3d18a.gif

8. Zwischen den Seiten des Postfachs den Übergang „Weich ausblenden“ hinzufügen

In diesem Schritt fügen wir einen Übergang zwischen verschiedenen Postfächern hinzu. Da wir keine räumliche oder hierarchische Beziehung betonen möchten, verwenden wir einen Fade-Through-Effekt, um einen einfachen "Wechsel" zwischen E-Mail-Listen durchzuführen.

Bevor Sie zusätzlichen Code hinzufügen, versuchen Sie, die App auszuführen, auf das Antwort-Logo in der unteren App-Leiste zu tippen und das Postfach zu wechseln. Die Liste der E-Mails sollte sich ohne Übergang ändern.

Vorher

89033988ce26b92e.gif

Öffnen Sie zuerst die Datei mail_view_router.dart. Fügen Sie nach der MailViewRouterDelegate-Klassendefinition 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 letzten Schritt verwenden wir unsere neue FadeThroughTransitionPageWrapper, um die gewünschte Umstellung zu erreichen. In der MailViewRouterDelegate-Klassendefinition unter der pages-Eigenschaft verwenden wir anstelle von CustomTransitionPage das Element FadeThroughTransitionPageWrapper, um den Posteingangsbildschirm einzufassen:

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),
   ),
 ],
);

Starten Sie die App neu. Wenn Sie die untere Navigationsleiste öffnen und den Posteingang wechseln, sollte die aktuelle E-Mail-Liste verblassen und verkleinert werden, während die neue Liste verblasst und vergrößert wird. Sehr gut!

Nachher

8186940082b630d.gif

9. Zwischen den FABs „Schreiben“ und „Antworten“ einen Übergang 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 einen einfachen „Wechsel“ zwischen den Symbolen im FAB durchzuführen.

Bevor Sie zusätzlichen Code hinzufügen, versuchen Sie, die App auszuführen, auf eine E-Mail zu tippen und die E-Mail-Ansicht zu öffnen. Das Symbol für die Floating Action Button sollte sich ohne Übergang ändern.

Vorher

d8e3afa0447cfc20.gif

Für den Rest des Codelabs arbeiten wir in home.dart. Sie müssen also keinen Import für das Animationspaket hinzufügen, da wir das bereits in Schritt 2 für home.dart getan haben.

Die Konfiguration der nächsten Übergänge wird sehr ähnlich sein, da sie alle die wiederverwendbare Klasse _FadeThroughTransitionSwitcher verwenden.

Fügen wir in home.dart das folgende Snippet unter _ReplyFabState hinzu:

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 jetzt in der _ReplyFabState nach dem fabSwitcher-Widget. fabSwitcher gibt ein anderes Symbol zurück, je nachdem, ob es sich in der E-Mail-Ansicht befindet oder nicht. Lassen Sie uns dies mit _FadeThroughTransitionSwitcher abschließen:

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 ein transparentes fillColor, damit 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 über eine vollständig animierte kontextbezogene UAS verfügen. Wenn Sie eine E-Mail-Ansicht öffnen, wird das alte FAB-Symbol ausgeblendet und skaliert, während das neue Symbol verblasst und herunterskaliert.

Nachher

c55bacd9a144ec69.gif

10. Übergang zwischen ausgeblendetem Postfachtitel hinzufügen

In diesem Schritt fügen wir einen Übergang hinzu, mit dem der Postfachtitel in der E-Mail-Ansicht zwischen dem sichtbaren und dem unsichtbaren Status hindurchgeblendet wird. Da wir keine räumliche oder hierarchische Beziehung betonen möchten, verwenden wir einen einfachen Wechsel zwischen dem Text-Widget, das den Postfachtitel enthält, und einem leeren SizedBox.

Bevor Sie zusätzlichen Code hinzufügen, versuchen Sie, die App auszuführen, auf eine E-Mail zu tippen und die E-Mail-Ansicht zu öffnen. Der Titel des Postfachs sollte ohne Übergang verschwinden.

Vorher

59eb57a6c71725c0.gif

Der Rest dieses Codelabs wird schnell erledigt, da wir die meisten Schritte im _FadeThroughTransitionSwitcher bereits im letzten Schritt erledigt haben.

Rufen wir jetzt die _AnimatedBottomAppBar-Klasse in home.dart auf, um den Übergang hinzuzufügen. Wir verwenden _FadeThroughTransitionSwitcher aus unserem letzten Schritt wieder und umschließen die onMailView-Bedingung, die entweder ein leeres SizedBox zurückgibt oder einen Postfachtitel, der mit der unteren Leiste synchronisiert 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's schon.

Starten Sie die App noch einmal. 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 hochskaliert werden. Super!

Nachher

3f1a3db01a481124.gif

11. Zwischen den Aktionen in der unteren App-Leiste einen Übergang hinzufügen

In diesem Schritt fügen wir einen Übergang hinzu, mit dem die Aktionen der unteren App-Leiste je nach App-Kontext ausgeblendet werden. Da wir keine räumliche oder hierarchische Beziehung hervorheben möchten, verwenden wir ein Ausblenden, um eine einfache „Auswechslung“ zwischen den Aktionen in der unteren App-Leiste vorzunehmen, wenn sich die App auf der Startseite befindet, wenn die untere Leiste sichtbar ist und wenn wir uns in der E-Mail-Ansicht befinden.

Bevor Sie zusätzlichen Code hinzufügen, starten Sie die App, tippen Sie auf eine E-Mail und öffnen Sie die E-Mail-Ansicht. Du kannst auch auf das Antwortlogo tippen. Die Aktionen der unteren App-Leiste sollten sich ohne Übergang ändern.

Vorher

5f662eac19fce3ed.gif

Ähnlich wie im letzten Schritt verwenden wir wieder _FadeThroughTransitionSwitcher. Um den gewünschten Übergang zu erzielen, rufen Sie die _BottomAppBarActionItems-Klassendefinition auf und umschließen Sie das Rückgabe-Widget der build()-Funktion 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 verblassen und herausgezoomt werden, während die neuen Aktionen verblassen und herangezoomt werden. Gut gemacht!

Nachher

cff0fa2afa1c5a7f.gif

12. Glückwunsch!

Mit weniger als 100 Zeilen Dart-Code hat das Animationspaket dabei geholfen, schöne Übergänge in einer vorhandenen App zu erstellen, die den Material Design-Richtlinien entspricht und auf allen Geräten einheitlich aussieht und funktioniert.

d5637de49eb64d8a.gif

Weiteres Vorgehen

Weitere Informationen zum Material-Motion-System findest du in den Richtlinien und der vollständigen Entwicklerdokumentation. Versuche auch, deiner App einige Materialübergänge hinzuzufügen.

Vielen Dank, dass du Material Motion ausprobiert hast. Wir hoffen, dass Ihnen dieses Codelab gefallen hat.

Ich konnte dieses Codelab mit angemessenem Zeit- und Arbeitsaufwand abschließen

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

Ich möchte das Bewegungssystem Material 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 dem Flutter-Framework finden Sie in der Flutter Gallery.

46ba920f17198998.png

6ae8ae284bf4f9fa.png