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

1. Wprowadzenie

Material Design to system do tworzenia odważnych i pięknych produktów cyfrowych. Łącząc styl, branding, interakcje i ruchy w ramach spójnego zestawu zasad i komponentów, zespoły produktowe mogą w pełni wykorzystać potencjał projektowania.

logo_components_color_2x_web_96dp.png

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

Co to jest system animacji Material w Flutterze?

System animacji Material Design na potrzeby platformy 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ścia w Material Design:

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

11807bdf36c66657.gif

  • Wzorzec wspólnej osi: przejścia między elementami interfejsu, między którymi istnieje związek przestrzenny lub nawigacyjny; wykorzystuje wspólną transformację na osi x, y lub z do wzmocnienia związku między elementami.

71218f390abae07e.gif

  • Przenikanie: przejścia między elementami interfejsu, które nie mają ze sobą silnych powiązań. Używa sekwencyjnego zaciemniania i rozjaśniania ze skalą elementu przychodzącego.

385ba37b8da68969.gif

  • Zanikanie: przeznaczone do elementów interfejsu, które pojawiają się lub znikają na ekranie.

cfc40fd6e27753b6.gif

Pakiet animacji zawiera widżety przejść dla tych wzorów, które zostały opracowane na podstawie biblioteki animacji Flutter (flutter/animation.dart) i biblioteki materiałów Flutter (flutter/material.dart):

W tym ćwiczeniu w języku programowania Dart będziesz używać przejść Material opartych na frameworku Flutter i bibliotece Material, co oznacza, że będziesz pracować z widżetami. :)

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 styl swojej aplikacji.

Dostaniesz kod startowy aplikacji Reply. Będziesz musiał(-aś) wdrożyć w niej te przejścia w stylu Material Design, które możesz zobaczyć na GIF-ie z ukończonego ćwiczenia:

  • Transformer kontenera – przejście ze strony z listą e-mailową na stronę z informacjami o e-mailu
  • Przejście z przycisku szybkiego dostępu do strony tworzenia e-maila w Container Transform
  • Wspólna oś Z – przejście z ikony wyszukiwania na stronę widoku wyszukiwania
  • Przejście z efektem zanikania między stronami skrzynki pocztowej
  • Przejście z efektem zanikania między opcją tworzenia wiadomości i opcją odpowiedzi
  • Fade Through – przejście między znikającymi tytułami skrzynek pocztowych
  • Przenikanie między działaniami na dolnym pasku aplikacji

b26fe84fed12d17d.gif

Czego potrzebujesz

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

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

Początkujący Średni Zaawansowany

Czego chcesz się nauczyć z tego Codelab?

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

Do wykonania tego laboratorium potrzebne są 2 programy: Flutter SDKedytor.

Możesz uruchomić laboratorium programistyczne na dowolnym z tych urządzeń:

  • Fizyczne urządzenie z Android 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 wymagana jest przeglądarka 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 tworzyć aplikacje na komputery z systemem Windows, musisz to robić w Windowsie, 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. Pobieranie aplikacji startowej ćwiczeń z programowania

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 z aplikacją startową Codelab

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. Wykonaj instrukcje „Uruchamianie aplikacji” opisane w artykule Pierwsze kroki: jazda testowa dla wybranego edytora.

Gotowe! Kod startowy dla strony głównej Reply powinien działać na urządzeniu lub w 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. Nie martw się, ta metoda spowolnienia animacji na urządzeniu nie wpłynie na animacje na urządzeniu poza aplikacją Reply.

d23a7bfacffac509.gif

Opcjonalnie: tryb ciemny

Jeśli jasny motyw w Reply męczy Ci oczy, mamy coś dla Ciebie. W aplikacji jest ustawienie, które pozwala zmienić motyw aplikacji na tryb ciemny, aby lepiej pasował do Twoich oczu. To ustawienie jest dostępne po kliknięciu ikony ustawień, gdy otwarta jest dolna szuflada.

87618d8418eee19e.gif

4. Zapoznaj się z przykładowym kodem 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 skonfigurowana jest główna nawigacja w aplikacji, otwórz plik 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. Słucha stanu naszej aplikacji, aby sprawdzić, czy ustawiliśmy trasę do ReplySearchPath. Jeśli tak, odbudowuje nawigator z elementem SearchPage u góry stosu. Zwróć uwagę, że nasze ekrany są zawinięte w CustomTransitionPage bez zdefiniowanych przejść. Pokazuje on jeden sposób nawigowania między ekranami bez korzystania z przejść niestandardowych.

home.dart

W stanie aplikacji ReplySearchPath ustawiamy ścieżkę ReplySearchPath, wykonując w komponencie _BottomAppBarActionItems w komponencie home.dart te czynności:

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 zmiennej RouterProvider i ustawiamy jej wartość 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. Zobaczysz nawigację podobną do tej 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 ekrany wewnętrzne aplikacji, które zajmują tylko część głównej powierzchni kanwy, 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

Ustawiamy bieżącą skrzynkę pocztową w stanie aplikacji, wykonując te czynności w funkcji _HomePageState w funkcji 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(() {});
}

W funkcji _onDestinationSelected uzyskujemy dostęp do zmiennej EmailStore i ustawiamy jej wartość 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();
     },
   ),
 );
},

Ten film pokazuje, jak przejść na stronę tworzenia e-maila bez korzystania z przejść niestandardowych. W tym ćwiczeniu zapoznasz się z kodem aplikacji Reply, aby skonfigurować przejścia w stylu Material Design, które będą współpracować z różnymi działaniami nawigacyjnymi w aplikacji.

Teraz, gdy znasz już kod startowy, zaimplementujmy pierwszą zmianę.

5. Dodawanie przejścia Przekształcenie kontenera z listy e-maili na stronę szczegółów e-maila

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 on zawierać proste przejście, co oznacza, że ekran jest zastępowany bez przejścia:

Przed

48b00600f73c7778.gif

Zacznij od dodania importu pakietu animacji u góry pliku mail_card_preview.dart, jak pokazano w tym fragmencie kodu:

mail_card_preview.dart

import 'package:animations/animations.dart';

Teraz, gdy masz już zaimportowany pakiet animacji, możesz zacząć dodawać do aplikacji piękne przejścia. Zacznijmy od utworzenia klasy StatelessWidget, która będzie zawierać 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 opakowania. W definicji klasy MailPreviewCard owiniemy widget Material z funkcji build() za pomocą nowego elementu _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 element _OpenContainerWrapper zawiera element InkWell, a właściwości koloru tego elementu określają kolor kontenera, w którym się znajduje. 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 powinnaś/powinieneś mieć w pełni działającą transformację kontenera. Kliknięcie e-maila powoduje rozwinięcie listy i wyświetlenie 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 ponownie aplikację i kliknij przycisk szybkiego działania, aby sprawdzić, czy podczas uruchamiania 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 ClosedCallback, który jest wywoływany, gdy trasa OpenContainer została wyjęta z bufora lub wróciła do stanu zamkniętego. Wartość zwrotna tej transakcji jest przekazywana do tej funkcji jako argument. Używamy tego Callback, aby powiadomić dostawcę naszej aplikacji o tym, że opuściliśmy trasę ComposePage, aby mógł on powiadomić wszystkich słuchaczy.

Podobnie jak w ostatnim kroku usuniemy z widżetu widżet Material, ponieważ widżet OpenContainer obsługuje kolor widżetu zwracany przez funkcję closedBuilder z wartością closedColor. Usuniemy też wywołanie Navigator.push() w układance InkWell w komponencie onTap i zastąpimy je wywołaniem openContainer() Callback z komponentu OpenContainer, ponieważ komponent OpenContainer obsługuje teraz własną obsługę routingu.

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 czas na uporządkowanie starego kodu. 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

Na tym etapie dodamy przejście od ikony wyszukiwania do widoku wyszukiwania pełnoekranowego. Ponieważ zmiana nawigacji nie obejmuje żadnego 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 dodatkowy kod, uruchom aplikację i kliknij ikonę wyszukiwania w prawym dolnym rogu ekranu. Powinien wyświetlić się ekran widoku wyszukiwania bez 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 usłudze pages spróbuj owinąć ekran wyszukiwania w klasie SharedAxisTransitionPageWrapper zamiast w klasie 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, strona wyszukiwania zostanie wyświetlona w powiększeniu. Zwróć jednak uwagę, że strona główna nie zmienia rozmiaru, tylko pozostaje nieruchoma, gdy strona wyszukiwania się powiększa. Dodatkowo po naciśnięciu przycisku Wstecz strona główna nie jest wyświetlana w całości, tylko pozostaje nieruchoma, gdy strona wyszukiwania znika z widoczności. To jeszcze nie koniec.

Rozwiążmy oba problemy, dodając do 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 kliknij ikonę wyszukiwania. Ekrany widoku głównego i wyszukiwania powinny jednocześnie zanikać i zmieniać rozmiary wzdłuż osi Z, tworząc płynne przejście między tymi 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, użyjemy zanikania, aby wykonać prostą „przemianę” 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 e-maili powinna się zmienić bez przejścia.

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 polu właściwości pages zamiast dodawać ekran skrzynki pocztowej CustomTransitionPage, użyj funkcji 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),
   ),
 ],
);

Ponownie uruchom aplikację. Gdy otworzysz dolny panel nawigacji i zmienisz skrzynki pocztowe, 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 szybkiego działania. Ponieważ nie chcemy podkreślać relacji przestrzennej ani hierarchicznej, użyjemy efektu przejścia, aby wykonać prostą „podmianę” ikon w przycisku FAB.

Zanim dodasz dodatkowy kod, uruchom aplikację, kliknij e-maila i otwórz widok e-maila. Ikona przycisku FAB powinna się zmieniać bez przejścia.

Przed

d8e3afa0447cfc20.gif

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

Sposób konfiguracji kolejnych przekształceń będzie bardzo podobny, ponieważ wszystkie będą używać klasy wielokrotnego użytku _FadeThroughTransitionSwitcher.

W języku home.dart do elementu _ReplyFabState dodajmy następujący 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 naszym _ReplyFabState znajdź widżet fabSwitcher. Funkcja fabSwitcher zwraca inną ikonę w zależności od tego, czy jest widok e-maila. Podsumowanie danych 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,
             ),
     );
...

Nasz element _FadeThroughTransitionSwitcher ma przezroczyste tło, więc podczas przejścia nie ma tła między elementami. Tworzymy też UniqueKey i przypisujemy go do jednej z ikon.

Na tym etapie powinieneś mieć w pełni animowany kontekstualny przycisk FAB. Gdy przechodzisz do widoku e-maila, stara ikona przycisku szybkiego dostępu zanika i zmniejsza się, a nowa pojawia się i powiększa.

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, użyjemy efektu znikania, aby przeprowadzić prostą „wymianę” widżetu Text, który obejmuje tytuł skrzynki pocztowej, na pusty widżet 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.

Teraz przejdź do klasy _AnimatedBottomAppBar w komponencie 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 koniec tego kroku.

Ponownie uruchom aplikację. 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 przejście z rozmyciem, aby płynnie przechodzić między działaniami na dolnym pasku aplikacji na podstawie kontekstu aplikacji. Nie chcemy podkreślać związku przestrzennego ani hierarchicznego, dlatego użyjemy zanikania, aby w prosty sposób przełączać się między czynnościami na dolnym pasku aplikacji, gdy aplikacja jest na stronie głównej, gdy widoczna jest dolna szuflada i w widoku e-maila.

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

Przed

5f662eac19fce3ed.gif

Podobnie jak w ostatnim kroku, użyjemy ponownie zmiennej _FadeThroughTransitionSwitcher. Aby uzyskać pożądane przejście, otwórz definicję klasy _BottomAppBarActionItems i owiń widżet zwracania funkcji build() w _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
...

Spróbujmy. Gdy otworzysz e-maila i przejdziesz do widoku e-maila, stare działania na dolnym pasku aplikacji powinny zniknąć, a nowe powinny się pojawić. Brawo!

Po

cff0fa2afa1c5a7f.gif

12. Gratulacje!

Za pomocą zaledwie kilkudziesięciu linii kodu Dart udało Ci się utworzyć w dotychczasowej aplikacji piękne przejścia, które są zgodne ze wskazówkami dotyczącymi Material Design, a także wyglądają i działają 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 Material Motion. 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

Zdecydowanie się zgadzam Zdecydowanie się zgadzam Nie mam zdania Nie zgadzam się Zdecydowanie 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