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

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 oferecem 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, além dos 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. Essas partes são, na maioria, visualizações, controles e funções tradicionais do SDK da plataforma.

Embora as diretrizes do Material Design mencionem muitos componentes, nem todos são bons candidatos para serem reutilizados no código e, portanto, não podem ser encontrados nos MDC. Você pode criar essas experiências por conta própria para dar um estilo personalizado ao app. Tudo isso usando um código tradicional.

O que você 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

Componente do MDC-Flutter neste codelab

  • Forma

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

Iniciante Intermediário Proficiente

Antes de começar

Para começar a desenvolver apps para dispositivos móveis usando o Flutter, é necessário:

  1. fazer o download do SDK do Flutter e instalá-lo;
  2. atualizar seu PATH com o SDK do Flutter SDK;
  3. instalar o Android Studio com os plug-ins do Flutter e do Dart ou o editor que você preferir;
  4. instalar um emulador do Android, um simulador de iOS (exige um Mac com Xcode) ou usar um dispositivo físico.

Para ver mais informações sobre a instalação do Flutter, consulte Primeiros passos: instalação (link em inglês). Para configurar um editor, consulte Primeiros passos: configurar um editor (link em inglês). Você pode usar as opções padrão para instalar um emulador do Android, como um smartphone Pixel 3 com a imagem do sistema mais recente. Recomendamos ativar a aceleração de VM, embora não seja obrigatório. Depois que concluir as quatro etapas acima, você poderá retornar ao codelab. Para concluir este codelab, você só precisa instalar o Flutter em uma plataforma (Android ou iOS).

Conferir se o SDK do Flutter está no estado correto

Antes de continuar este codelab, verifique se o SDK está no estado correto. Se o SDK do Flutter tiver sido instalado anteriormente, use o comando flutter upgrade para garantir que o SDK esteja no estado mais recente.

 flutter upgrade

O comando flutter upgrade executará automaticamente o flutter doctor. Se esta for uma nova instalação do Flutter e nenhuma atualização for necessária, execute o flutter doctor manualmente. Você receberá uma notificação se for necessário instalar alguma dependência para concluir a configuração. Ignore as marcas de seleção que não forem relevantes para você (por exemplo, Xcode, se você não quiser desenvolver para iOS).

 flutter doctor

Perguntas frequentes

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?

Faça o download do app inicial

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

Configurar seu projeto

As instruções a seguir presumem que você esteja usando o Android Studio (IntelliJ).

Abrir o projeto

1. Abra o Android Studio.

2. Se a tela de boas-vindas for exibida, clique em Open an existing Android Studio project.

3. Navegue até o diretório material-components-flutter-codelabs/mdc_100_series e clique em Open. O projeto será aberto. Você pode ignorar qualquer erro exibido no Dart Analysis antes de criar o projeto pela primeira vez.

4. Se solicitado, faça o seguinte:

  • Instale todas as atualizações de plataforma e de plug-ins ou o FlutterRunConfigurationType.
  • Se o SDK do Dart ou do Flutter não estiver configurado, defina o caminho do SDK do Flutter para o plug-in do Flutter (link em inglês).
  • Configure os frameworks do Android.
  • Clique em "Get dependencies" ou "Run 'flutter packages get'".

Em seguida, reinicie o Android Studio.

Executar o app inicial

As instruções a seguir presumem que você esteja testando em um emulador ou dispositivo Android. Também será possível testar em um simulador ou dispositivo iOS se você tiver o Xcode instalado.

1. Selecione o dispositivo ou emulador. Se o emulador do Android ainda não estiver em execução, selecione Tools -> Android -> AVD Manager para criar um dispositivo virtual e iniciar o emulador. Se já houver um AVD, você poderá iniciar o emulador diretamente do seletor de dispositivos no Android Studio, como mostrado na próxima etapa. Para o simulador do iOS, se ele ainda não estiver em execução, inicie-o na máquina de desenvolvimento selecionando Flutter Device Selection -> Open iOS Simulator.

2. Inicie o app do Flutter:

  • Localize o menu suspenso "Flutter Device Selection" na parte superior da tela do editor e selecione o dispositivo, por exemplo, iPhone SE ou o SDK do Android criado para <version>.
  • Pressione o ícone Play ().

Pronto! Você verá a página de login do app Shrine dos codelabs anteriores no simulador ou emulador.

Android

iOS

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 'package:meta/meta.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,
  })  : assert(currentCategory != null),
        assert(frontLayer != null),
        assert(backLayer != null),
        assert(frontTitle != null),
        assert(backTitle != null);

  @override
  _BackdropState createState() => _BackdropState();
}

// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)

O pacote meta será importado para marcar as propriedades @required. Essa é uma prática recomendada quando há 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. Observe que também temos declarações após o construtor para verificar se os valores transmitidos nesses campos não são null.

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(
      brightness: Brightness.light,
      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 função build() do ShrineApp. Modifique home: para ser um Backdrop com um HomePage como a frontLayer:

      // TODO: Change home: to a Backdrop with a HomePage frontLayer (104)
      home: 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'),
      ),

Se você clicar no botão Play, verá que a nossa página inicial e a barra de apps são exibidas:

Android

iOS

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:

ad988a22875b5e82.png

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

Nesta etapa, você 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,
    this.child,
  }) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Material(
      elevation: 16.0,
      shape: 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

Definimos uma forma personalizada 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 ver a camada de trás do pano de fundo.

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)
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: 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: 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: Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

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

Android

iOS

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 {
  OneProductCardColumn({this.product});

  final Product product;

  @override
  Widget build(BuildContext context) {
    // TODO: Replace Column with a ListView (104)
    return ListView(
      physics: const ClampingScrollPhysics(),
      reverse: true,
      children: <Widget>[
        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

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: EdgeInsetsDirectional.only(start: 28.0),
            child: top != null
                ? ProductCard(
                    imageAspectRatio: imageAspectRatio,
                    product: top,
                  )
                : SizedBox(
                    height: heightOfCards,
                  ),
          ),
          SizedBox(height: spacerHeight),
          Padding(
            padding: 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

Não há mais erros de estouro.

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 'package:meta/meta.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,
  })  : assert(currentCategory != null),
        assert(onCategoryTap != null);

  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>[
            SizedBox(height: 16.0),
            Text(
              categoryString,
              style: theme.textTheme.bodyText1,
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 14.0),
            Container(
              width: 70.0,
              height: 2.0,
              color: kShrinePink400,
            ),
          ],
        )
      : Padding(
        padding: 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: 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. Destaque ShrineApp.
  2. Pressione alt (Opção) + Enter.
  3. Selecione "Convert to StatefulWidget".
  4. Modifique a classe ShrineAppState para ser particular (_ShineAppState). Para fazer isso no menu principal do ambiente de desenvolvimento integrado, selecione "Refactor > Rename". Como alternativa, no código, você pode destacar o nome da classe, ShrineAppState, depois clicar com o botão direito do mouse e selecionar "Refactor > Rename". 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:

// TODO: Convert ShrineApp to stateful widget (104)
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 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'category_menu_page.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';

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

      home: 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: Text('SHRINE'),
        backTitle: Text('MENU'),
      ),

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

Android

iOS

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/products_repository.dart';
import 'model/product.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});

  @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

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

Atualize o simulador, toque no ícone de menu e selecione uma categoria. O menu será fechado automaticamente, e você verá a categoria dos itens selecionados. Agora, você 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
    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 superior da camada em primeiro plano.

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

Em backdrop.dart, crie uma nova classe _BackdropTitle.

// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
  final Function onPress;
  final Widget frontTitle;
  final Widget backTitle;

  const _BackdropTitle({
    Key key,
    Listenable listenable,
    this.onPress,
    @required this.frontTitle,
    @required this.backTitle,
  })  : assert(frontTitle != null),
        assert(backTitle != null),
        super(key: key, listenable: listenable);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = this.listenable;

    return DefaultTextStyle(
      style: Theme.of(context).primaryTextTheme.headline6,
      softWrap: false,
      overflow: TextOverflow.ellipsis,
      child: Row(children: <Widget>[
        // branded icon
        SizedBox(
          width: 72.0,
          child: IconButton(
            padding: EdgeInsets.only(right: 8.0),
            onPressed: this.onPress,
            icon: Stack(children: <Widget>[
              Opacity(
                opacity: animation.value,
                child: ImageIcon(AssetImage('assets/slanted_menu.png')),
              ),
              FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: Offset(1.0, 0.0),
                ).evaluate(animation),
                child: 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: Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: Offset(0.5, 0.0),
                ).evaluate(animation),
                child: backTitle,
              ),
            ),
            Opacity(
              opacity: CurvedAnimation(
                parent: animation,
                curve: Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: 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: Icon(
            Icons.search,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
            );
          },
        ),
        IconButton(
          icon: 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.

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. Para aprender sobre mais componentes no MDC-Flutter, acesse o catálogo de widgets do Flutter (link em inglês).

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.

Para saber como conectar um app ao Firebase para ter um back-end ativo, consulte o codelab Firebase para 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 componentes do Material Design no futuro

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