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

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 в будущем.

Полностью согласен Соглашаться Нейтральный Не согласен Категорически не согласен