Tworzenie pięknych przejść dzięki Material Motion dla Flutter

1. Wprowadzenie

Material Design to system do tworzenia odważnych i atrakcyjnych produktów cyfrowych. Łącząc styl, budowanie marki, interakcję i ruch w ramach spójnego zestawu zasad i komponentów, zespoły zajmujące się poszczególnymi produktami i usługami mogą w pełni wykorzystać swój potencjał projektowy.

logo_components_color_2x_web_96dp.png

Material Komponenty (MDC) pomagają deweloperom wdrażać interfejs Material Design. MDC, stworzona przez zespół inżynierów i projektantów UX w Google, zawiera dziesiątki pięknych i funkcjonalnych komponentów interfejsu. Jest dostępny na Androida, iOS, internet oraz Flutter.material.io/develop

Czym jest system ruchu Material Design na potrzeby Flutter?

System animacji Material Design na potrzeby technologii Flutter to zestaw wzorców przejść w pakiecie animacji, który może pomóc użytkownikom zrozumieć aplikację i poruszać się po niej, zgodnie z opisem w wytycznych dotyczących stylu Material Design.

Oto 4 główne wzorce przejść z użyciem materiału:

  • Przekształcenie kontenera: przejścia między elementami interfejsu, które zawierają kontener. tworzy widoczne połączenie między dwoma różnymi elementami interfejsu, płynnie przekształcając jeden element w drugi.

11807bdf36c66657.gif

  • Wspólna oś: przejścia między elementami interfejsu, które są powiązane przestrzenną lub nawigacyjną. korzysta z udostępnianego przekształcenia na osi x, y lub z, aby wzmocnić relacje między elementami.

71218f390abae07e.gif

  • Przenikanie: przejścia między elementami interfejsu, które nie są ze sobą ściśle powiązane. używa sekwencyjnego zanikania i zanikania ze skalą elementu przychodzącego.

385ba37b8da68969.gif

  • Zanikanie: opcja używana w przypadku elementów interfejsu, które pojawiają się lub znikają na ekranie.

cfc40fd6e27753b6.gif

Pakiet animacji zawiera widżety przejść dla tych wzorców, utworzone na podstawie biblioteki animacji Flutter (flutter/animation.dart) oraz biblioteki materiałów Flutter (flutter/material.dart):

W ramach tego ćwiczenia w Codelabs będziesz używać przejścia Material Design bazujące na platformie Flutter i bibliotece Material, co oznacza, że będziesz korzystać z widżetów. :)

Co utworzysz

Dzięki temu ćwiczeniu w Codelabs dowiesz się, jak tworzyć przejścia z przykładowej aplikacji e-mailowej Flutter o nazwie Reply z użyciem Dart, aby zademonstrować, jak za pomocą przejść z pakietu animacji możesz dostosować wygląd i sposób działania aplikacji.

Otrzymasz kod startowy aplikacji Reply, a Ty wykorzystasz w niej te przejścia Material Design, które możesz zobaczyć w tym GIF-ie z ukończonego ćwiczenia w Codelabs:

  • Przejście ze przekształcenia kontenera ze strony listy e-mailowej na stronę z informacjami o e-mailu
  • Przejście ze Przekształcenia kontenera ze strony typu FAB na stronę tworzenia e-maila
  • Wspólna oś Z – przejście z ikony wyszukiwania na stronę widoku wyszukiwania
  • Przejście zanikanie przez strony skrzynki pocztowej
  • Przenikanie – przejście między tworzeniem i odpowiadaniem za pomocą przycisku PPP
  • Przejście z efektem zanikania między znikającym tytułem skrzynki pocztowej
  • Przenikanie między działaniami na dolnym pasku aplikacji

b26fe84fed12d17d.gif

Czego potrzebujesz

  • Podstawowa wiedza o programowaniu Flutter i Dart
  • Edytor kodu
  • Emulatora lub urządzenia z Androidem/iOS
  • Przykładowy kod (patrz następny krok)

Jak oceniasz swój poziom doświadczenia w tworzeniu aplikacji Flutter?

Początkujący Poziom średnio zaawansowany Biegły

Czego chcesz się dowiedzieć z tego ćwiczenia z programowania?

Jestem w tym nowym temacie i chcę uzyskać ogólne informacje na ten temat. Wiem coś na ten temat, ale chcę odświeżyć informacje. Szukam przykładowego kodu do użycia w moim projekcie. Potrzebuję wyjaśnienia czegoś konkretnego.

2. Konfigurowanie środowiska programistycznego Flutter

Aby ukończyć ten moduł, potrzebujesz 2 oprogramowania: pakietu SDK Flutter i edytora.

Ćwiczenie z programowania możesz uruchomić na dowolnym z tych urządzeń:

  • Fizyczne urządzenie z Androidem lub iOS podłączone do komputera i ustawione w trybie programisty.
  • Symulator iOS (wymaga zainstalowania narzędzi Xcode).
  • Emulator Androida (wymaga skonfigurowania Android Studio).
  • Przeglądarka (do debugowania wymagany jest Chrome).
  • Aplikacja komputerowa w systemie Windows, Linux lub macOS Programowanie należy tworzyć na platformie, na której zamierzasz wdrożyć usługę. Jeśli więc chcesz opracować aplikację komputerową dla systemu Windows, musisz to zrobić w tym systemie, aby uzyskać dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania związane z konkretnymi systemami operacyjnymi, które zostały szczegółowo omówione na stronie docs.flutter.dev/desktop.

3. Pobierz aplikację startową w Codelabs

Opcja 1. Sklonowanie z GitHuba aplikacji startowej z programowania

Aby skopiować to ćwiczenie z programowania z GitHuba, uruchom te polecenia:

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

Opcja 2. Pobierz plik ZIP aplikacji startowej z programowania

Aplikacja startowa znajduje się w katalogu material-components-flutter-motion-codelab-starter.

Sprawdzanie zależności projektu

Projekt zależy od pakietu animacji. Zwróć uwagę na sekcję dependencies w pubspec.yaml:

animations: ^2.0.0

Otwieranie projektu i uruchamianie aplikacji

  1. Otwórz projekt w wybranym edytorze.
  2. Postępuj zgodnie z instrukcjami „Uruchom aplikację” w artykule Rozpocznij: jazdę próbną dla wybranego edytora.

Gotowe! Kod startowy strony głównej Reply powinien uruchomić się na Twoim urządzeniu/emulatorze. Powinna wyświetlić się skrzynka odbiorcza z listą e-maili.

Odpowiedz na stronę główną

Opcjonalnie: spowalnianie animacji na urządzeniu

Ponieważ ćwiczenie w Codelabs wymaga szybkich, ale dopracowanych przejść, warto spowolnić animacje na urządzeniu i obserwować szczegóły. Można to zrobić za pomocą ustawienia w aplikacji. Aby je wyświetlić, wystarczy kliknąć ikonę ustawień, gdy dolna szuflada jest otwarta. Bez obaw – ta metoda spowalniania animacji na urządzeniu nie ma wpływu na animacje widoczne poza aplikacją Reply.

d23a7bfacffac509.gif

Opcjonalnie: tryb ciemny

Jeśli jasny motyw odpowiedzi szkodzi Ci, nie szukaj dalej. W aplikacji dostępne jest ustawienie, które umożliwia zmianę motywu aplikacji na tryb ciemny, aby lepiej pasowały do Twoich oczu. Dostęp do tego ustawienia można uzyskać, klikając ikonę ustawień, gdy otwarta jest dolna szuflada.

87618d8418eee19e.gif

4. Zapoznaj się z kodem przykładowej aplikacji

Spójrzmy na kod. Opracowaliśmy aplikację, która korzysta z pakietu animacji do przełączania się między różnymi ekranami.

  • HomePage (Strona główna): wyświetla wybraną skrzynkę pocztową.
  • InboxPage: wyświetla listę e-maili
  • MailPreviewCard: wyświetla podgląd e-maila
  • MailViewPage: wyświetla pojedynczy, pełny e-mail
  • ComposePage: umożliwia tworzenie nowego e-maila.
  • SearchPage: wyświetla widok wyszukiwania.

router.dart

Aby dowiedzieć się, jak skonfigurowano nawigację główną aplikacji, otwórz router.dart w katalogu 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);
 }
}

To główny nawigator, który obsługuje ekrany aplikacji, które zajmują cały obszar roboczy, np. HomePage i SearchPage. Nasłuchuje stanu naszej aplikacji, aby sprawdzić, czy została ustawiona trasa na ReplySearchPath. Jeśli tak, odbudowuje nawigator z elementem SearchPage u góry stosu. Zwróć uwagę, że nasze ekrany są otoczone warstwą CustomTransitionPage bez zdefiniowanego przejścia. Pokazuje jeden sposób poruszania się między ekranami bez niestandardowych przejść.

home.dart

Ustawiliśmy trasę na ReplySearchPath w stanie aplikacji, wykonując te czynności w narzędziu _BottomAppBarActionItems w home.dart:

Align(
 alignment: AlignmentDirectional.bottomEnd,
 child: IconButton(
   icon: const Icon(Icons.search),
   color: ReplyColors.white50,
   onPressed: () {
     Provider.of<RouterProvider>(
       context,
       listen: false,
     ).routePath = const ReplySearchPath();
   },
 ),
);

W parametrze onPressed uzyskujemy dostęp do elementu RouterProvider i ustawiamy jego routePath na ReplySearchPath. RouterProvider śledzi stan głównego nawigatora.

mail_view_router.dart

Zobaczmy, jak skonfigurowana jest wewnętrzna nawigacja w aplikacji. Otwórz aplikację mail_view_router.dart w katalogu lib. Wyświetli się nawigator podobny do tego powyżej:

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

To jest nasz wewnętrzny nawigator. Obsługuje wewnętrzne ekrany aplikacji, które zajmują tylko część obszaru roboczego, np. InboxPage. InboxPage wyświetla listę e-maili w zależności od obecnego stanu naszej aplikacji. Nawigator jest odbudowywany z prawidłowym elementem InboxPage nałożonym na stos za każdym razem, gdy zmieni się właściwość currentlySelectedInbox stanu aplikacji.

home.dart

Ustawiliśmy obecną skrzynkę odbiorczą w stanie naszej aplikacji, wykonując te czynności w usłudze _HomePageState w aplikacji home.dart:

void _onDestinationSelected(String destination) {
 var emailStore = Provider.of<EmailStore>(
   context,
   listen: false,
 );

 if (emailStore.onMailView) {
   emailStore.currentlySelectedEmailId = -1;
 }

 if (emailStore.currentlySelectedInbox != destination) {
   emailStore.currentlySelectedInbox = destination;
 }

 setState(() {});
}

Funkcja _onDestinationSelected uzyskuje dostęp do elementu EmailStore i ustawia jego currentlySelectedInbox na wybrane miejsce docelowe. Urządzenie EmailStore śledzi stan naszych wewnętrznych nawigatorów.

home.dart

Aby zobaczyć przykład użycia routingu nawigacji, otwórz plik home.dart w katalogu lib. Znajdź klasę _ReplyFabState we właściwości onTap widżetu InkWell, która powinna wyglądać tak:

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

Tutaj możesz zobaczyć, jak możesz przejść do strony tworzenia e-maila bez wykonywania niestandardowych przejść. Podczas tego ćwiczenia w programie zagłębisz się w kod usługi Reply, aby skonfigurować przejścia w materiałach, które będą współdziałać z różnymi działaniami nawigacyjnymi w aplikacji.

Teraz, gdy znasz już kod startowy, zaimplementujmy pierwsze przejście.

5. Dodaj przejście z listy e-mailowej na stronę z informacjami o e-mailu w ramach transformacji kontenera

Najpierw musisz dodać przejście po kliknięciu e-maila. Na potrzeby tej zmiany nawigacji dobrze sprawdza się wzorzec przekształcenia kontenera, ponieważ służy do przejść między elementami interfejsu, które zawierają kontener. Ten wzorzec tworzy widoczne połączenie między 2 elementami interfejsu.

Zanim dodasz kod, uruchom aplikację Reply i kliknij e-maila. Powinien wykonać proste cięcie skokowe, co oznacza, że ekran będzie zastępowany brakiem przejścia:

Przed

48b00600f73c7778.gif

Zacznij od dodania na początku kodu mail_card_preview.dart importu pakietu animacji, jak w tym fragmencie kodu:

mail_card_preview.dart

import 'package:animations/animations.dart';

Po zaimportowaniu pakietu animacji możemy zacząć dodawać do aplikacji atrakcyjne przejścia. Zacznijmy od utworzenia zajęć StatelessWidget, które będą zawierać nasz widżet OpenContainer.

W narzędziu mail_card_preview.dart dodaj po definicji klasy MailPreviewCard ten fragment kodu:

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

Teraz użyjmy nowego otoczenia. W definicji klasy MailPreviewCard dodamy widżet Material z funkcji build() o nową wersję _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(
...

Nasz _OpenContainerWrapper ma widżet InkWell, a właściwości koloru OpenContainer określają kolor kontenera, który zawiera. Dlatego możemy usunąć widżety Material i Inkwell. Wynikowy kod wygląda tak:

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

Na tym etapie powinno być w pełni działające przekształcenie kontenera. Kliknięcie e-maila powoduje rozwinięcie pozycji na liście do ekranu szczegółów, a jednocześnie cofanie się listy. Naciśnięcie przycisku wstecz zwija ekran ze szczegółami e-maila z powrotem do elementu listy i skaluje go w górę na liście e-maili.

Po

663e8594319bdee3.gif

6. Dodaj przejście przekształcenia kontenera ze strony typu FAB na stronę tworzenia e-maila

Wróćmy do przekształcenia kontenera i dodamy przejście z pływającego przycisku polecenia ComposePage do rozwijania przycisku typu FAB do nowego e-maila do napisania przez użytkownika. Najpierw uruchom aplikację ponownie i kliknij przycisk FAB, aby zobaczyć, że po uruchomieniu ekranu tworzenia e-maila nie ma żadnego przejścia.

Przed

4aa2befdc5170c60.gif

Sposób, w jaki skonfigurujemy to przejście, będzie bardzo podobny do tego w poprzednim kroku, ponieważ używamy tej samej klasy widżetu: OpenContainer.

W programie home.dart zaimportujmy package:animations/animations.dart na początku pliku i zmodyfikujmy metodę _ReplyFabState build(). Zapakujmy zwrócony widżet Material do widżetu OpenContainer:

home.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
 openBuilder: (context, closedContainer) {
   return const ComposePage();
 },
 openColor: theme.cardColor,
 onClosed: (success) {
   Provider.of<EmailStore>(
     context,
     listen: false,
   ).onCompose = false;
 },
 closedShape: circleFabBorder,
 closedColor: theme.colorScheme.secondary,
 closedElevation: 6,
 closedBuilder: (context, openContainer) {
   return Material(
     color: theme.colorScheme.secondary,
     ...

Oprócz parametrów używanych do konfigurowania poprzedniego widżetu OpenContainer ustaw teraz także onClosed. onClosed to typ ClosedCallback, który jest wywoływany, gdy trasa OpenContainer została otwarta lub wróciła do stanu zamkniętego. Wartość zwrotna tej transakcji jest przekazywana do tej funkcji jako argument. Użyjemy tego elementu (Callback), aby powiadomić dostawcę aplikacji, że opuściliśmy trasę ComposePage, aby mogło ono powiadomić wszystkich słuchaczy.

Podobnie jak w poprzednim kroku, usuniemy widżet Material z naszego widżetu, ponieważ widżet OpenContainer obsługuje kolor widżetu zwróconego przez closedBuilder z parametrem closedColor. Usuniemy również wywołanie Navigator.push() z elementu onTap w widżecie InkWell i zastąpimy je wywołaniem openContainer() Callback podanym przez closedBuilder widżetu OpenContainer, ponieważ teraz widżet OpenContainer obsługuje własny routing.

Wynikowy kod wygląda tak:

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

Teraz musimy usunąć stary kod. Nasz widżet OpenContainer obsługuje teraz powiadamianie dostawcy aplikacji, że nie korzystamy już z aplikacji ComposePage do onClosed ClosedCallback, więc możemy usunąć poprzednią implementację w aplikacji mail_view_router.dart:

mail_view_router.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
emailStore.onCompose = false; /// delete this line
return SynchronousFuture<bool>(true);

To wszystko na dziś. Powinno nastąpić przejście z przycisku PPP do ekranu tworzenia wiadomości, który będzie podobny do tego:

Po

5c7ad1b4b40f9f0c.gif

7. Dodaj przejście z ikony wyszukiwania na stronę widoku wyszukiwania na wspólnej osi Z

W tym kroku dodamy przejście z ikony wyszukiwania do widoku wyszukiwania na pełnym ekranie. Ponieważ zmiana nawigacji nie wymaga trwałego kontenera, możemy użyć przejścia we wspólnej osi Z, aby wzmocnić relację przestrzenną między dwoma ekranami i wskazać przesunięcie o jeden poziom wyżej w hierarchii aplikacji.

Zanim dodasz kolejny kod, uruchom aplikację i kliknij ikonę wyszukiwania w prawym dolnym rogu ekranu. Powinno to spowodować wyświetlenie ekranu wyszukiwania bez żadnego przejścia.

Przed

df7683a8ad7b920e.gif

Na początek przejdźmy do pliku router.dart. Po definicji klasy ReplySearchPath dodaj ten fragment kodu:

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;
       });
 }
}

Teraz użyjmy nowej wersji SharedAxisTransitionPageWrapper, aby wprowadzić odpowiednie zmiany. W definicji klasy ReplyRouterDelegate, w ramach właściwości pages, dodajmy do ekranu wyszukiwania parametr SharedAxisTransitionPageWrapper zamiast 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(),
     ),
 ],
);

Teraz spróbuj ponownie uruchomić aplikację.

81b3ea098926931.gif

Wszystko zaczyna się wyświetlać. Gdy klikniesz ikonę wyszukiwania na dolnym pasku aplikacji, pojawi się na niej widok wspólnej osi. Zwróć jednak uwagę, że strona główna nie skaluje się w poziomie, a zamiast tego pozostaje statyczna, gdy strona wyszukiwania się nad nią przeskaluje. Dodatkowo po naciśnięciu przycisku Wstecz strona główna nie jest skalowana w widok, ale pozostaje statyczna, gdy strona wyszukiwania skaluje się poza widoczny obszar. To jeszcze nie koniec.

Rozwiążmy oba problemy, dodając do pola HomePage atrybut SharedAxisTransitionWrapper zamiast CustomTransitionPage:

router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
   const SharedAxisTransitionPageWrapper(
     transitionKey: ValueKey('home'),
     screen: HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('search'),
       screen: SearchPage(),
     ),
 ],
);

Znakomicie. Teraz uruchom aplikację jeszcze raz i dotknij ikony wyszukiwania. Ekrany głównego i widoku wyszukiwania powinny jednocześnie zanikać i skalować dokładnie wzdłuż osi Z, tworząc płynny efekt między obydwoma ekranami.

Po

462d890086a3d18a.gif

8. Dodawanie przejścia między stronami skrzynki pocztowej

W tym kroku dodamy przejście między różnymi skrzynkami pocztowymi. Ponieważ nie chcemy podkreślać relacji przestrzennej ani hierarchicznej, do przeprowadzenia prostej „zmiany” użyjemy zanikania. między listami adresów e-mail.

Zanim dodasz kolejny kod, uruchom aplikację, dotknij logo Odpowiedz na dolnym pasku aplikacji i zmień skrzynki pocztowe. Lista adresów e-mail powinna się zmienić bez konieczności przenoszenia.

Przed

89033988ce26b92e.gif

Na początek przejdźmy do pliku mail_view_router.dart. Po definicji klasy MailViewRouterDelegate dodaj ten fragment kodu:

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;
       });
 }
}

Podobnie jak w ostatnim kroku, skorzystaj z naszego nowego narzędzia FadeThroughTransitionPageWrapper, aby dokonać zmiany, na której Ci zależy. W definicji klasy MailViewRouterDelegate w obrębie właściwości pages zamiast dodawać ekran skrzynki pocztowej CustomTransitionPage, użyj FadeThroughTransitionPageWrapper:

mail_view_router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Fade through transition between mailbox pages (Motion)
   FadeThroughTransitionPageWrapper(
     mailbox: InboxPage(destination: currentlySelectedInbox),
     transitionKey: ValueKey(currentlySelectedInbox),
   ),
 ],
);

Uruchom aplikację ponownie. Po otwarciu dolnego panelu nawigacji i zmianie skrzynek pocztowych bieżąca lista e-maili powinna zanikać i skalować w poziomie, a nowa lista zanika i skaluje się. Super!

Po

8186940082b630d.gif

9. Dodaj przenikanie między tworzeniem a przyciskiem FAB odpowiedzi

W tym kroku dodamy przejście między różnymi ikonami przycisku FAB. Ponieważ nie chcemy podkreślać relacji przestrzennej ani hierarchicznej, do przeprowadzenia prostej „zmiany” użyjemy zanikania. między ikonami na przycisku PPP.

Zanim dodasz kolejny kod, uruchom aplikację, wybierz e-maila i otwórz widok e-maili. Ikona przycisku FAB powinna się zmienić bez przejścia.

Przed

d8e3afa0447cfc20.gif

Do końca tego ćwiczenia będziemy pracować w programie home.dart, więc nie musisz się martwić o dodanie importu pakietu animacji, bo już to robiliśmy w przypadku pakietu home.dart w kroku 2.

Sposób konfigurowania kilku kolejnych przejść będzie bardzo podobny, ponieważ wszystkie będą korzystać z klasy wielokrotnego użytku: _FadeThroughTransitionSwitcher.

W języku home.dart w _ReplyFabState dodaj ten fragment kodu:

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

Teraz w aplikacji _ReplyFabState znajdź widżet fabSwitcher. Ikona fabSwitcher wyświetla inną ikonę w zależności od tego, czy jest w widoku e-maila. Podsumowanie tego raportu to _FadeThroughTransitionSwitcher:

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
static final fabKey = UniqueKey();
static const double _mobileFabDimension = 56;

@override
Widget build(BuildContext context) {
 final theme = Theme.of(context);
 final circleFabBorder = const CircleBorder();

 return Selector<EmailStore, bool>(
   selector: (context, emailStore) => emailStore.onMailView,
   builder: (context, onMailView, child) {
     // TODO: Add Fade through transition between compose and reply FAB (Motion)
     final fabSwitcher = _FadeThroughTransitionSwitcher(
       fillColor: Colors.transparent,
       child: onMailView
           ? Icon(
               Icons.reply_all,
               key: fabKey,
               color: Colors.black,
             )
           : const Icon(
               Icons.create,
               color: Colors.black,
             ),
     );
...

Na potrzeby elementu _FadeThroughTransitionSwitcher dodaliśmy przezroczyste pole fillColor, dzięki czemu podczas przejścia między elementami nie ma tła. Utworzymy też element UniqueKey i przypiszemy go do jednej z ikon.

Na tym etapie powinien być w pełni animowany kontekstowy przycisk typu FAB. Przejście do widoku e-maila powoduje zanikanie i skalowanie starej ikony przycisku PPP, podczas gdy nowy zanika i skaluje się.

Po

c55bacd9a144ec69.gif

10. Dodaj przejście między znikającym tytułem skrzynki pocztowej

W tym kroku dodamy przenikanie z przejściem, które będzie zanikać między obszarem widocznym a niewidocznym w widoku e-maila. Ponieważ nie chcemy podkreślać relacji przestrzennej ani hierarchicznej, do przeprowadzenia prostej „zmiany” użyjemy zanikania. między widżetem Text obejmującym tytuł skrzynki pocztowej, a pustym polem SizedBox.

Zanim dodasz kolejny kod, uruchom aplikację, wybierz e-maila i otwórz widok e-maili. Tytuł skrzynki pocztowej powinien zniknąć bez przejścia.

Przed

59eb57a6c71725c0.gif

Pozostała część tych ćwiczeń w Codelabs będzie krótka, ponieważ w ostatnim kroku wykonaliśmy już większość czynności w ramach _FadeThroughTransitionSwitcher.

Przejdźmy teraz do zajęć _AnimatedBottomAppBar w usłudze home.dart, aby dodać przejście. Wykorzystamy ponownie pole _FadeThroughTransitionSwitcher z poprzedniego kroku i zapakujemy wartość warunkową onMailView, która zwróci pusty SizedBox lub tytuł skrzynki pocztowej zsynchronizowany z dolnym panelem:

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

To wszystko.

Uruchom aplikację ponownie. Po otwarciu e-maila w widoku e-maila tytuł skrzynki pocztowej na dolnym pasku aplikacji powinien zanikać i skalować w poziomie. Świetnie!

Po

3f1a3db01a481124.gif

11. Dodaj przejście między działaniami na dolnym pasku aplikacji

W tym kroku dodamy przenikanie z przejściem, które będzie zanikać po działaniach na dolnym pasku aplikacji w zależności od kontekstu aplikacji. Ponieważ nie chcemy podkreślać relacji przestrzennej ani hierarchicznej, do przeprowadzenia prostej „zmiany” użyjemy zanikania. między działaniami na dolnym pasku aplikacji, gdy aplikacja jest na stronie głównej, gdy widoczny jest dolny panel lub gdy jesteśmy w widoku e-maila.

Zanim dodasz kolejny kod, uruchom aplikację, wybierz e-maila i otwórz widok e-maili. Możesz też kliknąć logo Odpowiedz. Działania na dolnym pasku aplikacji powinny się zmienić bez przejścia.

Przed

5f662eac19fce3ed.gif

Podobnie jak w ostatnim kroku, ponownie będziemy korzystać z _FadeThroughTransitionSwitcher. Aby osiągnąć oczekiwane przejście, przejdź do definicji klasy _BottomAppBarActionItems i dodaj widżet zwrotny funkcji build() za pomocą tagu _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
...

Teraz spróbujmy to zrobić. Po otwarciu e-maila w widoku e-maila poprzednie działania na dolnym pasku aplikacji powinny być stopniowo zanikać i skalować, a nowe działania będą stopniowo pojawiać się i powiększać. Brawo!

Po

cff0fa2afa1c5a7f.gif

12. Gratulacje!

Pakiet animacji wykorzystuje mniej niż 100 wierszy kodu Dart, aby stworzyć piękne przejścia w dotychczasowej aplikacji zgodnej z wytycznymi Material Design, a także zachowujące się i wyglądać spójnie na wszystkich urządzeniach.

d5637de49eb64d8a.gif

Dalsze kroki

Aby dowiedzieć się więcej o systemie Material Motion, zapoznaj się ze wskazówkami i pełną dokumentacją dla programistów oraz spróbuj dodać do swojej aplikacji przejścia w stylu Material Design.

Dziękujemy za wypróbowanie ruchu Material Design. Mamy nadzieję, że to ćwiczenie z programowania Ci się podobało.

Udało mi się ukończyć to ćwiczenia z programowania w rozsądny sposób i w rozsądny sposób

Całkowicie się zgadzam Zgadzam się Nie mam zdania Nie zgadzam się Całkowicie się nie zgadzam

Chcę w przyszłości nadal korzystać z systemu ruchu Material

Całkowicie się zgadzam Zgadzam się Nie mam zdania Nie zgadzam się Całkowicie się nie zgadzam

Więcej demonstracji korzystania z widżetów z biblioteki Material Flutter oraz platformy Flutter znajdziesz w Flutter Gallery.

46ba920f17198998.png

6ae8ae284bf4f9fa.png