MDC-104 Flutter: componentes avançados do Material Design

1. Introdução

logo_components_color_2x_web_96dp.png

Os componentes do Material Design (MDC, na sigla em inglês) ajudam os desenvolvedores a implementar o Material Design. Criados por uma equipe de engenheiros e designers de UX do Google, os MDC apresentam dezenas de componentes de IU bonitos e funcionais e estão disponíveis para Android, iOS, Web e Flutter.material.io/develop

No codelab MDC-103, você personalizou a cor, a elevação, a tipografia e a forma dos Componentes do Material Design (MDC) para criar o estilo do app.

Um componente do sistema do Material Design realiza um conjunto de tarefas predefinidas e tem características específicas, como um botão. No entanto, um botão é mais do que apenas uma maneira de um usuário realizar uma ação. Ele também é uma expressão visual de forma, tamanho e cor que indica aos usuários que ele é interativo e que acontecerá algo quando os usuários clicarem ou tocarem nele.

As diretrizes do Material Design descrevem os componentes do ponto de vista de um designer. Elas descrevem uma grande variedade de funções básicas disponíveis nas plataformas e os elementos anatômicos que fazem parte de cada componente. Por exemplo, um pano de fundo contém uma camada de fundo e o conteúdo dela, uma camada em primeiro plano e o conteúdo dela, regras de movimento e opções de exibição. Cada um desses componentes pode ser personalizado conforme as necessidades, os casos de uso e o conteúdo de cada app.

O que você vai criar

Neste codelab, você modificará a IU do app Shrine para usar uma apresentação em dois níveis conhecida como "pano de fundo". Um pano de fundo inclui um menu com uma lista de categorias selecionáveis usadas para filtrar os produtos mostrados na grade assimétrica. Neste codelab, você usará os seguintes componentes do Flutter:

  • Forma
  • Movimento
  • Widgets do Flutter (usados nos codelabs anteriores)

Android

iOS

app de e-commerce com um tema rosa e marrom, uma barra de apps superior e uma grade assimétrica e rolável horizontalmente cheia de produtos

app de e-commerce com um tema rosa e marrom, uma barra de apps superior e uma grade assimétrica e rolável horizontalmente cheia de produtos

menu listando quatro categorias

menu listando quatro categorias

Componentes e subsistemas do MDC-Flutter neste codelab

  • Forma

Como você classificaria seu nível de experiência em desenvolvimento com o Flutter?

Iniciante Intermediário Proficiente

2. Configurar o ambiente de desenvolvimento do Flutter

Você precisa de dois softwares para concluir este laboratório: o SDK do Flutter e um editor.

É possível executar o codelab usando qualquer um destes dispositivos:

  • Um dispositivo físico Android ou iOS conectado ao seu computador e configurado para o modo de desenvolvedor.
  • O simulador para iOS, que exige a instalação de ferramentas do Xcode.
  • O Android Emulator, que requer configuração no Android Studio.
  • Um navegador (o Chrome é necessário para depuração).
  • Como um aplicativo para computador Windows, Linux ou macOS. Você precisa desenvolver na plataforma em que planeja implantar. Portanto, se quiser desenvolver um app para um computador Windows, você terá que desenvolver no Windows para acessar a cadeia de builds adequada. Há requisitos específicos de cada sistema operacional que são abordados em detalhes em docs.flutter.dev/desktop.

3. Fazer o download do app inicial do codelab

Está continuando do MDC-103?

Se você concluiu o MDC-103, o código para este codelab já está pronto. Pule para a etapa: Adicionar o menu do pano de fundo.

Está começando do zero?

O app inicial está localizado no diretório material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.

... ou clone-o do GitHub

Para clonar este codelab do GitHub, execute estes comandos:

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

Abrir o projeto e executar o app

  1. Abrir o projeto no seu editor favorito.
  2. Siga as instruções para "Executar o app" em Introdução: Test drive no editor escolhido.

Pronto. A página de login do Shrine dos codelabs anteriores será exibida no seu dispositivo.

Android

iOS

Página de login do Shrine

Página de login do Shrine

4. Adicionar o menu do pano de fundo

Um pano de fundo será exibido atrás de todos os outros conteúdos e componentes. Ele é composto por duas camadas: uma camada de fundo, que exibe ações e filtros, e uma camada em primeiro plano, que exibe o conteúdo. Você pode usar um pano de fundo para exibir informações e ações interativas, como filtros de navegação ou conteúdo.

Remover a barra de apps da tela de início

O widget HomePage será o conteúdo da nossa camada em primeiro plano. No momento, ele tem apenas uma barra de apps. Moveremos a barra de apps para a camada de fundo, e o HomePage incluirá apenas a AsymmetricView.

Em home.dart, modifique a função build() para retornar apenas uma AsymmetricView:

// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));

Adicionar o widget Backdrop

Crie um widget com o nome Backdrop que inclua a frontLayer e a backLayer.

A backLayer inclui um menu que permite selecionar uma categoria para filtrar a lista (currentCategory). Como queremos que a seleção do menu persista, faremos Backdrop ser um widget com estado.

Adicione um novo arquivo a /lib com o nome 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)

Observe que marcamos determinadas propriedades required. Essa é uma prática recomendada para propriedades no construtor que não têm valor padrão e não podem ser null e, portanto, não podem ser deixadas de lado.

Na definição da classe Backdrop, adicione a classe _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(),
    );
  }
}

A função build() retorna um Scaffold com uma barra de apps da mesma forma que o HomePage. No entanto, o corpo do Scaffold é uma Pilha. Os elementos filhos de uma pilha podem se sobrepor. O tamanho e a localização de cada filho são especificados em relação ao pai da pilha.

Agora, adicione uma instância do Backdrop ao ShrineApp.

Em app.dart, importe backdrop.dart e 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';

Em app.dart,, modifique a rota / retornando um Backdrop que tem HomePage como 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'),
),

Após salvar seu projeto, nossa página inicial e a barra de apps vão aparecer:

Android

iOS

Página de produto do Shrine com fundo rosa

Página de produto do Shrine com fundo rosa

A backLayer mostra a área rosa em uma nova camada atrás da frontLayer da página inicial.

Use o Flutter Inspector (link em inglês) para verificar se a pilha realmente tem um contêiner por trás do HomePage. O resultado será semelhante a este:

4783ed30f1cc010.png

Agora, você pode ajustar o design e o conteúdo das duas camadas.

5. Adicionar uma forma

Nesta etapa, você vai criar o estilo da camada em primeiro plano para adicionar um corte no canto superior esquerdo.

O Material Design se refere a esse tipo de personalização como uma forma. As superfícies do Material Design podem ter formas arbitrárias. As formas dão ênfase e estilo às superfícies e podem ser usadas para expressar o branding. As formas retangulares comuns podem ser personalizadas com bordas e arestas curvas ou inclinadas para qualquer um dos lados. Elas podem ser simétricas ou irregulares.

Adicionar uma forma à camada em primeiro plano

O logotipo inclinado do Shrine foi a inspiração da história da forma desse app. A história da forma é o uso comum das formas aplicadas em um app. Por exemplo, a forma do logotipo é refletida nos elementos da página de login que têm uma forma aplicada a eles. Nesta etapa, você estilizará a camada em primeiro plano usando um corte inclinado no canto superior esquerdo.

Em backdrop.dart, adicione uma nova classe _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,
          ),
        ],
      ),
    );
  }
}

Em seguida, na função _buildStack() de _BackdropState, una a camada em primeiro plano a uma classe _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),
      ],
    );
  }

Atualize o simulador.

Android

iOS

Página de produto do Shrine com formato personalizado

Página de produto do Shrine com formato personalizado

Definimos um formato personalizado para a superfície principal do Shrine. Devido à elevação da superfície, os usuários podem perceber que há algo por trás da camada branca. Vamos adicionar movimento para que os usuários possam conferir a camada de trás do pano de fundo.

6. Adicionar movimento

Movimentos são uma forma de dar vida ao app. Eles podem ser grandes e chamativos, sutis e minimalistas, ou do jeito que você preferir. Porém, lembre-se de que o tipo de movimento usado precisa ser adequado à situação. Os movimentos aplicados a ações frequentes e repetidas precisam ser sutis para que as ações não distraiam o usuário nem desperdicem tempo demais. No entanto, há situações adequadas, como a primeira vez que um usuário abre um app, em que usar um movimento mais chamativo e algumas animações pode ajudar a orientar o usuário sobre o uso do app.

Adicionar um movimento de revelação ao botão de menu

Na parte superior de backdrop.dart, fora do escopo de qualquer classe ou função, adicione uma constante para representar a velocidade que queremos dar à animação:

// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;

Adicione um widget AnimationController a _BackdropState, instancie-o na função initState() e descarte-o na função dispose() do estado:

  // 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)

O AnimationController coordena as animações e fornece a API para reproduzir, inverter e interromper a animação. Agora, precisamos de funções para fazer os movimentos.

Adicione funções que determinam e alteram a visibilidade da camada em primeiro plano:

  // 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);
  }

Una a backLayer a um widget ExcludeSemantics. Esse widget excluirá os itens de menu da backLayer da árvore de semântica quando a camada de fundo não estiver visível.

    return Stack(
      key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        ExcludeSemantics(
          child: widget.backLayer,
          excluding: _frontLayerVisible,
        ),
      ...

Mude a função _buildStack() para receber o BuildContext e BoxConstraints. Além disso, inclua uma PositionedTransition que receba uma animação 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,
          ),
        ),
      ],
    );
  }

Por fim, em vez de chamar a função _buildStack para o corpo do Scaffold, retorne um widget LayoutBuilder que usa a _buildStack como o builder:

    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: LayoutBuilder(builder: _buildStack),
    );

Atrasamos a criação da pilha de camadas em primeiro plano/de fundo até o tempo de layout usando LayoutBuilder, para podermos incorporar a altura geral real do pano de fundo. O LayoutBuilder é um widget especial cujo callback do builder fornece restrições de tamanho.

Na função build(), transforme o ícone de menu inicial da barra de apps em um IconButton e use-o para alternar a visibilidade da camada em primeiro plano quando um usuário tocar no botão.

      // TODO: Replace leading menu icon with IconButton (104)
      leading: IconButton(
        icon: const Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

Atualize o simulador e, em seguida, toque no botão de menu.

Android

iOS

Menu vazio do Shrine com dois erros

Menu vazio do Shrine com dois erros

A camada em primeiro plano é animada (desliza) para baixo. Porém, há um erro em vermelho e um erro de estouro na parte inferior. Isso ocorre porque essa animação aperta e diminui a AsymmetricView, o que dá menos espaço às colunas. Assim, elas não podem ser dispostas no espaço fornecido e isso resulta em um erro. Se substituirmos as colunas por ListViews, o tamanho das colunas permanecerá igual durante as animações.

Unir colunas de produtos em uma ListView

Em supplemental/product_columns.dart, substitua a coluna em OneProductCardColumn por uma 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>[
        const SizedBox(
          height: 40.0,
        ),
        ProductCard(
          product: product,
        ),
      ],
    );
  }
}

A coluna inclui MainAxisAlignment.end. Para iniciar o layout na parte inferior, marque reverse: true. A ordem dos filhos será invertida por causa dessa mudança.

Atualize o simulador e, em seguida, toque no botão de menu.

Android

iOS

Menu vazio do Shrine com um erro

Menu vazio do Shrine com um erro

O aviso de estouro em cinza da OneProductCardColumn desapareceu. Agora, vamos corrigir o outro erro.

Em supplemental/product_columns.dart, mude a forma como a imageAspectRatio é calculada e substitua a coluna em TwoProductCardColumn por uma 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,
            ),
          ),
        ],
      );

Também adicionamos um pouco de segurança à imageAspectRatio.

Atualize o simulador. Depois, toque no botão de menu.

Android

iOS

Menu vazio do Shrine

Menu vazio do Shrine

Não há mais erros de estouro.

7. Adicionar um menu à camada de fundo

Um menu é uma lista de itens de texto tocáveis que notificam os listeners quando esses itens são tocados. Nesta etapa, você adicionará um menu de filtragem de categoria.

Adicionar o menu

Adicione o menu à camada em primeiro plano e os botões interativos à camada de fundo.

Crie um novo arquivo com o nome 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.bodyText1,
                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.bodyText1!.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()),
      ),
    );
  }
}

Ele é um GestureDetector que une uma coluna cujos filhos são os nomes das categorias. Um sublinhado é usado para indicar a categoria selecionada.

Em app.dart, converta o widget ShrineApp de sem estado para com estado.

  1. Destacar ShrineApp.
  2. Com base no seu IDE, mostre as ações do código:
  3. Android Studio: pressione ⌥Enter (macOS) ou Alt + Enter
  4. VS Code: pressione ⌘. (macOS) ou Ctrl+.
  5. Selecione "Converter para StatefulWidget".
  6. Modifique a classe ShrineAppState para ser particular (_ShineAppState). Clique em ShrineAppState com o botão direito do mouse e:
  7. Android Studio: selecione Refatorar > Renomear
  8. VS Code: selecione "Renomear símbolo"
  9. Digite _ShrineAppState para tornar a classe particular.

Em app.dart, adicione uma variável a _ShrineAppState para a categoria selecionada e um callback quando ela for tocada:

class _ShrineAppState extends State<ShrineApp> {
  Category _currentCategory = Category.all;

  void _onCategoryTap(Category category) {
    setState(() {
      _currentCategory = category;
    });
  }

Em seguida, mude a camada de fundo para uma CategoryMenuPage.

Em app.dart, importe a 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';

Na função build(), mude o campo backLayer para CategoryMenuPage e o campo currentCategory para usar a variável da instância.

'/': (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'),
            ),

Atualize o simulador e, em seguida, toque no botão de menu.

Android

iOS

Menu do Shrine com quatro categorias

Menu do Shrine com quatro categorias

Se você tocar em uma opção de menu, nada acontecerá ainda. Vamos corrigir isso.

Em home.dart, adicione uma variável para a categoria e transmita-a para a 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),
    );
  }
}

Em app.dart, transmita a _currentCategory para a frontLayer:

// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),

Atualize o simulador. Toque no botão de menu no simulador e selecione uma categoria.

Android

iOS

Página do produto filtrada pelo Shrine

Página do produto filtrada pelo Shrine

Toque no ícone do menu para ver os produtos. Eles foram filtrados.

Fechar a camada em primeiro plano após uma seleção de menu

Em backdrop.dart, adicione uma substituição para a função didUpdateWidget() (chamado sempre que a configuração do widget muda) em _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);
    }
  }

Salve seu projeto para acionar uma recarga automática. Toque no ícone do menu e selecione uma categoria. O menu será fechado automaticamente e a categoria dos itens selecionados será exibida. Agora, você vai adicionar essa funcionalidade à camada em primeiro plano.

Alternar a camada em primeiro plano

Em backdrop.dart, adicione um callback que responde ao toque à camada do pano de fundo:

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;

Em seguida, adicione um GestureDetector ao filho da _FrontLayer e aos filhos da coluna:

      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,
          ),
        ],
      ),

Em seguida, implemente a nova propriedade onTap em _BackdropState na função _buildStack():

          PositionedTransition(
            rect: layerAnimation,
            child: _FrontLayer(
              // TODO: Implement onTap property on _BackdropState (104)
              onTap: _toggleBackdropLayerVisibility,
              child: widget.frontLayer,
            ),
          ),

Atualize o simulador e toque na parte superior da camada em primeiro plano. A camada será aberta e fechada sempre que você tocar na parte de cima da camada em primeiro plano.

8. Adicionar um ícone de marca

A iconografia da marca também se aplica aos ícones conhecidos. Vamos personalizar o ícone de revelação e mesclá-lo ao nosso título para criar um visual exclusivo para a marca.

Mudar o ícone do botão de menu

Android

iOS

Página de produto do Shrine com ícone de marca

Página de produto do Shrine com ícone de marca

Em backdrop.dart, crie uma nova classe _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.headline6!,
      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,
              ),
            ),
          ],
        )
      ]),
    );
  }
}

O _BackdropTitle é um widget personalizado que substitui o widget Text simples pelo parâmetro title do widget AppBar. Ele tem um ícone de menu e transições animados entre o título em primeiro plano e o título de fundo. O ícone de menu animado usará um novo recurso. A referência ao novo slanted_menu.png precisa ser adicionada ao arquivo pubspec.yaml.

assets:
    - assets/diamond.png
    # TODO: Add slanted menu asset (104)
    - assets/slanted_menu.png
    - packages/shrine_images/0-0.jpg

Remova a propriedade leading do builder AppBar. É necessário remover o ícone personalizado da marca para que ele seja renderizado no lugar do widget leading original. A animação listenable e o gerenciador onPress do ícone da marca são transmitidos para o _BackdropTitle. O frontTitle e o backTitle também são transmitidos para que possam ser renderizados no título do pano de fundo. O parâmetro title do AppBar ficará assim:

// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
  listenable: _controller.view,
  onPress: _toggleBackdropLayerVisibility,
  frontTitle: widget.frontTitle,
  backTitle: widget.backTitle,
),

O ícone da marca é criado no _BackdropTitle.. Ele contém uma Stack de ícones animados: um menu inclinado e um losango, que são unidos em um IconButton que pode ser pressionado. O IconButton será unido em uma SizedBox para criar espaço para o movimento horizontal do ícone.

A arquitetura "tudo é um widget" do Flutter permite que o layout da AppBar padrão seja alterado sem ter que criar um widget AppBar totalmente novo. O parâmetro title, que é originalmente um widget Text, pode ser substituído por um _BackdropTitle mais complexo. Como o _BackdropTitle também inclui o ícone personalizado, ele substitui a propriedade leading, que agora poderá ser omitida. Essa simples substituição de widget é realizada sem alterar nenhum dos outros parâmetros, como os ícones de ação, que continuarão funcionando.

Adicionar um atalho para a tela de login

Em backdrop.dart, adicione um atalho para voltar à tela de login usando os dois ícones à direita na barra de apps. Mude as etiquetas semânticas dos ícones para refletir a nova funcionalidade.

        // 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()),
            );
          },
        ),

Você receberá um erro se tentar atualizar o app. Importe login.dart para corrigir o erro:

import 'login.dart';

Atualize o app e toque nos botões de pesquisa ou ajuste para voltar à tela de login.

9. Parabéns!

Nesses quatro codelabs, você viu como usar os componentes do Material Design para criar experiências do usuário únicas e elegantes que expressam a personalidade e o estilo da marca.

Próximas etapas

Este codelab, o MDC-104, encerra essa sequência de codelabs. Acesse o catálogo de widgets dos componentes do Material Design para aprender sobre mais componentes no MDC-Flutter.

Como uma meta extra, tente substituir o ícone da marca por um AnimatedIcon (link em inglês) que transiciona entre a animação de dois ícones quando o pano de fundo fica visível.

Existem muitos outros codelabs do Flutter que você pode testar se quiser. Temos outro codelab específico do Material e que pode ser do seu interesse: Como criar transições incríveis com o movimento do Material Design para o Flutter.

Este codelab exigiu esforço e tempo normais para ser concluído

Concordo totalmente Concordo Não concordo nem discordo Discordo Discordo totalmente

Quero continuar usando os componentes do Material Design no futuro

Concordo totalmente Concordo Não concordo nem discordo Discordo Discordo totalmente