1. Введение
| Компоненты Material (MDC) помогают разработчикам внедрять Material Design. Созданные командой инженеров и UX-дизайнеров Google, MDC включают в себя десятки красивых и функциональных компонентов пользовательского интерфейса и доступны для Android, iOS, веб-приложений и Flutter.material.io/develop |
В практическом задании MDC-103 вы настроили цвет, высоту, типографику и форму компонентов Material Components (MDC) для оформления вашего приложения.
Компонент в системе Material Design выполняет набор предопределенных задач и обладает определенными характеристиками, подобно кнопке. Однако кнопка — это не просто способ выполнения действия пользователем, это также визуальное выражение формы, размера и цвета, которое сообщает пользователю о ее интерактивности и о том, что при касании или щелчке произойдет какое-либо действие.
В руководстве по Material Design компоненты описываются с точки зрения дизайнера. В нем представлен широкий спектр базовых функций, доступных на разных платформах, а также анатомические элементы, из которых состоит каждый компонент. Например, фон содержит задний слой и его содержимое, передний слой и его содержимое, правила движения и параметры отображения. Каждый из этих компонентов может быть настроен в соответствии с потребностями, сценариями использования и содержимым каждого приложения.
Что вы построите
В этом практическом задании вы измените пользовательский интерфейс приложения Shrine на двухуровневое представление, называемое «фоном». Фон включает в себя меню со списком выбираемых категорий, используемых для фильтрации товаров, отображаемых в асимметричной сетке. В этом практическом задании вы будете использовать следующее:
- Форма
- Движение
- Виджеты Flutter (которые вы использовали в предыдущих практических занятиях).
Android | iOS |
|
|
|
|
Компоненты и подсистемы Material Flutter в этом практическом занятии.
- Форма
Как бы вы оценили свой уровень опыта в разработке на Flutter?
2. Настройте среду разработки Flutter.
Для выполнения этой лабораторной работы вам понадобятся два программных компонента: Flutter SDK и редактор .
Вы можете выполнить это практическое задание, используя любое из следующих устройств:
- Физическое устройство Android или iOS , подключенное к компьютеру и настроенное на режим разработчика.
- Симулятор iOS (требуется установка инструментов Xcode).
- Эмулятор Android (требуется настройка в Android Studio).
- Для работы требуется браузер (для отладки необходим Chrome).
- Если вы разрабатываете настольное приложение для Windows , Linux или macOS , вам необходимо работать на той платформе, на которой вы планируете его развернуть. Таким образом, если вы хотите разработать настольное приложение для Windows, вам необходимо использовать Windows для доступа к соответствующей цепочке сборки. Существуют специфические требования к операционной системе, которые подробно описаны в документации docs.flutter.dev/desktop .
3. Скачайте стартовое приложение Codelab.
Продолжение темы MDC-103?
Если вы прошли курс MDC-103, ваш код должен быть готов для этой практической работы. Перейдите к шагу: Добавьте фоновое меню .
Начинать с нуля?
Стартовое приложение находится в каталоге material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series .
...или клонируйте его с GitHub
Чтобы клонировать этот код с GitHub, выполните следующие команды:
git clone https://github.com/material-components/material-components-flutter-codelabs.git cd material-components-flutter-codelabs/mdc_100_series git checkout 104-starter_and_103-complete
Откройте проект и запустите приложение.
- Откройте проект в выбранном вами редакторе.
- Следуйте инструкциям, чтобы запустить приложение в разделе « Начало работы: Тестовая версия » для выбранного вами редактора.
Успех! На вашем устройстве должна отобразиться страница входа в Shrine, которую вы видели в предыдущих кодовых заданиях.
Android | iOS |
|
|
4. Добавьте фоновое меню.
Фоновое изображение отображается за всем остальным содержимым и компонентами. Оно состоит из двух слоев: заднего слоя (отображающего действия и фильтры) и переднего слоя (отображающего содержимое). Фоновое изображение можно использовать для отображения интерактивной информации и действий, таких как навигация или фильтры содержимого.
Удалите панель приложений на главном экране.
Виджет HomePage будет содержимым нашего фронтального слоя. Сейчас он содержит панель приложений. Мы переместим панель приложений на задний слой, и HomePage будет включать только AsymmetricView.
В home.dart измените функцию build() так, чтобы она просто возвращала AsymmetricView:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
Добавьте виджет «Фон»
Создайте виджет с именем Backdrop , который будет включать в себя frontLayer и backLayer .
backLayer включает в себя меню, позволяющее выбрать категорию для фильтрации списка ( currentCategory ). Поскольку нам нужно, чтобы выбранный пункт меню сохранялся, мы сделаем Backdrop виджетом с состоянием.
Добавьте в папку /lib новый файл с именем backdrop.dart :
import 'package:flutter/material.dart';
import 'model/product.dart';
// TODO: Add velocity constant (104)
class Backdrop extends StatefulWidget {
final Category currentCategory;
final Widget frontLayer;
final Widget backLayer;
final Widget frontTitle;
final Widget backTitle;
const Backdrop({
required this.currentCategory,
required this.frontLayer,
required this.backLayer,
required this.frontTitle,
required this.backTitle,
Key? key,
}) : super(key: key);
@override
_BackdropState createState() => _BackdropState();
}
// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)
Обратите внимание, что мы помечаем некоторые свойства required . Это рекомендуемая практика для свойств в конструкторе, которые не имеют значения по умолчанию и не могут быть null , поэтому их не следует забывать.
В определение класса Backdrop добавьте класс _BackdropState:
// TODO: Add _BackdropState class (104)
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
// TODO: Add AnimationController widget (104)
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack() {
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
widget.frontLayer,
],
);
}
@override
Widget build(BuildContext context) {
var appBar = AppBar(
elevation: 0.0,
titleSpacing: 0.0,
// TODO: Replace leading menu icon with IconButton (104)
// TODO: Remove leading property (104)
// TODO: Create title with _BackdropTitle parameter (104)
leading: Icon(Icons.menu),
title: Text('SHRINE'),
actions: <Widget>[
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
// TODO: Add open login (104)
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
// TODO: Add open login (104)
},
),
],
);
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: _buildStack(),
);
}
}
Функция build() возвращает Scaffold с панелью приложения, как это было раньше в HomePage. Но тело Scaffold — это Stack . Дочерние элементы Stack могут перекрываться. Размер и местоположение каждого дочернего элемента указываются относительно родительского элемента Stack.
Теперь добавьте экземпляр Backdrop в ShrineApp.
В app.dart импортируйте backdrop.dart и model/product.dart :
import 'backdrop.dart'; // New code
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // New code
import 'supplemental/cut_corners_border.dart';
В app.dart, измените маршрут / , вернув объект Backdrop , у которого в качестве frontLayer используется HomePage :
// TODO: Change to a Backdrop with a HomePage frontLayer (104)
'/': (BuildContext context) => Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: Category.all,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: Container(color: kShrinePink100),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
Сохраните свой проект, и вы увидите, что отображается наша главная страница и панель приложения:
Android | iOS |
|
|
На заднем слое отображается розовая область в новом слое, расположенном за главной страницей переднего слоя.
Вы можете использовать Flutter Inspector , чтобы убедиться, что в стеке действительно есть контейнер за домашней страницей. Результат должен выглядеть примерно так:

Теперь вы можете настраивать дизайн и содержимое обоих слоев.
5. Добавьте фигуру.
На этом этапе вы оформите передний слой, добавив вырез в верхнем левом углу.
В Material Design этот тип персонализации называется формой. Материальные поверхности могут иметь произвольные формы. Формы придают поверхностям выразительность и стиль, а также могут использоваться для выражения фирменной символики. Обычные прямоугольные формы могут быть персонализированы с помощью закругленных или угловатых углов и кромок, а также любого количества сторон. Они могут быть симметричными или неправильной формы.
Добавьте фигуру на передний слой.
Угловой логотип Shrine вдохновил на создание концепции использования форм в приложении Shrine. Концепция использования форм — это общее применение форм в приложении. Например, форма логотипа повторяется в элементах страницы входа, к которым применена определенная форма. На этом шаге вы оформите лицевой слой, добавив угловой срез в верхнем левом углу.
В backdrop.dart добавьте новый класс _FrontLayer :
// TODO: Add _FrontLayer class (104)
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
Expanded(
child: child,
),
],
),
);
}
}
Затем в функции _buildStack() класса _BackdropState оберните передний слой в объект _FrontLayer:
Widget _buildStack() {
// TODO: Create a RelativeRectTween Animation (104)
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
// TODO: Add a PositionedTransition (104)
// TODO: Wrap front layer in _FrontLayer (104)
_FrontLayer(child: widget.frontLayer),
],
);
}
Перезагрузка.
Android | iOS |
|
|
Мы придали основной поверхности Shrine необычную форму. Однако мы хотим, чтобы она визуально соединялась с панелью приложения.
Изменить цвет панели приложения
В app.dart измените функцию _buildShrineTheme() следующим образом:
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light(useMaterial3: true);
return base.copyWith(
colorScheme: base.colorScheme.copyWith(
primary: kShrinePink100,
onPrimary: kShrineBrown900,
secondary: kShrineBrown900,
error: kShrineErrorRed,
),
textTheme: _buildShrineTextTheme(base.textTheme),
textSelectionTheme: const TextSelectionThemeData(
selectionColor: kShrinePink100,
),
appBarTheme: const AppBarTheme(
foregroundColor: kShrineBrown900,
backgroundColor: kShrinePink100,
),
inputDecorationTheme: const InputDecorationTheme(
border: CutCornersBorder(),
focusedBorder: CutCornersBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
floatingLabelStyle: TextStyle(
color: kShrineBrown900,
),
),
);
}
Горячая перезагрузка. Теперь должна появиться новая цветная панель приложений.
Android | iOS |
|
|
Благодаря этому изменению пользователи могут видеть, что за передним белым слоем что-то находится. Давайте добавим анимацию, чтобы пользователи могли увидеть задний слой фона.
6. Добавьте движение.
Анимация — это способ оживить ваше приложение. Она может быть масштабной и эффектной, едва заметной и минималистичной, или любой промежуточной. Но помните, что тип используемой анимации должен соответствовать ситуации. Анимация, применяемая к повторяющимся, регулярным действиям, должна быть небольшой и незаметной, чтобы эти действия не отвлекали пользователя и не отнимали слишком много времени. Однако существуют подходящие ситуации, например, когда пользователь впервые открывает приложение, которые могут быть более привлекательными, а некоторые анимации могут помочь пользователю понять, как использовать ваше приложение.
Добавить анимацию появления к кнопке меню
В верхней части файла backdrop.dart , вне области видимости какого-либо класса или функции, добавьте константу, представляющую желаемую скорость анимации:
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
Добавьте виджет AnimationController в _BackdropState, создайте его экземпляр в функции initState() и освободите его ресурсы в функции dispose() состояния:
// TODO: Add AnimationController widget (104)
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
// TODO: Add override for didUpdateWidget (104)
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// TODO: Add functions to get and change front layer visibility (104)
AnimationController координирует анимации и предоставляет API для воспроизведения, реверсирования и остановки анимации. Теперь нам нужны функции, которые заставляют её двигаться.
Добавьте функции, которые определяют и изменяют видимость фронтального слоя:
// TODO: Add functions to get and change front layer visibility (104)
bool get _frontLayerVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
void _toggleBackdropLayerVisibility() {
_controller.fling(
velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
}
Оберните backLayer в виджет ExcludeSemantics. Этот виджет исключит пункты меню backLayer из дерева семантики, когда backLayer не виден.
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
Измените функцию _buildStack() так, чтобы она принимала BuildContext и BoxConstraints. Также добавьте PositionedTransition, который принимает анимацию RelativeRectTween:
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
// TODO: Create a RelativeRectTween Animation (104)
Animation<RelativeRect> layerAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, layerTop, 0.0, layerTop - layerSize.height),
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
// TODO: Add a PositionedTransition (104)
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
child: widget.frontLayer,
),
),
],
);
}
Наконец, вместо вызова функции _buildStack для тела Scaffold, верните виджет LayoutBuilder , который использует _buildStack в качестве своего построителя:
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
Мы отложили построение стека слоев «передний/задний план» до момента компоновки, используя LayoutBuilder, чтобы учесть фактическую общую высоту фона. LayoutBuilder — это специальный виджет, функция обратного вызова которого предоставляет ограничения по размеру.
В функции build() замените главный значок меню на панели приложения на IconButton и используйте его для переключения видимости основного слоя при нажатии на кнопку.
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
Перезагрузите симулятор, затем нажмите кнопку меню.
Android | iOS |
|
|
Передний слой анимируется (скользит) вниз. Но если посмотреть вниз, появится красная ошибка и ошибка переполнения. Это происходит потому, что AsymmetricView сжимается и уменьшается в размерах из-за этой анимации, что, в свою очередь, уменьшает пространство для столбцов. В итоге столбцы не могут разместиться в отведенном пространстве, и это приводит к ошибке. Если мы заменим столбцы на ListView, размер столбцов должен оставаться таким же, как и при анимации.
Оберните столбцы с товарами в ListView.
В файле supplemental/product_columns.dart замените столбец в OneProductCardColumn на ListView:
class OneProductCardColumn extends StatelessWidget {
const OneProductCardColumn({required this.product, Key? key}) : super(key: key);
final Product product;
@override
Widget build(BuildContext context) {
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
reverse: true,
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 550,
),
child: ProductCard(
product: product,
),
),
const SizedBox(
height: 40.0,
),
],
);
}
}
В столбец включена MainAxisAlignment.end . Чтобы начать компоновку снизу, установите флажок reverse: true . Порядок дочерних элементов будет изменен в обратную сторону, чтобы компенсировать это изменение.
Перезагрузите устройство и нажмите кнопку меню.
Android | iOS |
|
|
Серое предупреждение о переполнении в столбце OneProductCardColumn исчезло! Теперь давайте исправим остальное.
В supplemental/product_columns.dart измените способ вычисления imageAspectRatio и замените столбец в TwoProductCardColumn на ListView:
// TODO: Change imageAspectRatio calculation (104)
double imageAspectRatio = heightOfImages >= 0.0
? constraints.biggest.width / heightOfImages
: 49.0 / 33.0;
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
children: <Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(start: 28.0),
child: top != null
? ProductCard(
imageAspectRatio: imageAspectRatio,
product: top!,
)
: SizedBox(
height: heightOfCards,
),
),
const SizedBox(height: spacerHeight),
Padding(
padding: const EdgeInsetsDirectional.only(end: 28.0),
child: ProductCard(
imageAspectRatio: imageAspectRatio,
product: bottom,
),
),
],
);
Мы также добавили некоторые функции повышения безопасности для imageAspectRatio .
Перезагрузите устройство. Затем нажмите кнопку меню.
Android | iOS |
|
|
Больше никаких переливов.
7. Добавьте меню на задний слой.
Меню — это список текстовых элементов, на которые можно нажимать, и которые уведомляют пользователя о нажатии на эти элементы. На этом шаге вы добавите меню фильтрации по категориям.
Добавить меню
Добавьте меню на передний слой, а интерактивные кнопки — на задний слой.
Создайте новый файл с именем lib/category_menu_page.dart :
import 'package:flutter/material.dart';
import 'colors.dart';
import 'model/product.dart';
class CategoryMenuPage extends StatelessWidget {
final Category currentCategory;
final ValueChanged<Category> onCategoryTap;
final List<Category> _categories = Category.values;
const CategoryMenuPage({
Key? key,
required this.currentCategory,
required this.onCategoryTap,
}) : super(key: key);
Widget _buildCategory(Category category, BuildContext context) {
final categoryString =
category.toString().replaceAll('Category.', '').toUpperCase();
final ThemeData theme = Theme.of(context);
return GestureDetector(
onTap: () => onCategoryTap(category),
child: category == currentCategory
? Column(
children: <Widget>[
const SizedBox(height: 16.0),
Text(
categoryString,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 14.0),
Container(
width: 70.0,
height: 2.0,
color: kShrinePink400,
),
],
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text(
categoryString,
style: theme.textTheme.bodyLarge!.copyWith(
color: kShrineBrown900.withAlpha(153)
),
textAlign: TextAlign.center,
),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.only(top: 40.0),
color: kShrinePink100,
child: ListView(
children: _categories
.map((Category c) => _buildCategory(c, context))
.toList()),
),
);
}
}
Это GestureDetector, внутри которого находится столбец, дочерними элементами которого являются названия категорий. Подчеркивание используется для обозначения выбранной категории.
В app.dart преобразуйте виджет ShrineApp из stateless в stateful.
- Выделите
ShrineApp. - В зависимости от вашей IDE, отобразите действия с кодом:
- Android Studio: Нажмите ⌥Enter (macOS) или Alt + Enter.
- VS Code: Нажмите ⌘ (macOS) или Ctrl+.
- Выберите "Преобразовать в StatefulWidget".
- Измените класс ShrineAppState на private (_ShrineAppState). Щелкните правой кнопкой мыши по ShrineAppState и
- Android Studio: выберите Рефакторинг > Переименовать
- VS Code: выберите «Переименовать символ».
- Введите _ShrineAppState, чтобы сделать класс приватным.
В app.dart добавьте переменную в _ShrineAppState для выбранной категории и функцию обратного вызова при нажатии на неё:
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
Затем измените задний слой на CategoryMenuPage.
В app.dart импортируйте CategoryMenuPage:
import 'backdrop.dart';
import 'category_menu_page.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';
В функции build() измените значение поля backLayer на CategoryMenuPage, а значение поля currentCategory на переменную экземпляра.
'/': (BuildContext context) => Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: _currentCategory,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: CategoryMenuPage(
currentCategory: _currentCategory,
onCategoryTap: _onCategoryTap,
),
frontTitle: const Text('SHRINE'),
backTitle: const Text('MENU'),
),
Перезагрузите устройство и нажмите кнопку «Меню».
Android | iOS |
|
|
Если нажать на пункт меню, ничего не происходит... пока. Давайте это исправим.
В home.dart добавьте переменную для параметра Category и передайте её в AsymmetricView.
import 'package:flutter/material.dart';
import 'model/product.dart';
import 'model/products_repository.dart';
import 'supplemental/asymmetric_view.dart';
class HomePage extends StatelessWidget {
// TODO: Add a variable for Category (104)
final Category category;
const HomePage({this.category = Category.all, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO: Pass Category variable to AsymmetricView (104)
return AsymmetricView(
products: ProductsRepository.loadProducts(category),
);
}
}
В app.dart передайте _currentCategory для frontLayer :.
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),
Перезагрузите устройство. Нажмите кнопку меню в симуляторе и выберите категорию.
Android | iOS |
|
|
Они отфильтрованы!
Закройте передний слой после выбора пункта меню.
В backdrop.dart добавьте переопределение функции didUpdateWidget() (вызываемой при каждом изменении конфигурации виджета) в _BackdropState:
// TODO: Add override for didUpdateWidget() (104)
@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
_toggleBackdropLayerVisibility();
} else if (!_frontLayerVisible) {
_controller.fling(velocity: _kFlingVelocity);
}
}
Сохраните проект, чтобы запустить горячую перезагрузку. Нажмите на значок меню и выберите категорию. Меню должно закрыться автоматически, и вы увидите выбранную категорию элементов. Теперь вы добавите эту функциональность и на лицевой слой.
Переключить передний слой
В backdrop.dart добавьте функцию обратного вызова, срабатывающую при касании слоя фона:
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key? key,
this.onTap, // New code
required this.child,
}) : super(key: key);
final VoidCallback? onTap; // New code
final Widget child;
Затем добавьте GestureDetector к дочерним элементам _FrontLayer: Column's children:.
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 40.0,
alignment: AlignmentDirectional.centerStart,
),
),
Expanded(
child: child,
),
],
),
Затем реализуйте новое свойство onTap в свойстве _BackdropState в функции _buildStack() :
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
Перезагрузите и коснитесь верхней части переднего слоя. Слой должен открываться и закрываться каждый раз, когда вы коснетесь верхней части переднего слоя.
8. Добавьте фирменный значок.
Фирменная иконография распространяется и на знакомые значки. Давайте сделаем значок раскрытия уникальным и объединим его с заголовком для создания неповторимого фирменного стиля.
Изменить значок кнопки меню
Android | iOS |
|
|
В файле backdrop.dart создайте новый класс _BackdropTitle.
// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
final void Function() onPress;
final Widget frontTitle;
final Widget backTitle;
const _BackdropTitle({
Key? key,
required Animation<double> listenable,
required this.onPress,
required this.frontTitle,
required this.backTitle,
}) : _listenable = listenable,
super(key: key, listenable: listenable);
final Animation<double> _listenable;
@override
Widget build(BuildContext context) {
final Animation<double> animation = _listenable;
return DefaultTextStyle(
style: Theme.of(context).textTheme.titleLarge!,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: <Widget>[
// branded icon
SizedBox(
width: 72.0,
child: IconButton(
padding: const EdgeInsets.only(right: 8.0),
onPressed: this.onPress,
icon: Stack(children: <Widget>[
Opacity(
opacity: animation.value,
child: const ImageIcon(AssetImage('assets/slanted_menu.png')),
),
FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(1.0, 0.0),
).evaluate(animation),
child: const ImageIcon(AssetImage('assets/diamond.png')),
)]),
),
),
// Here, we do a custom cross fade between backTitle and frontTitle.
// This makes a smooth animation between the two texts.
Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, 0.0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: const Offset(-0.25, 0.0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
)
]),
);
}
}
Виджет _BackdropTitle — это пользовательский виджет, который заменит обычный Text виджет в параметре title виджета AppBar . Он имеет анимированную иконку меню и анимированные переходы между заголовками на лицевой и оборотной сторонах экрана. Анимированная иконка меню будет использовать новый ресурс. Ссылку на новый файл slanted_menu.png необходимо добавить в файл pubspec.yaml .
assets:
- assets/diamond.png
# TODO: Add slanted menu asset (104)
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
Удалите свойство leading в конструкторе AppBar . Удаление необходимо для того, чтобы фирменная иконка отображалась на месте исходного виджета ` leading . listenable и обработчик onPress для фирменной иконки передаются в ` _BackdropTitle . Также передаются frontTitle и backTitle , чтобы они отображались внутри заголовка фона. Параметр title в AppBar должен выглядеть следующим образом:
// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
Фирменная иконка создается в элементе _BackdropTitle. Она содержит Stack анимированных иконок: наклонное меню и ромб, который заключен в IconButton , чтобы его можно было нажать. Затем IconButton заключен в SizedBox , чтобы освободить место для горизонтального движения иконок.
Архитектура Flutter «все — это виджет» позволяет изменять макет стандартной AppBar без необходимости создания совершенно нового AppBar виджета. Параметр title , который изначально является Text виджетом, можно заменить более сложным _BackdropTitle . Поскольку _BackdropTitle также включает пользовательскую иконку, он занимает место свойства leading , которое теперь можно опустить. Эта простая замена виджета выполняется без изменения каких-либо других параметров, таких как значки действий, которые продолжают функционировать самостоятельно.
Добавить ярлык для возврата на экран входа в систему.
В backdrop.dart, добавьте ярлык для возврата на экран входа в систему с помощью двух значков в конце панели приложения: измените семантические метки значков, чтобы они отражали их новое назначение.
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: const Icon(
Icons.search,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
),
IconButton(
icon: const Icon(
Icons.tune,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
),
При попытке перезагрузки страницы возникнет ошибка. Для исправления ошибки импортируйте login.dart :
import 'login.dart';
Перезагрузите приложение и нажмите кнопки поиска или настройки, чтобы вернуться на экран входа в систему.
9. Поздравляем!
В ходе этих четырех практических занятий вы научились использовать Material Components для создания уникальных, элегантных пользовательских интерфейсов, отражающих индивидуальность и стиль бренда.
Следующие шаги
Этот практический семинар, MDC-104, завершает данную последовательность семинаров. Вы можете изучить еще больше компонентов Material Flutter, посетив каталог виджетов Material Components .
В качестве дополнительной цели попробуйте заменить фирменную иконку на AnimatedIcon , которая будет анимироваться между двумя иконками, когда фон станет видимым.
Существует множество других практических заданий по Flutter , которые вы можете попробовать в зависимости от ваших интересов. У нас есть еще одно практическое задание, посвященное Material Motion, которое может вас заинтересовать: Создание красивых переходов с помощью Material Motion для Flutter .






















