1. Введение
Material Components (MDC) помогают разработчикам реализовать Material Design. Созданный командой инженеров и UX-дизайнеров Google, MDC включает в себя десятки красивых и функциональных компонентов пользовательского интерфейса и доступен для Android, iOS, Интернета и Flutter.material.io/develop. |
В codelab MDC-103 вы настроили цвет, высоту, типографику и форму Material Components (MDC) для стилизации своего приложения.
Компонент в системе Material Design выполняет набор предопределенных задач и имеет определенные характеристики, как кнопка. Однако кнопка — это больше, чем просто способ выполнения пользователем действия, это также визуальное выражение формы, размера и цвета, которое позволяет пользователю понять, что она интерактивна и что что-то произойдет при прикосновении или щелчке.
Рекомендации Material Design описывают компоненты с точки зрения дизайнера. Они описывают широкий спектр базовых функций, доступных на разных платформах, а также анатомические элементы, составляющие каждый компонент. Например, фон содержит задний слой и его содержимое, передний слой и его содержимое, правила движения и параметры отображения. Каждый из этих компонентов можно настроить в соответствии с потребностями, вариантами использования и контентом каждого приложения.
Что ты построишь
В этой лаборатории вы измените пользовательский интерфейс приложения Shrine на двухуровневую презентацию, называемую «фоном». Фон включает меню, в котором перечислены выбираемые категории, используемые для фильтрации продуктов, показанных в асимметричной сетке. В этой лаборатории кода вы будете использовать следующее:
- Форма
- Движение
- Виджеты Flutter (которые вы использовали в предыдущих мастер-классах по написанию кода)
Андроид | 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 из предыдущих тестов кода.
Андроид | iOS |
4. Добавьте фоновое меню
Фон появляется позади всего остального контента и компонентов. Он состоит из двух слоев: заднего слоя (отображает действия и фильтры) и переднего уровня (отображает контент). Вы можете использовать фон для отображения интерактивной информации и действий, таких как навигация или фильтры контента.
Удалить домашнюю панель приложений
Виджет 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. Но тело Скаффолда — это 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'),
),
Сохраните проект. Вы должны увидеть, что появилась наша домашняя страница, а также панель приложения:
Андроид | iOS |
BackLayer показывает розовую область на новом слое позади домашней страницы frontLayer.
Вы можете использовать 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),
],
);
}
Перезагрузить.
Андроид | iOS |
Мы придали основной поверхности Храма особую форму. Однако мы хотим, чтобы это визуально было связано с панелью приложения.
Изменение цвета панели приложения
В 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,
),
),
);
}
Горячий перезапуск. Теперь должна появиться новая цветная панель приложения.
Андроид | 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. Этот виджет будет исключать пункты меню заднего слоя из семантического дерева, когда задний слой не виден.
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,
),
Перезагрузите компьютер, затем нажмите кнопку меню в симуляторе.
Андроид | iOS |
Передний слой анимируется (сдвигается) вниз. Но если вы посмотрите вниз, увидите красную ошибку и ошибку переполнения. Это связано с тем, что AsymmetricView сжимается и становится меньше из-за этой анимации, что, в свою очередь, оставляет меньше места для столбцов. В конце концов, столбцы не могут расположиться в заданном пространстве, что приводит к ошибке. Если мы заменим столбцы на ListViews, размер столбца должен остаться таким же, как они анимируются.
Оберните столбцы продуктов в 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
. Детский порядок меняется на обратный, чтобы компенсировать изменение.
Перезагрузите и нажмите кнопку меню.
Андроид | 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
.
Перезагрузить. Затем нажмите кнопку меню.
Андроид | 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 из состояния без сохранения состояния в состояние с сохранением состояния.
- Выделите
ShrineApp.
- На основе вашей IDE покажите действия кода:
- Android Studio: нажмите ⌥Enter (macOS) или Alt + Enter.
- VS Code: нажмите ⌘. (macOS) или Ctrl+.
- Выберите «Преобразовать в StatefulWidget».
- Измените класс ShrineAppState на частный (_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'),
),
Перезагрузите и нажмите кнопку «Меню».
Андроид | 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),
Перезагрузить. Нажмите кнопку меню в симуляторе и выберите категорию.
Андроид | 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. Добавьте фирменную иконку
Фирменная иконография распространяется и на знакомые иконки. Давайте сделаем значок раскрытия индивидуальным и объединим его с нашим заголовком, чтобы придать ему уникальный фирменный вид.
Изменить значок кнопки меню
Андроид | 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 для Flutter .