MDC-104 Flutter: componenti Material Advanced

1. Introduzione

logo_components_color_2x_web_96dp.png

Material Components (MDC) aiutano gli sviluppatori a implementare Material Design. Creato da un team di ingegneri e designer UX di Google, MDC offre dozzine di componenti dell'interfaccia utente belli e funzionali ed è disponibile per Android, iOS, web e Flutter.material.io/develop

Nel codelab MDC-103, hai personalizzato il colore, l'elevazione, la tipografia e la forma dei componenti dei materiali (MDC) per definire lo stile della tua app.

Un componente del sistema Material Design esegue una serie di attività predefinite e presenta determinate caratteristiche, ad esempio un pulsante. Tuttavia, un pulsante è molto più di un modo per consentire a un utente di eseguire un'azione. È anche un'espressione visiva di forma, dimensioni e colore che comunica all'utente che è interattivo e che qualcosa accadrà al tocco o al clic.

Le linee guida di Material Design descrivono i componenti dal punto di vista di un designer. Descrivono un'ampia gamma di funzioni di base disponibili su più piattaforme, nonché gli elementi anatomici che compongono ogni componente. Ad esempio, uno sfondo contiene un livello posteriore e i relativi contenuti, il livello anteriore e i relativi contenuti, regole di movimento e opzioni di visualizzazione. Ciascuno di questi componenti può essere personalizzato in base alle esigenze, ai casi d'uso e ai contenuti di ogni app.

Cosa creerai

In questo codelab, modificherai l'interfaccia utente dell'app Santuario in una presentazione a due livelli chiamata "sfondo". Lo sfondo include un menu che elenca le categorie selezionabili utilizzate per filtrare i prodotti mostrati nella griglia asimmetrica. In questo codelab utilizzerai quanto segue:

  • Shape
  • Movimento
  • Widget Flutter (utilizzati nei codelab precedenti)

Android

iOS

App di e-commerce a tema rosa e marrone con una barra delle app in alto e una griglia asimmetrica scorrevole orizzontalmente piena di prodotti

app di e-commerce a tema rosa e marrone con una barra delle app in alto e una griglia asimmetrica a scorrimento orizzontale piena di prodotti.

menu con 4 categorie

scheda del menu 4 categorie

Componenti e sottosistemi Flutter Material in questo codelab

  • Shape

Come valuteresti il tuo livello di esperienza nello sviluppo di Flutter?

Principiante Intermedio Proficiente

2. Configura l'ambiente di sviluppo di Flutter

Per completare questo lab sono necessari due software: l'SDK Flutter e l'editor.

Puoi eseguire il codelab utilizzando uno di questi dispositivi:

  • Un dispositivo fisico Android o iOS connesso al computer e impostato sulla modalità sviluppatore.
  • Il simulatore iOS (è richiesta l'installazione degli strumenti Xcode).
  • L'emulatore Android (richiede la configurazione in Android Studio).
  • Un browser (è necessario Chrome per il debug).
  • Come applicazione desktop Windows, Linux o macOS. Devi sviluppare sulla piattaforma in cui prevedi di eseguire il deployment. Quindi, se vuoi sviluppare un'app desktop per Windows, devi sviluppare su Windows per accedere alla catena di build appropriata. Alcuni requisiti specifici del sistema operativo sono descritti in dettaglio all'indirizzo docs.flutter.dev/desktop.

3. Scarica l'app iniziale del codelab

Vuoi continuare da MDC-103?

Se hai completato MDC-103, il tuo codice dovrebbe essere pronto per questo codelab. Vai al passaggio Aggiungi il menu Sfondo.

Parti da zero?

L'app iniziale si trova nella directory material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.

…oppure clonalo da GitHub

Per clonare questo codelab da GitHub, esegui questi comandi:

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

Apri il progetto ed esegui l'app

  1. Apri il progetto nell'editor che preferisci.
  2. Segui le istruzioni "Esegui l'app" in Inizia: prova per l'editor che hai scelto.

Operazione riuscita. Sul tuo dispositivo dovresti visualizzare la pagina di accesso a Santuario dei codelab precedenti.

Android

iOS

Pagina di accesso a Shrine

Pagina di accesso al santuario

4. Aggiungere il menu Sfondo

Viene visualizzato uno sfondo dietro tutti gli altri contenuti e componenti. È formato da due livelli: uno posteriore (per visualizzare azioni e filtri) e uno anteriore (per i contenuti). Puoi utilizzare uno sfondo per visualizzare informazioni e azioni interattive, come la navigazione o i filtri dei contenuti.

Rimuovere la barra delle app Home

Il widget HomePage sarà il contenuto del nostro livello anteriore. Al momento ha una barra delle app. Sposteremo la barra dell'app sul livello Indietro e la home page includerà solo la Vista AsymmetricView.

In home.dart, modifica la funzione build() in modo che restituisca solo un AsymmetricView:

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

Aggiungere il widget Sfondo

Crea un widget chiamato Sfondo che includa i frontLayer e i backLayer.

backLayer include un menu che consente di selezionare una categoria per filtrare l'elenco (currentCategory). Poiché vogliamo che la selezione del menu resti invariata, la funzione Sfondo sarà un widget stateful.

Aggiungi un nuovo file a /lib denominato 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)

Tieni presente che contrassegniamo alcune proprietà con required. Questa è una best practice per le proprietà nel costruttore che non hanno un valore predefinito e non possono essere null e quindi non devono essere dimenticate.

Nella definizione della classe Backdrop, aggiungi la 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(),
    );
  }
}

La funzione build() restituisce uno Scaffold con una barra delle app, come accadeva per HomePage. Ma il corpo di Scaffold è uno Stack. Gli elementi secondari di uno stack possono sovrapporsi. Le dimensioni e la posizione di ogni elemento secondario sono specificate in base all'elemento principale della serie.

Ora aggiungi un'istanza Backdrop a ShrineApp.

In app.dart, importa 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';

In app.dart,, modifica il percorso / restituendo un Backdrop con HomePage come 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'),
),

Salva il progetto. Dovresti vedere la nostra home page e la barra delle app:

Android

iOS

Pagina del prodotto del santuario con sfondo rosa

Pagina di prodotto di Shrine con sfondo rosa

Il livello backlayer mostra l'area rosa in un nuovo livello dietro la home page frontlayer.

Puoi utilizzare Flutter Inspector per verificare che lo stack abbia effettivamente un contenitore dietro una home page. Dovrebbe essere simile al seguente:

92ed338a15a074bd.png

Ora puoi modificare il design e i contenuti di entrambi i livelli.

5. Aggiungere una forma

In questo passaggio darai uno stile al livello anteriore per aggiungere un taglio nell'angolo in alto a sinistra.

Con questo tipo di personalizzazione, Material Design si riferisce a una forma. Le superfici dei materiali possono avere forme arbitrarie. Le forme aggiungono enfasi e stile alle superfici e possono essere utilizzate per esprimere il branding. Le forme rettangolari ordinarie possono essere personalizzate con angoli e bordi curvi o inclinati e un numero qualsiasi di lati. Possono essere simmetriche o irregolari.

Aggiungere una forma al livello anteriore

Il logo inclinato di Santuario ha ispirato la storia della forma dell'app Santuario. Una storia di forma è l'uso comune di forme che vengono applicate in un'app. Ad esempio, la forma del logo viene riprodotta negli elementi della pagina di accesso a cui è stata applicata una forma. In questo passaggio, applicherai uno stile al livello frontale con un taglio inclinato nell'angolo in alto a sinistra.

In backdrop.dart, aggiungi una nuova 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,
          ),
        ],
      ),
    );
  }
}

Poi, nella funzione _buildStack() di _BackdropState, inserisci il livello anteriore in un _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),
      ],
    );
  }

Ricarica.

Android

iOS

Pagina del prodotto del santuario con forma personalizzata

Pagina di prodotto del santuario con forma personalizzata

Abbiamo dato alla superficie principale di Shrine una forma personalizzata. Tuttavia, vogliamo che questo sia visivamente collegato alla barra delle app.

Cambiare il colore della barra delle app

In app.dart, modifica la funzione _buildShrineTheme() nel seguente modo:

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

Riavvio a caldo. A questo punto, dovrebbe essere visualizzata la nuova barra colorata delle app.

Android

iOS

Pagina di prodotto di Shrine con barra delle app colorata

Pagina di prodotto di Shrine con barra delle app colorata

Per questo motivo, gli utenti possono vedere che c'è qualcosa appena dietro il livello bianco anteriore. Aggiungiamo un movimento in modo che gli utenti possano vedere il livello di sfondo.

6. Aggiungi movimento

Il movimento è un modo per dare vita alla tua app. Può essere grande e scenografico, discreto e minimalista o qualsiasi cosa in mezzo. Tuttavia, ricorda che il tipo di movimento che utilizzi deve essere adatto alla situazione. Il movimento applicato ad azioni regolari ripetute deve essere ridotto e sottile, in modo che non distraggano l'utente e non richiedano troppo tempo a intervalli regolari. Tuttavia, esistono situazioni più accattivanti, ad esempio la prima volta che un utente apre un'app, che possono essere più accattivanti, e alcune animazioni possono aiutare a informare l'utente su come utilizzare l'app.

Aggiungere un'animazione di rivelazione al pulsante del menu

Nella parte superiore di backdrop.dart, al di fuori dell'ambito di qualsiasi classe o funzione, aggiungi una costante per rappresentare la velocità che vogliamo che abbia la nostra animazione:

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

Aggiungi un widget AnimationController a _BackdropState, crea un'istanza nella funzione initState() e eliminalo nella funzione dispose() dello stato:

  // 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 coordina le animazioni e ti fornisce l'API per riprodurre, invertire e interrompere l'animazione. Ora abbiamo bisogno di funzioni che lo facciano muovere.

Aggiungi funzioni che determinano e modificano la visibilità del livello frontale:

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

Inserisci il backLayer in un widget ExcludeSemantics. Questo widget esclude i menu di backLayer dall'albero della semantica quando il livello di fondo non è visibile.

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

Modifica la funzione _buildStack() per utilizzare BuildContext e BoxConstraints. Inoltre, includi una transizione Posizionata che prende un'animazione 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,
          ),
        ),
      ],
    );
  }

Infine, invece di chiamare la funzione _buildStack per il corpo dello Scaffold, restituisci un widget LayoutBuilder che utilizzi _buildStack come generatore:

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

Utilizzando LayoutBuilder, abbiamo ritardato la creazione dello stack di livelli anteriore/posteriore fino al momento del layout, in modo da poter incorporare l'altezza complessiva effettiva dello sfondo. LayoutBuilder è un widget speciale il cui callback del builder fornisce vincoli di dimensione.

Nella funzione build(), trasforma l'icona del menu principale nella barra delle app in IconaPulsante e usala per attivare/disattivare la visibilità del livello frontale quando tocchi il pulsante.

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

Ricarica, quindi tocca il pulsante del menu nel simulatore.

Android

iOS

Menu del santuario vuoto con due errori

Menu Santuario vuoto con due errori

Il livello anteriore viene animato (slide) verso il basso. Se guardi verso il basso, invece, appaiono un errore rosso e un errore di overflow. Questo perché la vista AsymmetricView viene compressa e diventa più piccola a causa di questa animazione, che a sua volta lascia meno spazio alle colonne. Infine, le colonne non possono allinearsi con lo spazio specificato e generano un errore. Se sostituiamo le colonne con ListView, le dimensioni delle colonne dovrebbero rimanere invariate durante l'animazione.

Aggregare colonne di prodotti in una ListView

In supplemental/product_columns.dart, sostituisci la colonna in OneProductCardColumn con una 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,
        ),

      ],
    );
  }
}

La colonna include MainAxisAlignment.end. Per iniziare il layout dal basso, contrassegna reverse: true. L'ordine dei bambini viene invertito per compensare la modifica.

Ricarica e tocca il pulsante Menu.

Android

iOS

Menu del santuario vuoto con un errore

Menu del santuario vuoto con un errore

L'avviso di overflow grigio su OneProductCardColumn è scomparso. Ora correggiamo l'altro.

In supplemental/product_columns.dart, modifica il modo in cui viene calcolato imageAspectRatio e sostituisci la colonna in TwoProductCardColumn con un 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,
            ),
          ),
        ],
      );

Inoltre, abbiamo aggiunto un po' di sicurezza a imageAspectRatio.

Ricarica. Quindi tocca il pulsante Menu.

Android

iOS

Menu del santuario vuoto

Menu del santuario vuoto

Niente più overflow.

7. Aggiungere un menu nel livello posteriore

Un menu è un elenco di elementi di testo toccabili che avvisano gli ascoltatori quando vengono toccati. In questo passaggio aggiungerai un menu di filtri per le categorie.

Aggiungere il menu

Aggiungi il menu al livello anteriore e i pulsanti interattivi al livello posteriore.

Crea un nuovo file denominato 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()),
      ),
    );
  }
}

Si tratta di un GestureDetector che racchiude una colonna i cui elementi secondari sono i nomi delle categorie. Viene utilizzata una sottolineatura per indicare la categoria selezionata.

In app.dart, converti il widget SantuarioApp da stateless a stateful.

  1. Evidenzia ShrineApp.
  2. In base al tuo IDE, mostra le azioni del codice:
  3. Android Studio: premi ⌥Invio (macOS) o Alt + Invio
  4. VS Code: premi ⌘. (macOS) o Ctrl+.
  5. Seleziona "Convert to StatefulWidget".
  6. Modifica la classe ShrineAppState in privata (_ShrineAppState). Fai clic con il tasto destro del mouse su SantuarioAppState.
  7. Android Studio: seleziona Rielabora > Rinomina
  8. VS Code: seleziona Rinomina simbolo
  9. Inserisci _ShrineAppState per rendere privato il corso.

In app.dart, aggiungi una variabile a _ShrineAppState per la categoria selezionata e un callback quando viene toccata:

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

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

Poi, modifica il livello di fondo in una pagina CategoryMenuPage.

In app.dart, importa la pagina 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';

Nella funzione build(), modifica il campo backLayer in CategoryMenuPage e il campo currentCategory in modo che accetti la variabile di istanza.

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

Ricarica e tocca il pulsante Menu.

Android

iOS

Menu del santuario con 4 categorie

Menu del santuario con 4 categorie

Se tocchi un'opzione di menu, non succede nulla… per il momento. Risolviamo il problema.

In home.dart, aggiungi una variabile per Categoria e passala ad 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),
    );
  }
}

Nel campo app.dart, passa il valore _currentCategory per frontLayer:.

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

Ricarica. Tocca il pulsante del menu nel simulatore e seleziona una categoria.

Android

iOS

Pagina del prodotto filtrata da Santuario

Pagina del prodotto filtrata da Santuario

Sono filtrati

Chiudi il livello anteriore dopo una selezione nel menu

In backdrop.dart, aggiungi un'override per la funzione didUpdateWidget() (chiamata ogni volta che la configurazione del widget cambia) in _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);
    }
  }

Salva il progetto per attivare un ricaricamento a caldo. Tocca l'icona del menu e seleziona una categoria. Il menu dovrebbe chiudersi automaticamente e dovresti vedere la categoria di elementi selezionata. Ora aggiungerai questa funzionalità anche al livello frontale.

Attiva/disattiva il livello anteriore

In backdrop.dart, aggiungi un callback on-tap al livello di sfondo:

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;

Quindi, aggiungi un egDetector al gruppo secondario di _Frontlayer: gli elementi secondari della colonna.

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

Poi implementa la nuova proprietà onTap su _BackdropState nella funzione _buildStack():

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

Ricarica e tocca la parte superiore del livello anteriore. Il livello dovrebbe aprirsi e chiudersi ogni volta che tocchi la parte superiore del livello anteriore.

8. Aggiungere un'icona con il brand

L'iconografia del brand si estende anche alle icone familiari. Creiamo un'icona di visualizzazione personalizzata e uniamola al titolo per creare un look unico e personalizzato.

Cambiare l'icona del pulsante del menu

Android

iOS

Pagina di prodotto del santuario con icona del brand

Pagina di prodotto del santuario con icona del brand

In backdrop.dart, crea una nuova 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.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 è un widget personalizzato che sostituirà il semplice widget Text per il parametro title del widget AppBar. Presenta un'icona di menu animata e transizioni animate tra i titoli del fronte e del retro. L'icona del menu animata utilizzerà un nuovo asset. Il riferimento al nuovo slanted_menu.png deve essere aggiunto al pubspec.yaml.

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

Rimuovi la proprietà leading nello strumento per la creazione di AppBar. La rimozione è necessaria per il rendering dell'icona personalizzata del brand al posto del widget leading originale. L'animazione listenable e il gestore onPress dell'icona del brand vengono passati a _BackdropTitle. Anche frontTitle e backTitle vengono passati in modo che possano essere visualizzati all'interno del titolo dello sfondo. Il parametro title di AppBar dovrebbe avere il seguente aspetto:

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

L'icona del brand viene creata in _BackdropTitle.. Contiene un Stack di icone animate: un menu inclinato e un diamante, racchiuso in un IconButton in modo che possa essere premuto. L'elemento IconButton viene quindi aggregato in un SizedBox per fare spazio al movimento dell'icona orizzontale.

L'architettura "tutto è un widget" di Flutter consente di modificare il layout del AppBar predefinito senza dover creare un widget AppBar personalizzato completamente nuovo. Il parametro title, che in origine è un widget Text, può essere sostituito con un _BackdropTitle più complesso. Poiché _BackdropTitle include anche l'icona personalizzata, prende il posto della proprietà leading, che ora può essere omessa. Questa semplice sostituzione del widget viene eseguita senza modificare gli altri parametri, come le icone delle azioni, che continuano a funzionare autonomamente.

Aggiungi una scorciatoia per tornare alla schermata di accesso

In backdrop.dart,aggiungi una scorciatoia per tornare alla schermata di accesso dalle due icone finali nella barra delle app: modifica le etichette semantiche delle icone in base alla loro nuova finalità.

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

Se tenti di ricaricare, riceverai un messaggio di errore. Importa login.dart per correggere l'errore:

import 'login.dart';

Ricarica l'app e tocca i pulsanti per la ricerca o per regolare per tornare alla schermata di accesso.

9. Complimenti!

Nel corso di questi quattro codelab, hai imparato a utilizzare i componenti materiali per creare esperienze utente uniche ed eleganti che esprimono la personalità e lo stile del brand.

Passaggi successivi

Questo codelab, MDC-104, completa questa sequenza di codelab. Puoi esplorare ancora più componenti in Material Flutter visitando il catalogo di widget Material Components.

Per un obiettivo aggiuntivo, prova a sostituire l'icona con brand con un elemento AnimatedIcon che si anima tra due icone quando lo sfondo viene reso visibile.

Ci sono tanti altri codelab di Flutter da provare, in base ai tuoi interessi. Abbiamo un altro codelab specifico su Material che potrebbe interessarti: Creare transizioni meravigliose con Material Motion per Flutter.

Ho completato questo codelab con una quantità di tempo e di sforzi ragionevoli

Molto d'accordo D'accordo Né d'accordo né in disaccordo In disaccordo Molto in disaccordo

Vorrei continuare a utilizzare Material Components in futuro

Totalmente d'accordo D'accordo Neutrale In disaccordo Totalmente in disaccordo