MDC-104 Flutter: componenti Material Advanced

1. Introduzione

logo_components_color_2x_web_96dp.png

Material Components (MDC) consente agli sviluppatori di implementare Material Design. Creato dal team di ingegneri e designer UX di Google, MDC è dotato di decine di componenti UI 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 non è solo un modo per eseguire un'azione da parte dell'utente, ma è anche un'espressione visiva della forma, delle dimensioni e del colore che consente all'utente di sapere che è interattivo e che qualcosa succederà toccando o facendo 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 categorie selezionabili utilizzate per filtrare i prodotti mostrati nella griglia asimmetrica. In questo codelab, utilizzerai:

  • 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 a scorrimento orizzontale 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

menu con 4 categorie

Componenti e sottosistemi di Material Flutter in questo codelab

  • Shape

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

Principiante Livello intermedio Eccellente

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 (richiede l'installazione degli strumenti Xcode).
  • L'emulatore Android (richiede la configurazione in Android Studio).
  • Un browser (per il debug è richiesto Chrome).
  • Come applicazione desktop Windows, Linux o macOS. Devi svilupparle 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.

Vuoi iniziare da zero?

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

...o clonarlo 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 per "Esegui l'app". in Inizia: prova 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 al santuario

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 iniziale. 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 semplicemente un'immagine 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.

L'backLayer include un menu che consente di selezionare una categoria per filtrare l'elenco (currentCategory). Poiché vogliamo che la selezione del menu rimanga invariata, trasformeremo la funzionalità Sfondo in 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)

Alcune proprietà vengono contrassegnate come required. Questa è una best practice per le proprietà nel costruttore che non hanno un valore predefinito e non possono essere null, per cui 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, proprio come faceva la Home page. Ma il corpo di Scaffold è uno Stack. Gli elementi secondari di uno stack possono sovrapporsi. La dimensione e la posizione di ogni elemento secondario sono specificate rispetto all'elemento principale dell'elenco filtri.

Ora aggiungi un'istanza di Backdrop a SantuarioApp.

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 che ha 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 di prodotto del santuario con sfondo rosa

Pagina di prodotto del santuario 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 a questo:

92ed338a15a074bd.png

Ora puoi regolare la posizione di entrambi i livelli design e contenuti.

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 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 forma è l'uso comune di forme applicate all'interno di un'app. Ad esempio, la forma del logo viene riprodotta negli elementi della pagina di accesso a cui è 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 un nuovo corso _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,
          ),
        ],
      ),
    );
  }
}

Quindi, nella funzione _buildStack() di _BackdropState, aggrega il livello frontale 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 del prodotto del santuario con forma personalizzata

Abbiamo assegnato alla superficie principale del santuario 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() come segue:

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 del prodotto del santuario con barra delle app colorata

Pagina del prodotto del santuario con barra delle app colorata

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

6. Aggiungi movimento

Il movimento è un modo per dare vita alla tua app. I tuoi video possono essere grandi e drammatici, discreti e minimalisti, o qualsiasi altra via di mezzo. Tuttavia, ricorda che il tipo di movimento utilizzato 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 che, ad esempio, possono essere più accattivanti, ad esempio la prima volta che un utente apre un'app, e alcune animazioni possono aiutare a istruire l'utente su come utilizzare l'app.

Aggiungi il movimento 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à desiderata per l'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);
  }

Aggrega il backlayer in un widget EscludiSemantics. Questo widget escluderà le voci di menu di backlayer dalla struttura semantica quando il livello back 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. Includi anche una transizione 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 IconButton e utilizzalo 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 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, vedrai 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 sostituisci le colonne con ListView, le dimensioni delle colonne dovrebbero rimanere invariate.

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

Ricarica e tocca il pulsante Menu.

Android

iOS

Menu Santuario vuoto con un errore

Menu 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 Santuario vuoto

Menu Santuario vuoto

Niente più overflow.

7. Aggiungere un menu nel livello posteriore

Un menu è un elenco di elementi di testo toccabili che avvisa gli ascoltatori quando gli elementi di testo vengono toccati. In questo passaggio, aggiungerai un menu di filtro per categoria.

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 aggrega 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 codice: premi ⌘. (macOS) o Ctrl+.
  5. Seleziona "Convert to StatefulWidget".
  6. Imposta la classe SantuarioAppState come privata (_ShrineAppState). Fai clic con il tasto destro del mouse su SantuarioAppState.
  7. Android Studio: seleziona Refactoring > Rinomina
  8. VS Code: seleziona Rinomina simbolo
  9. Inserisci _ShrineAppState per rendere il corso privato.

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

Quindi modifica il livello indietro in una Pagina del menu di categoria.

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(), cambia il campo backlayer in CategoryMenuPage e il campo currentCategory per assumere la variabile 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 niente...per ora. Risolviamo il problema.

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

In app.dart, supera _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 di prodotto filtrata da Santuario

Pagina di 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.

Attivare/disattivare il livello anteriore

In backdrop.dart, aggiungi un callback al tocco 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 in _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 frontale.

8. Aggiungi un'icona con brand

L'iconografia con brand si estende anche alle icone già note. Creiamo un'icona per la rivelazione personalizzata e la uniamo al titolo per ottenere un look brandizzato e unico.

Cambiare l'icona del pulsante del menu

Android

iOS

Pagina di prodotto del santuario con icona con brand

Pagina di prodotto del santuario con icona con brand

In backdrop.dart, crea un nuovo corso _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 animato utilizzerà una nuova risorsa. 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 consentire il rendering dell'icona con brand personalizzata nella posizione originale del widget leading. L'animazione listenable e il gestore onPress per l'icona con brand vengono trasmessi 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 con brand viene creata in _BackdropTitle.. Contiene una serie di icone animate Stack: un menu obliquo e un rombo, che è racchiuso in una IconButton per poter essere premuto. L'elemento IconButton viene quindi aggregato in un SizedBox per fare spazio al movimento dell'icona orizzontale.

"Tutto è un widget" di Flutter consente di modificare il layout dell'elemento 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 alla schermata di accesso dalle due icone finali nella barra delle app: modifica le etichette semantiche delle icone per riflettere il nuovo scopo.

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

Il codelab MDC-104 completa questa sequenza di codelab. Puoi esplorare altri componenti di Material Flutter visitando il catalogo dei widget di 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

Totalmente d'accordo D'accordo Né chiara, né confusa In disaccordo Totalmente in disaccordo

Vorrei continuare a utilizzare Material Components in futuro

Totalmente d'accordo D'accordo Né chiara, né confusa In disaccordo Totalmente in disaccordo