MDC-104 Flutter: усовершенствованные компоненты материалов

MDC-104 Flutter:
усовершенствованные компоненты материалов

О практической работе

subjectПоследнее обновление: июн. 6, 2023
account_circleАвторы: Material Flutter Team

1. Введение

logo_comComponents_color_2x_web_96dp.png

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

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

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

список меню 4 категории

список меню 4 категории

Компоненты и подсистемы 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

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

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

Успех! На вашем устройстве вы должны увидеть страницу входа в 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

Страница продукта Shrine с розовым фоном

Страница продукта Shrine с розовым фоном

BackLayer показывает розовую область на новом слое позади домашней страницы frontLayer.

Вы можете использовать Flutter Inspector , чтобы убедиться, что в стеке действительно есть контейнер за домашней страницей. Это должно быть похоже на это:

92ed338a15a074bd.png

Теперь вы можете настроить дизайн и содержимое слоев.

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

Страница продукта Shrine с индивидуальной формой

Страница продукта 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,
     
),
   
),
 
);
}

Горячий перезапуск. Теперь должна появиться новая цветная панель приложения.

Андроид

iOS

Страница продукта Shrine с цветной панелью приложения

Страница продукта Shrine с цветной панелью приложения

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

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

Пустое меню Shrine с двумя ошибками

Пустое меню Shrine с двумя ошибками

Передний слой анимируется (сдвигается) вниз. Но если вы посмотрите вниз, увидите красную ошибку и ошибку переполнения. Это связано с тем, что 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

Пустое меню Shrine с одной ошибкой

Пустое меню Shrine с одной ошибкой

Предупреждение серого цвета о переполнении 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 из состояния без сохранения состояния в состояние с сохранением состояния.

  1. Выделите ShrineApp.
  2. На основе вашей IDE покажите действия кода:
  3. Android Studio: нажмите ⌥Enter (macOS) или Alt + Enter.
  4. VS Code: нажмите ⌘. (macOS) или Ctrl+.
  5. Выберите «Преобразовать в StatefulWidget».
  6. Измените класс ShrineAppState на частный (_ShrineAppState). Щелкните правой кнопкой мыши ShrineAppState и
  7. Android Studio: выберите «Рефакторинг» > «Переименовать».
  8. VS Code: выберите «Переименовать символ».
  9. Введите _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

Меню храма с 4 категориями

Меню храма с 4 категориями

Если вы коснетесь пункта меню, ничего не произойдет… пока. Давайте это исправим.

В 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

Страница продукта с фильтром Shrine

Страница продукта с фильтром Shrine

Они фильтруются!

Закрыть передний слой после выбора меню

В 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

Страница продукта Shrine с фирменным значком

Страница продукта Shrine с фирменным значком

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

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

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