1. Введение
Material Design — это система для создания смелых и красивых цифровых продуктов. Объединяя стиль, брендинг, взаимодействие и анимацию в рамках единого набора принципов и компонентов, продуктовые команды могут реализовать свой максимальный дизайнерский потенциал.
| Компоненты Material (MDC) помогают разработчикам внедрять Material Design. Созданные командой инженеров и UX-дизайнеров Google, MDC включают в себя десятки красивых и функциональных компонентов пользовательского интерфейса и доступны для Android, iOS, веб-приложений и Flutter.material.io/develop |
Что представляет собой система анимации Material для Flutter?
Система анимации Material Design для Flutter представляет собой набор шаблонов переходов в пакете анимации, которые помогают пользователям понимать и перемещаться по приложению, как описано в рекомендациях Material Design .
Четыре основных типа материальных переходов следующие:
- Трансформация контейнера: переход между элементами пользовательского интерфейса, содержащими контейнер; создает видимую связь между двумя различными элементами пользовательского интерфейса путем плавного преобразования одного элемента в другой.

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

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

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

Пакет animations предлагает виджеты переходов для этих шаблонов, созданные на основе как библиотеки анимации Flutter ( flutter/animation.dart ), так и библиотеки материалов Flutter ( flutter/material.dart ):
В этом практическом занятии вы будете использовать переходы Material, созданные на основе фреймворка Flutter и библиотеки Material, то есть будете работать с виджетами. :)
Что вы построите
В этом практическом занятии мы покажем вам, как создать несколько переходов в примере почтового приложения Flutter под названием Reply , используя Dart, чтобы продемонстрировать, как можно использовать переходы из пакета animations для настройки внешнего вида вашего приложения.
Вам будет предоставлен исходный код для приложения Reply, и вы должны будете интегрировать в приложение следующие переходы Material Design, которые можно увидеть на GIF-анимации из готового примера ниже:
- Переход с помощью функции Container Transform со списка рассылок на страницу с подробной информацией о рассылке.
- Переход от кнопки FAB к странице создания электронного письма с помощью функции Container Transform
- Общий переход по оси Z от значка поиска к странице просмотра результатов поиска.
- Плавный переход между страницами почтового ящика
- Плавный переход между кнопками «Составить» и «Отправить» (FAB)
- Плавный переход между исчезающим заголовком почтового ящика
- Плавный переход между действиями в нижней панели приложения

Что вам понадобится
- Базовые знания разработки на 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 .
Проверьте зависимости проекта.
Проект зависит от пакета animations . В файле pubspec.yaml обратите внимание, что раздел dependencies содержит следующее:
animations: ^2.0.0
Откройте проект и запустите приложение.
- Откройте проект в выбранном вами редакторе.
- Следуйте инструкциям, чтобы запустить приложение в разделе « Начало работы: Тестовая версия » для выбранного вами редактора.
Успех! Стартовый код для главной страницы Reply должен работать на вашем устройстве/эмуляторе. Вы должны увидеть папку «Входящие» со списком писем.

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

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

4. Ознакомьтесь с примером кода приложения.
Давайте посмотрим на код. Мы предоставили приложение, которое использует пакет animations для перехода между различными экранами приложения.
- Главная страница: отображает выбранный почтовый ящик
- Страница входящих сообщений : отображает список электронных писем.
- 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 без каких-либо определённых переходов. Это показывает один из способов навигации между экранами без каких-либо пользовательских переходов.
home.dart
Мы задаем маршрут к 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 в состоянии нашего приложения.
home.dart
Мы устанавливаем текущий почтовый ящик в состоянии нашего приложения, выполняя следующие действия внутри _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
Наконец, чтобы увидеть пример использования маршрутизации навигации, откройте 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, чтобы настроить анимации Material Design, которые будут работать в тандеме с различными действиями навигации в приложении.
Теперь, когда вы ознакомились со стартовым кодом, давайте реализуем наш первый переход.
5. Добавьте переход «Преобразование контейнера» со списка писем на страницу с подробной информацией о письме.
Для начала добавим эффект перехода при нажатии на электронное письмо. Для этого изменения навигации хорошо подходит шаблон преобразования контейнера, поскольку он предназначен для переходов между элементами пользовательского интерфейса, содержащими контейнер. Этот шаблон создает видимую связь между двумя элементами пользовательского интерфейса.
Прежде чем добавлять какой-либо код, попробуйте запустить приложение «Ответить» и нажать на электронное письмо. Должно произойти простое переключение, то есть экран будет заменен без перехода:
До

Для начала добавьте импорт пакета animations в начало файла mail_card_preview.dart , как показано в следующем фрагменте кода:
mail_card_preview.dart
import 'package:animations/animations.dart';
Теперь, когда у вас есть импорт пакета animations, мы можем начать добавлять красивые переходы в ваше приложение. Давайте начнем с создания класса 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 определяют цвет контейнера, который он содержит. Поэтому мы можем удалить виджеты Material и Inkwell. В результате код будет выглядеть следующим образом:
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,
),
);
На этом этапе у вас должна быть полностью работоспособная трансформация контейнера. Щелчок по электронному письму разворачивает элемент списка в экран с подробными сведениями, одновременно уменьшая список писем. Нажатие кнопки «Назад» сворачивает экран с подробными сведениями об электронном письме обратно в элемент списка, при этом увеличивая масштаб списка писем.
После

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

Способ настройки этого перехода будет очень похож на тот, который мы использовали на предыдущем шаге, поскольку мы применяем тот же класс виджета — OpenContainer .
В home.dart давайте импортируем package:animations/animations.dart в самом начале файла и изменим метод build() _ReplyFabState . Давайте обернём возвращаемый виджет Material виджетом 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,
...
В дополнение к параметрам, использованным для настройки нашего предыдущего виджета OpenContainer , теперь также устанавливается значение onClosed . onClosed — это функция ClosedCallback , вызываемая при закрытии маршрута OpenContainer . Возвращаемое значение транзакции передается в эту функцию в качестве аргумента. Мы используем эту Callback , чтобы уведомить поставщика нашего приложения о том, что мы покинули маршрут ComposePage , чтобы он мог уведомить всех слушателей.
Аналогично тому, что мы сделали на предыдущем шаге, мы удалим виджет Material из нашего виджета, поскольку виджет OpenContainer обрабатывает цвет виджета, возвращаемого closedBuilder с помощью closedColor . Мы также удалим вызов Navigator.push() внутри onTap нашего виджета InkWell и заменим его на openContainer() Callback предоставляемый closedBuilder виджета OpenContainer , поскольку теперь виджет 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 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 к экрану создания сообщения, который выглядит следующим образом:
После

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

Для начала перейдём к файлу 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(),
),
],
);
Теперь попробуйте запустить приложение снова.

Всё начинает выглядеть отлично! При нажатии на значок поиска в нижней панели приложения происходит масштабирование страницы поиска, при этом страница поиска увеличивается в видимой области. Однако обратите внимание, что главная страница не увеличивается в масштабе, а остаётся статичной, в то время как страница поиска увеличивается в масштабе поверх неё. Кроме того, при нажатии кнопки «Назад» главная страница не увеличивается в масштабе, а остаётся статичной, в то время как страница поиска уменьшается в масштабе. Но это ещё не всё.
Давайте исправим обе проблемы, обернув 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, создавая эффект бесшовного перехода между двумя экранами.
После

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

Для начала перейдём к файлу 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),
),
],
);
Перезапустите приложение. Когда вы откроете нижнюю панель навигации и смените почтовый ящик, текущий список писем должен плавно уменьшаться, а новый список — плавно уменьшаться. Отлично!
После

9. Добавьте плавный переход между кнопками «Составить» и «Ответить» (FAB).
На этом этапе мы добавим переход между различными значками FAB. Поскольку мы не хотим подчеркивать пространственную или иерархическую связь, мы используем эффект плавного перехода для простого «обмена» значками на FAB.
Прежде чем добавлять какой-либо дополнительный код, попробуйте запустить приложение, нажать на электронное письмо и открыть окно просмотра письма. Значок FAB должен измениться без плавного перехода.
До

В оставшейся части практического занятия мы будем работать с файлом home.dart , поэтому не беспокойтесь о добавлении импорта пакета animations, поскольку мы уже сделали это для home.dart на шаге 2.
Способ настройки следующих нескольких переходов будет очень похожим, поскольку все они будут использовать многократно используемый класс _FadeThroughTransitionSwitcher .
В home.dart добавим следующий фрагмент кода в раздел _ReplyFabState :
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,
);
}
}
Теперь в нашем _ReplyFabState найдите виджет fabSwitcher . fabSwitcher возвращает разные значки в зависимости от того, находится ли он в режиме просмотра письма или нет. Давайте обернём его в наш _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,
),
);
...
Мы задаем для нашего _FadeThroughTransitionSwitcher прозрачный fillColor , чтобы между элементами не было фона во время перехода. Мы также создаем UniqueKey и присваиваем его одной из иконок.
На этом этапе у вас должна быть полностью анимированная контекстная кнопка FAB. При переходе в режим просмотра электронного письма старый значок FAB будет плавно исчезать и уменьшаться, а новый — плавно появляться и увеличиваться.
После

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

Остальная часть этого практического задания будет выполнена быстро, поскольку большую часть работы в нашем компоненте _FadeThroughTransitionSwitcher мы уже проделали на предыдущем шаге.
Теперь перейдём к классу _AnimatedBottomAppBar в home.dart , чтобы добавить анимацию перехода. Мы будем повторно использовать класс _FadeThroughTransitionSwitcher из предыдущего шага и обернём в него условный оператор onMailView , который либо возвращает пустой SizedBox , либо заголовок почтового ящика, плавно меняющийся синхронно с нижней выдвижной панелью:
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,
),
);
},
),
),
),
Вот и всё, этот шаг завершен!
Перезапустите приложение. Когда вы откроете электронное письмо и перейдете в режим просмотра письма, заголовок почтового ящика в нижней панели приложения должен плавно исчезнуть и увеличиться в размере. Отлично!
После

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

Аналогично предыдущему шагу, мы снова будем использовать наш _FadeThroughTransitionSwitcher . Для достижения желаемого перехода перейдите к определению класса _BottomAppBarActionItems и оберните возвращаемый виджет нашей функции build() в _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
...
Давайте попробуем! Когда вы открываете электронное письмо и переходите в режим просмотра письма, старые действия в нижней панели приложения должны плавно исчезать и увеличиваться в размере, а новые действия — появляться и уменьшаться. Отлично!
После

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

Следующие шаги
Для получения более подробной информации о системе анимации Material обязательно ознакомьтесь с рекомендациями и полной документацией для разработчиков , а также попробуйте добавить несколько переходов Material в свое приложение!
Спасибо за то, что попробовали Material Motion. Надеемся, вам понравился этот практический урок!
Мне удалось выполнить это практическое задание за разумное время и с разумными затратами усилий.
Я хотел бы и в будущем продолжать использовать систему перемещения материалов.
Посмотрите галерею Flutter.
| Для получения дополнительных демонстраций использования виджетов, предоставляемых библиотекой Material Flutter, а также фреймворком Flutter, посетите галерею Flutter . |



