Создание красивых переходов с помощью Material Motion для Flutter

1. Введение

Material Design — это система для создания смелых и красивых цифровых продуктов. Объединив стиль, брендинг, взаимодействие и движение в рамках единого набора принципов и компонентов, продуктовые команды могут реализовать свой величайший дизайнерский потенциал.

logo_comComponents_color_2x_web_96dp.png

Material Components (MDC) помогают разработчикам реализовать Material Design. MDC, созданный командой инженеров и UX-дизайнеров Google, включает в себя десятки красивых и функциональных компонентов пользовательского интерфейса и доступен для Android, iOS, Интернета и Flutter.material.io/develop.

Какова система движения Material для Flutter?

Система движения Material для Flutter — это набор шаблонов перехода в пакете анимации, которые могут помочь пользователям понять приложение и перемещаться по нему, как описано в рекомендациях Material Design .

Четыре основных шаблона перехода материалов следующие:

  • Преобразование контейнера: переходы между элементами пользовательского интерфейса, включающими контейнер; создает видимую связь между двумя отдельными элементами пользовательского интерфейса, плавно преобразуя один элемент в другой.

11807bdf36c66657.gif

  • Общая ось: переходы между элементами пользовательского интерфейса, имеющими пространственные или навигационные отношения; использует общее преобразование по осям x, y или z, чтобы усилить связь между элементами.

71218f390abae07e.gif

  • Fade Through: переходы между элементами пользовательского интерфейса, которые не имеют сильной связи друг с другом; использует последовательное затухание и появление в масштабе входящего элемента.

385ba37b8da68969.gif

  • Fade: используется для элементов пользовательского интерфейса, которые входят или выходят за пределы экрана.

cfc40fd6e27753b6.gif

Пакет анимаций предлагает виджеты перехода для этих шаблонов, созданные на основе библиотеки анимаций Flutter ( flutter/animation.dart ) и библиотеки материалов Flutter ( flutter/material.dart ):

В этой кодовой лаборатории вы будете использовать переходы материалов, созданные на основе платформы Flutter и библиотеки материалов, то есть вы будете иметь дело с виджетами. :)

Что ты построишь

Эта лаборатория кода поможет вам создать некоторые переходы в примере почтового приложения Flutter под названием Reply с использованием Dart, чтобы продемонстрировать, как вы можете использовать переходы из пакета анимаций для настройки внешнего вида вашего приложения.

Будет предоставлен стартовый код для приложения «Ответ», и вы добавите в приложение следующие переходы материалов, которые можно увидеть в GIF-файле завершенной кодовой лаборатории ниже:

  • Переход Container Transform со списка адресов электронной почты на страницу сведений об электронной почте
  • Переход Container Transform от FAB к странице создания электронной почты
  • Общий переход по оси Z от значка поиска на страницу просмотра поиска
  • Переход между страницами почтового ящика Fade Through
  • Fade Through переход между созданием и ответом FAB
  • Переход Fade Through между исчезающим заголовком почтового ящика
  • Переход Fade Through между действиями нижней панели приложений

b26fe84fed12d17d.gif

Что вам понадобится

  • Базовые знания разработки Flutter и Dart.
  • Редактор кода
  • Эмулятор или устройство Android/iOS
  • Пример кода (см. следующий шаг)

Как бы вы оценили свой уровень опыта в создании приложений Flutter?

Новичок Средний Опытный

Что бы вы хотели узнать из этой кодовой лаборатории?

Я новичок в этой теме, и мне нужен хороший обзор. Я кое-что знаю по этой теме, но хочу освежить знания. Я ищу пример кода для использования в моем проекте. Я ищу объяснение чего-то конкретного.

2. Настройте среду разработки Flutter.

Для выполнения этой лабораторной работы вам понадобятся два программного обеспечения — Flutter SDK и редактор .

Вы можете запустить кодовую лабораторию, используя любое из этих устройств:

  • Физическое устройство Android или iOS , подключенное к вашему компьютеру и переведенное в режим разработчика.
  • Симулятор iOS (требуется установка инструментов Xcode).
  • Эмулятор Android (требуется установка в Android Studio).
  • Браузер (для отладки необходим Chrome).
  • В качестве настольного приложения для Windows , Linux или macOS . Вы должны разрабатывать на платформе, на которой планируете развернуть. Итак, если вы хотите разработать классическое приложение для Windows, вам необходимо разработать его в Windows, чтобы получить доступ к соответствующей цепочке сборки. Существуют требования, специфичные для операционной системы, которые подробно описаны на docs.flutter.dev/desktop .

3. Загрузите стартовое приложение Codelab.

Вариант 1. Клонируйте начальное приложение Codelab из GitHub.

Чтобы клонировать эту кодовую лабораторию из GitHub, выполните следующие команды:

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

Вариант 2. Загрузите ZIP-файл начального приложения Codelab.

Стартовое приложение находится в каталоге material-components-flutter-motion-codelab-starter .

Проверка зависимостей проекта

Проект зависит от пакета анимации . Обратите внимание, что в pubspec.yaml раздел dependencies включает следующее:

animations: ^2.0.0

Откройте проект и запустите приложение

  1. Откройте проект в любом редакторе.
  2. Следуйте инструкциям «Запустить приложение» в разделе «Начало работы: тест-драйв» для выбранного вами редактора.

Успех! Стартовый код домашней страницы Reply должен работать на вашем устройстве/эмуляторе. Вы должны увидеть папку «Входящие», содержащую список писем.

Ответить на главной странице

Необязательно: замедлите анимацию устройства.

Поскольку эта кодовая лаборатория включает в себя быстрые, но отточенные переходы, может быть полезно замедлить анимацию устройства, чтобы увидеть некоторые более мелкие детали переходов по мере их реализации. Это можно сделать с помощью настроек в приложении, доступных через нажатие на значок настроек, когда нижний ящик открыт. Не волнуйтесь, этот метод замедления анимации устройства не повлияет на анимацию на устройстве за пределами приложения «Ответить».

d23a7bfacffac509.gif

Дополнительно: темный режим

Если яркая тема «Ответа» режет вам глаза, не ищите дальше. В приложении есть встроенная настройка, позволяющая изменить тему приложения на темный режим, чтобы она лучше подходила вашим глазам. Доступ к этой настройке можно получить, коснувшись значка настроек, когда нижний ящик открыт.

87618d8418eee19e.gif

4. Ознакомьтесь с примером кода приложения.

Давайте посмотрим на код. Мы предоставили приложение, которое использует пакет анимации для перехода между различными экранами приложения.

  • Домашняя страница: отображает выбранный почтовый ящик.
  • InboxPage : отображает список писем.
  • MailPreviewCard : отображает предварительный просмотр электронного письма.
  • MailViewPage: отображает одно полное электронное письмо.
  • ComposePage: позволяет составить новое электронное письмо.
  • SearchPage: отображает представление поиска.

router.dart

Во-первых, чтобы понять, как настроена корневая навигация приложения, откройте router.dart в каталоге 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);
 }
}

Это наш корневой навигатор, который обрабатывает экраны нашего приложения, занимающие весь холст, например HomePage и SearchPage . Он прослушивает состояние нашего приложения, чтобы проверить, установили ли мы маршрут для ReplySearchPath . Если да, то он перестраивает наш навигатор, помещая SearchPage на вершину стека. Обратите внимание, что наши экраны заключены в CustomTransitionPage без определенных переходов. Здесь показан один из способов навигации между экранами без каких-либо настраиваемых переходов.

дом.дарт

Мы устанавливаем наш маршрут на ReplySearchPath в состоянии нашего приложения, выполнив следующие действия внутри _BottomAppBarActionItems в 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();
   },
 ),
);

В нашем параметре onPressed мы получаем доступ к нашему RouterProvider и устанавливаем для его routePath значение ReplySearchPath . Наш RouterProvider отслеживает состояние наших корневых навигаторов.

mail_view_router.dart

Теперь давайте посмотрим, как настроена внутренняя навигация нашего приложения. Откройте mail_view_router.dart в каталоге lib . Вы увидите навигатор, аналогичный приведенному выше:

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

Это наш внутренний навигатор. Он обрабатывает внутренние экраны нашего приложения, которые используют только тело холста, например InboxPage . InboxPage отображает список электронных писем в зависимости от текущего почтового ящика в состоянии нашего приложения. Навигатор перестраивается с использованием правильного InboxPage поверх стека всякий раз, когда происходит изменение свойства currentlySelectedInbox состояния нашего приложения.

дом.дарт

Мы устанавливаем текущий почтовый ящик в состояние нашего приложения, выполнив следующие действия внутри _HomePageState в 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(() {});
}

В нашей функции _onDestinationSelected мы получаем доступ к нашему EmailStore и устанавливаем его currentlySelectedInbox в выбранное место назначения. Наш EmailStore отслеживает состояние наших внутренних навигаторов.

дом.дарт

Наконец, чтобы увидеть пример используемой навигационной маршрутизации, откройте home.dart в каталоге lib . Найдите класс _ReplyFabState внутри свойства onTap виджета InkWell , который должен выглядеть следующим образом:

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

Здесь показано, как можно перейти на страницу создания электронного письма без какого-либо специального перехода. В ходе этой лабораторной работы вы погрузитесь в код Reply, чтобы настроить переходы материалов, которые работают в тандеме с различными действиями навигации по всему приложению.

Теперь, когда вы знакомы со стартовым кодом, давайте реализуем наш первый переход.

5. Добавьте переход Container Transform из списка адресов электронной почты на страницу сведений об электронной почте.

Для начала вы добавите переход при нажатии на письмо. Для этого изменения навигации хорошо подходит шаблон преобразования контейнера, поскольку он предназначен для переходов между элементами пользовательского интерфейса, включающими контейнер. Этот шаблон создает видимую связь между двумя элементами пользовательского интерфейса.

Прежде чем добавлять какой-либо код, попробуйте запустить приложение «Ответить» и нажать на электронное письмо. Он должен выполнить простой переход, что означает, что экран заменяется без перехода:

До

48b00600f73c7778.gif

Начните с добавления импорта пакета анимаций в начало файла mail_card_preview.dart , как показано в следующем фрагменте:

mail_card_preview.dart

import 'package:animations/animations.dart';

Теперь, когда у вас есть импорт пакета анимаций, мы можем начать добавлять красивые переходы в ваше приложение. Начнем с создания класса StatelessWidget , в котором будет размещен наш виджет OpenContainer .

В mail_card_preview.dart добавьте следующий фрагмент кода после определения класса MailPreviewCard :

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

Теперь давайте применим нашу новую обертку. Внутри определения класса MailPreviewCard мы обернем виджет Material из нашей функции build() нашим новым _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(
...

Наш _OpenContainerWrapper имеет виджет InkWell , а свойства цвета OpenContainer определяют цвет контейнера, который он включает. Поэтому мы можем удалить виджеты «Материал» и «Чернильница». Результирующий код выглядит следующим образом:

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

На этом этапе у вас должна быть полностью работающая трансформация контейнера. При нажатии на электронное письмо элемент списка разворачивается на экран сведений, а список электронных писем удаляется. Нажатие «Назад» сворачивает экран сведений об электронной почте обратно в элемент списка, одновременно увеличивая список электронных писем.

После

663e8594319bdee3.gif

6. Добавьте переход Container Transform из FAB на страницу создания электронного письма.

Давайте продолжим преобразование контейнера и добавим переход от плавающей кнопки действия к ComposePage расширяя FAB до нового электронного письма, которое будет написано пользователем. Сначала перезапустите приложение и нажмите FAB, чтобы увидеть, что при запуске экрана создания электронной почты нет перехода.

До

4aa2befdc5170c60.gif

Способ настройки этого перехода будет очень похож на то, как мы это делали на последнем шаге, поскольку мы используем тот же класс виджета — OpenContainer .

В home.dart давайте импортируем package:animations/animations.dart в начало файла и изменим метод _ReplyFabState build() . Давайте обернем возвращенный виджет Material виджетом OpenContainer :

дом.дарт

// 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,
     ...

В дополнение к параметрам, которые использовались для настройки нашего предыдущего виджета OpenContainer , теперь также устанавливается onClosed . onClosed — это ClosedCallback , который вызывается, когда маршрут OpenContainer был извлечен или вернулся в закрытое состояние. Возвращаемое значение этой транзакции передается этой функции в качестве аргумента. Мы используем этот Callback , чтобы уведомить поставщика нашего приложения о том, что мы покинули маршрут ComposePage , чтобы он мог уведомить всех прослушивателей.

Подобно тому, что мы сделали на последнем шаге, мы удалим виджет Material из нашего виджета, поскольку виджет OpenContainer обрабатывает цвет виджета, возвращаемый closedBuilder с помощью closedColor . Мы также удалим наш вызов Navigator.push() внутри onTap нашего виджета InkWell и заменим его openContainer() Callback заданным closedBuilder виджета OpenContainer , поскольку теперь виджет OpenContainer обрабатывает свою собственную маршрутизацию.

Результирующий код выглядит следующим образом:

дом.дарт

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

Теперь очистим старый код. Поскольку наш виджет OpenContainer теперь обрабатывает уведомление провайдера нашего приложения о том, что мы больше не находимся на ComposePage , через onClosed ClosedCallback , мы можем удалить нашу предыдущую реализацию в 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);

Вот и все на этом этапе! У вас должен получиться переход от FAB к экрану создания сообщения, который выглядит следующим образом:

После

5c7ad1b4b40f9f0c.gif

7. Добавьте переход по общей оси Z от значка поиска на страницу просмотра поиска.

На этом этапе мы добавим переход от значка поиска к полноэкранному режиму поиска. Поскольку в этом изменении навигации не задействован постоянный контейнер, мы можем использовать переход «Общая ось Z», чтобы усилить пространственные отношения между двумя экранами и указать перемещение на один уровень вверх в иерархии приложения.

Прежде чем добавлять дополнительный код, попробуйте запустить приложение и нажать значок поиска в правом нижнем углу экрана. Должен появиться экран просмотра поиска без перехода.

До

df7683a8ad7b920e.gif

Для начала давайте перейдем к нашему файлу router.dart . После определения класса ReplySearchPath добавьте следующий фрагмент:

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

Теперь давайте воспользуемся нашим новым SharedAxisTransitionPageWrapper чтобы добиться желаемого перехода. Внутри определения класса ReplyRouterDelegate , под свойством pages , давайте обернем наш экран поиска SharedAxisTransitionPageWrapper вместо 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(),
     ),
 ],
);

Теперь попробуйте перезапустить приложение.

81b3ea098926931.gif

Дела начинают выглядеть великолепно! Когда вы нажимаете на значок поиска в нижней панели приложения, переход по общей оси масштабирует страницу поиска для отображения. Однако обратите внимание, что домашняя страница не масштабируется, а остается статичной, когда страница поиска масштабируется поверх нее. Кроме того, при нажатии кнопки «Назад» домашняя страница не масштабируется, а остается статичной, поскольку страница поиска исчезает из поля зрения. Итак, мы еще не закончили.

Давайте исправим обе проблемы, также обернув HomePage нашим SharedAxisTransitionWrapper вместо 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(),
     ),
 ],
);

Вот и все! Теперь попробуйте перезапустить приложение и нажать на значок поиска. Главный экран и экран просмотра поиска должны одновременно исчезать и масштабироваться по оси Z, создавая плавный эффект между двумя экранами.

После

462d890086a3d18a.gif

8. Добавьте переход Fade Through между страницами почтового ящика.

На этом этапе мы добавим переход между разными почтовыми ящиками. Поскольку мы не хотим подчеркивать пространственные или иерархические отношения, мы воспользуемся плавным переходом, чтобы выполнить простой «обмен» между списками электронных писем.

Прежде чем добавлять какой-либо дополнительный код, попробуйте запустить приложение, коснувшись логотипа «Ответить» в нижней панели приложения и переключив почтовые ящики. Список адресов электронной почты должен меняться без перехода.

До

89033988ce26b92e.gif

Для начала зайдём в наш файл mail_view_router.dart . После определения класса MailViewRouterDelegate добавьте следующий фрагмент:

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

Как и в нашем последнем шаге, давайте воспользуемся нашим новым FadeThroughTransitionPageWrapper , чтобы добиться желаемого перехода. Внутри определения нашего класса MailViewRouterDelegate , в свойстве pages , вместо того, чтобы обертывать экран нашего почтового ящика CustomTransitionPage , используйте 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),
   ),
 ],
);

Перезапустите приложение. Когда вы открываете нижнюю панель навигации и меняете почтовые ящики, текущий список электронных писем должен исчезать и уменьшаться, в то время как новый список исчезает и увеличивается. Отлично!

После

8186940082b630d.gif

9. Добавьте переход Fade Through между написанием и ответом FAB.

На этом этапе мы добавим переход между различными значками FAB. Поскольку мы не хотим подчеркивать пространственные или иерархические отношения, мы воспользуемся плавным переходом, чтобы выполнить простую «перестановку» между значками в FAB.

Прежде чем добавлять какой-либо дополнительный код, попробуйте запустить приложение, нажать на электронное письмо и открыть представление электронной почты. Значок FAB должен измениться без перехода.

До

d8e3afa0447cfc20.gif

Оставшуюся часть лабораторной работы мы будем работать с home.dart , поэтому не беспокойтесь о добавлении импорта для пакета анимации, поскольку мы уже сделали это для home.dart на шаге 2.

Способ настройки следующих нескольких переходов будет очень похожим, поскольку все они будут использовать повторно используемый класс _FadeThroughTransitionSwitcher .

В home.dart добавим следующий фрагмент в _ReplyFabState :

дом.дарт

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

Теперь в нашем _ReplyFabState найдите виджет fabSwitcher . fabSwitcher возвращает другой значок в зависимости от того, отображается ли он в режиме электронной почты или нет. Давайте обернем это нашим _FadeThroughTransitionSwitcher :

дом.дарт

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

Мы присваиваем нашему _FadeThroughTransitionSwitcher прозрачный цвет fillColor , чтобы при переходе между элементами не было фона. Мы также создаем UniqueKey и присваиваем его одному из значков.

Теперь на этом этапе у вас должен быть полностью анимированный контекстный FAB. При переходе в режим просмотра электронной почты старый значок FAB исчезает и уменьшается в масштабе, а новый исчезает и увеличивается.

После

c55bacd9a144ec69.gif

10. Добавьте переход Fade Through между исчезающим заголовком почтового ящика.

На этом этапе мы добавим плавный переход, чтобы заголовок почтового ящика плавно переходил от видимого к невидимому состоянию при просмотре электронной почты. Поскольку мы не хотим подчеркивать пространственные или иерархические отношения, мы воспользуемся плавным переходом, чтобы выполнить простую «замену» между виджетом Text , который включает в себя заголовок почтового ящика, и пустым SizedBox .

Прежде чем добавлять какой-либо дополнительный код, попробуйте запустить приложение, нажать на электронное письмо и открыть представление электронной почты. Заголовок почтового ящика должен исчезнуть без перехода.

До

59eb57a6c71725c0.gif

Остальная часть этой кодовой работы будет быстрой, поскольку мы уже проделали большую часть работы в нашем _FadeThroughTransitionSwitcher на последнем этапе.

Теперь давайте перейдем к нашему классу _AnimatedBottomAppBar в home.dart , чтобы добавить наш переход. Мы будем повторно использовать _FadeThroughTransitionSwitcher из нашего последнего шага и обернуть наше условие onMailView , которое либо возвращает пустой SizedBox , либо заголовок почтового ящика, который исчезает синхронно с нижним ящиком:

дом.дарт

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

Вот и все, мы закончили этот шаг!

Перезапустите приложение. Когда вы открываете электронное письмо и переходите в режим просмотра электронной почты, заголовок почтового ящика в нижней панели приложения должен исчезнуть и уменьшиться. Потрясающий!

После

3f1a3db01a481124.gif

11. Добавьте переход Fade Through между действиями нижней панели приложения.

На этом этапе мы добавим плавный переход, чтобы плавно переходить через действия нижней панели приложения в зависимости от контекста приложения. Поскольку мы не хотим подчеркивать пространственные или иерархические отношения, мы будем использовать постепенное исчезновение, чтобы выполнить простую «перестановку» между действиями нижней панели приложения, когда приложение находится на домашней странице, когда виден нижний ящик и когда мы находимся на просмотре электронной почты.

Прежде чем добавлять какой-либо дополнительный код, попробуйте запустить приложение, нажать на электронное письмо и открыть представление электронной почты. Вы также можете попробовать нажать на логотип «Ответить». Действия нижней панели приложения должны меняться без перехода.

До

5f662eac19fce3ed.gif

Как и в предыдущем шаге, мы снова будем использовать наш _FadeThroughTransitionSwitcher . Чтобы добиться желаемого перехода, перейдите к определению нашего класса _BottomAppBarActionItems и оберните возвращаемый виджет нашей функции build() с помощью _FadeThroughTransitionSwitcher :

дом.дарт

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

Теперь давайте попробуем! Когда вы открываете электронное письмо и переходите в режим просмотра электронной почты, старые действия нижней панели приложения должны исчезать и уменьшаться, а новые действия исчезать и увеличиваться. Молодцы!

После

cff0fa2afa1c5a7f.gif

12. Поздравляем!

Используя менее 100 строк кода Dart, пакет анимации помог вам создать красивые переходы в существующем приложении, которое соответствует рекомендациям Material Design, а также одинаково выглядит и ведет себя на всех устройствах.

d5637de49eb64d8a.gif

Следующие шаги

Для получения дополнительной информации о системе движения Material обязательно ознакомьтесь с рекомендациями и полной документацией для разработчиков и попробуйте добавить в свое приложение несколько переходов Material!

Спасибо за попытку Material motion. Мы надеемся, что вам понравилась эта кодовая лаборатория!

Мне удалось завершить эту кодовую работу, потратив разумное количество времени и усилий.

Полностью согласен Соглашаться Нейтральный Не согласен Категорически не согласен

Я хотел бы продолжать использовать систему движения Material в будущем.

Полностью согласен Соглашаться Нейтральный Не согласен Категорически не согласен

Дополнительные демонстрации использования виджетов, предоставляемых библиотекой Material Flutter, а также платформой Flutter, обязательно посетите галерею Flutter .

46ba920f17198998.png

6ae8ae284bf4f9fa.png