1. Введение
| Компоненты Material Design (MDC) помогают разработчикам внедрять Material Design. MDC – это набор из десятков красивых и функциональных компонентов интерфейса, созданный командой инженеров и UX-дизайнеров Google. Он доступен для Android, iOS, веб-приложений и Flutter.material.io/develop |
В практической работе MDC-103 вы настроили цвет, высоту, типографику и форму компонентов Material Design (MDC) для оформления приложения.
Компонент системы проектирования Material Design выполняет набор заранее определенных задач и имеет определенные характеристики, например кнопку. Однако кнопка – это не только способ совершить действие, но и визуальный элемент, который по форме, размеру и цвету дает пользователю понять, что он интерактивный и что-то произойдет при нажатии.
В руководстве по Material Design компоненты описаны с точки зрения дизайнера. Они описывают широкий спектр базовых функций, доступных на разных платформах, и анатомические элементы, из которых состоит каждый компонент. Например, фон содержит задний слой и его контент, передний слой и его контент, правила движения и параметры показа. Каждый из этих компонентов можно настроить в соответствии с потребностями, вариантами использования и контентом приложения.
Что вы создадите
В этой практической работе вы измените интерфейс приложения Shrine, добавив двухуровневую презентацию, которая называется "задник". На фоне есть меню со списком категорий, которые можно выбрать, чтобы отфильтровать товары, показанные в асимметричной сетке. В этой практической работе вы будете использовать:
- Фигура
- Анимация
- Виджеты Flutter (которые вы использовали в предыдущих практических работах).
Android | iOS |
|
|
|
|
Компоненты и подсистемы Material Design для Flutter, используемые в этой практической работе
- Фигура
Как бы вы оценили свой уровень опыта в разработке на Flutter?
2. Как настроить среду разработки Flutter
Для выполнения этого практического занятия вам понадобятся два компонента: Flutter SDK и редактор.
Вы можете выполнить практическую работу на любом из этих устройств:
- Физическое устройство Android или iOS, подключенное к компьютеру и настроенное в режиме разработчика.
- Симулятор iOS (требуется установка инструментов Xcode).
- Эмулятор Android (требуется настройка в Android Studio).
- Браузер (для отладки требуется Chrome).
- Как приложение для компьютеров с Windows, Linux или macOS. Разработку нужно вести на той платформе, на которой вы планируете развертывание. Поэтому, если вы хотите разработать компьютерное приложение для Windows, вам необходимо использовать Windows, чтобы получить доступ к соответствующей цепочке сборки. Требования к операционной системе подробно описаны на сайте docs.flutter.dev/desktop.
3. Скачайте стартовое приложение для практической работы
Продолжение 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 будет контентом переднего слоя. Прямо сейчас у него есть панель приложения. Мы перенесем панель приложений на задний слой, а на главной странице останется только AsymmetricView.
В файле home.dart измените функцию build(), чтобы она возвращала только AsymmetricView:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
Как добавить виджет Backdrop
Создайте виджет 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 с панелью приложения, как и раньше. Но тело 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, у которого HomePage является frontLayer:
// 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 |
|
|
backLayer – розовый фон на новом слое за главной страницей frontLayer.
Вы можете использовать Flutter Inspector, чтобы убедиться, что у Stack действительно есть Container за HomePage. Это будет выглядеть примерно так:

Теперь вы можете изменить дизайн и контент обоих слоев.
5. Добавить фигуру
На этом шаге вы добавите вырез в левом верхнем углу переднего слоя.
В Material Design этот тип настройки называется формой. Поверхности материалов могут иметь произвольную форму. Фигуры помогают расставить акценты и добавить стиль, а также могут использоваться для продвижения бренда. Обычные прямоугольные фигуры можно изменять, добавляя им закругленные или скошенные углы и края, а также любое количество сторон. Они могут быть симметричными или неправильной формы.
Как добавить фигуру на передний план
Угловой логотип Shrine вдохновил на создание истории форм для приложения Shrine. История форм – это обычное использование форм, которые применяются во всем приложении. Например, форма логотипа повторяется в элементах страницы входа, к которым применена форма. На этом этапе вы создадите передний слой с угловым вырезом в левом верхнем углу.
В разделе "Клиенты API" (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 из семантического дерева, когда задний слой не виден.
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
В файле 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 и замените Column в 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 currentCatego<ry;
fi>nal ValueChangedCategory onC<ategoryT>ap;
final ListCategory _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);
r>eturn 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: _ca>tegories
.map((Category c) = _buildCategory(c, context))
.toList()),
),
);
}
}
Это GestureDetector, который оборачивает Column, дочерними элементами которого являются названия категорий. Выбранная категория подчеркивается.
В app.dart преобразуйте виджет ShrineApp из stateless в stateful.
- Выделить:
ShrineApp. - В зависимости от используемой IDE, выберите действия с кодом:
- Android Studio: нажмите ⌥Ввод (macOS) или Alt + Ввод
- VS Code: нажмите ⌘. (macOS) или Ctrl+.
- Выберите "Преобразовать в StatefulWidget".
- Измените класс ShrineAppState на частный (_ShrineAppState). Нажмите правой кнопкой мыши на ShrineAppState и
- Android Studio: выберите Refactor > Rename (Рефакторинг > Переименовать).
- VS Code: выберите Rename Symbol (Переименовать символ).
- Введите _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 co>ntext) = 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 добавьте переменную для категории и передайте ее в 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 добавьте обратный вызов on-tap в фоновый слой:
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:.
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(
translatio<n: Twe>enOffset(
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(
tr<anslat>ion: TweenOffset(
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(
tr<anslat>ion: TweenOffset(
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 co>ntext) = LoginPage()),
);
},
),
IconButton(
icon: const Icon(
Icons.tune,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildCo>ntext context) = LoginPage()),
);
},
),
При попытке перезагрузить страницу появится сообщение об ошибке. Чтобы устранить ошибку, импортируйте login.dart:
import 'login.dart';
Перезагрузите приложение и нажмите кнопку поиска или настройки, чтобы вернуться на экран входа.
9. Поздравляем!
В ходе этих четырех практических работ вы узнали, как использовать компоненты Material Design для создания уникальных и элегантных интерфейсов, отражающих индивидуальность и стиль бренда.
Дальнейшие действия
Эта практическая работа (MDC-104) завершает серию. Вы можете изучить ещё больше компонентов в Material Flutter, посетив каталог виджетов Material Components.
В качестве дополнительной цели попробуйте заменить значок бренда на AnimatedIcon, который будет анимировать переход между двумя значками, когда фон станет видимым.
Вы можете попробовать другие практические работы по Flutter, которые соответствуют вашим интересам. Также рекомендуем ознакомиться с другой практической работой, посвященной Material Design: Создание красивых переходов с помощью Material Motion для Flutter.






















