MDC-104 Flutter: Material Advanced Components

1. Einführung

logo_components_color_2x_web_96dp.png

Material Components (MDC) unterstützen Entwickler bei der Implementierung von Material Design. MDC wurde von einem Team aus Entwicklern und UX-Designern bei Google entwickelt. Es enthält Dutzende schöne und funktionale UI-Komponenten und ist für Android, iOS, das Web und Flutter.material.io/develop verfügbar.

Im Codelab MDC-103 haben Sie die Farbe, Höhe, Typografie und Form von Material Components (MDC) angepasst, um Ihre App zu gestalten.

Eine Komponente im Material-Design-System führt eine Reihe vordefinierter Aufgaben aus und hat bestimmte Eigenschaften, z. B. eine Schaltfläche. Allerdings ist eine Schaltfläche mehr als nur eine Möglichkeit für Nutzende, eine Aktion auszuführen. Sie ist auch ein visueller Ausdruck von Form, Größe und Farbe, der die Nutzenden zeigt, dass sie interaktiv ist und dass bei Berührung oder Klicken etwas passiert.

In den Material Design-Richtlinien werden Komponenten aus der Sicht von Designschaffenden beschrieben. Sie beschreiben eine breite Palette grundlegender Funktionen, die auf verschiedenen Plattformen verfügbar sind, sowie die anatomischen Elemente, aus denen jede Komponente besteht. Ein Hintergrund enthält beispielsweise eine Hintergrundebene und deren Inhalt, die vordere Ebene und ihren Inhalt, Bewegungsregeln und Anzeigeoptionen. Jede dieser Komponenten lässt sich an die Anforderungen, Anwendungsfälle und Inhalte der jeweiligen App anpassen.

Inhalt

In diesem Codelab ändern Sie die Benutzeroberfläche in der Shrine-App in eine Präsentation mit zwei Ebenen, die als „Hintergrund“ bezeichnet wird. Im Hintergrund befindet sich ein Menü mit auswählbaren Kategorien, mit denen die in dem asymmetrischen Raster angezeigten Produkte gefiltert werden können. In diesem Codelab verwenden Sie Folgendes:

  • Form
  • Bewegung
  • Flutter-Widgets, die Sie in den vorherigen Codelabs verwendet haben

Android

iOS

E-Commerce-App in Rosa und Braun mit einer oberen App-Leiste und einem asymmetrischen, horizontal scrollbaren Raster voller Produkte

E-Commerce-App in Rosa und Braun mit einer oberen App-Leiste und einem asymmetrischen, horizontal scrollbaren Raster voller Produkte

Speisekarte mit 4 Kategorien

Speisekarte mit 4 Kategorien

Codelab: Material Flutter-Komponenten und -Subsysteme

  • Form

Wie würden Sie Ihre Erfahrung mit der Flutter-Entwicklung bewerten?

<ph type="x-smartling-placeholder"></ph> Neuling Mittel Kompetent

2. Flutter-Entwicklungsumgebung einrichten

Für dieses Lab benötigen Sie zwei Softwareprogramme: das Flutter SDK und einen Editor.

Sie können das Codelab auf jedem dieser Geräte ausführen:

  • Ein physisches Android- oder iOS, das mit Ihrem Computer verbunden ist und sich im Entwicklermodus befindet.
  • Den iOS-Simulator (erfordert die Installation von Xcode-Tools).
  • Android-Emulator (Einrichtung in Android Studio erforderlich)
  • Ein Browser (zur Fehlerbehebung wird Chrome benötigt)
  • Als Windows-, Linux- oder macOS-Desktopanwendung Die Entwicklung muss auf der Plattform erfolgen, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktop-App entwickeln möchten, müssen Sie die Entwicklung unter Windows ausführen, damit Sie auf die entsprechende Build-Kette zugreifen können. Es gibt betriebssystemspezifische Anforderungen, die unter docs.flutter.dev/desktop ausführlich beschrieben werden.

3. Codelab-Starter-App herunterladen

Weiter mit MDC-103?

Wenn Sie MDC-103 abgeschlossen haben, sollte Ihr Code für dieses Codelab bereit sein. Fahren Sie mit dem Schritt Bilderrahmenmenü hinzufügen fort.

Neu beginnen?

Die Start-App befindet sich im Verzeichnis material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.

...oder von GitHub klonen

Führen Sie die folgenden Befehle aus, um dieses Codelab von GitHub zu klonen:

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

Projekt öffnen und App ausführen

  1. Öffnen Sie das Projekt in einem Editor Ihrer Wahl.
  2. Folgen Sie der Anleitung zum Ausführen der App. unter Erste Schritte: Testlauf für den ausgewählten Editor.

Fertig! Auf deinem Gerät sollte nun die Anmeldeseite von Shrine aus den vorherigen Codelabs angezeigt werden.

Android

iOS

Anmeldeseite für Schrein

Anmeldeseite für Schrein

4. Bilderrahmenmenü hinzufügen

Hinter allen anderen Inhalten und Komponenten wird ein Hintergrund angezeigt. Es besteht aus zwei Ebenen: einer hinteren Ebene, die Aktionen und Filter anzeigt, und einer vorderen Ebene, die Inhalte anzeigt. Sie können einen Hintergrund verwenden, um interaktive Informationen und Aktionen wie Navigation oder Inhaltsfilter anzuzeigen.

Leiste der Start-App entfernen

Das Startseiten-Widget ist der Inhalt des Front-Layers. Aktuell gibt es eine App-Leiste. Die App-Leiste wird in den Back-Layer verschoben und die Startseite enthält nur AsymmetricView.

Ändern Sie in home.dart die Funktion build() so, dass nur AsymmetricView zurückgegeben wird:

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

Bilderrahmen-Widget hinzufügen

Erstellen Sie ein Widget namens Bilderrahmen, das frontLayer und backLayer enthält.

Die backLayer enthält ein Menü, in dem du eine Kategorie zum Filtern der Liste auswählen kannst (currentCategory). Da die Menüauswahl bestehen bleiben soll, machen wir den Bilderrahmen zu einem zustandsorientierten Widget.

Fügen Sie /lib eine neue Datei mit dem Namen backdrop.dart hinzu:

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)

Beachten Sie, dass wir bestimmte Unterkünfte als required kennzeichnen. Dies ist eine Best Practice für Attribute im Konstruktor, die keinen Standardwert haben und nicht null sein können und daher nicht vergessen werden sollten.

Fügen Sie unter der Backdrop-Klassendefinition die _BackdropState-Klasse hinzu:

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

Die Funktion build() gibt ein Scaffold mit einer App-Leiste zurück, wie dies bei der Startseite der Fall war. Der Körper von Scaffold ist jedoch ein Stack. Die untergeordneten Elemente eines Stacks können sich überschneiden. Größe und Position jedes untergeordneten Elements werden relativ zum übergeordneten Stack-Element angegeben.

Fügen Sie ShrineApp jetzt eine Bilderrahmen-Instanz hinzu.

Importieren Sie in app.dart backdrop.dart und 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';

Ändern Sie in app.dart, die Route /, indem Sie eine Backdrop zurückgeben, deren frontLayer HomePage ist:

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

Wenn du dein Projekt speicherst, sollten unsere Startseite und die App-Leiste angezeigt werden:

Android

iOS

Shrine-Produktseite mit rosafarbenem Hintergrund

Shrine-Produktseite mit rosafarbenem Hintergrund

BackLayer zeigt den rosafarbenen Bereich in einem neuen Layer hinter der FrontLayer-Startseite an.

Mit dem Flutter Inspector können Sie prüfen, ob sich hinter einer Startseite tatsächlich ein Container im Stack befindet. Sie sollte in etwa so aussehen:

92ed338a15a074bd.png

Sie können jetzt beide Ebenen anpassen Design und Inhalt.

5. Form hinzufügen

In diesem Schritt gestalten Sie die vordere Ebene, sodass in der oberen linken Ecke ein Schnitt hinzugefügt wird.

Material Design bezeichnet diese Art der Anpassung als Form. Materialoberflächen können beliebige Formen haben. Formen verleihen Oberflächen Betonung und Stil und können verwendet werden, um Branding zum Ausdruck zu bringen. Gewöhnliche rechteckige Formen können mit gekrümmten oder abgeschrägten Ecken und Kanten sowie mit einer beliebigen Anzahl von Seiten angepasst werden. Sie können symmetrisch oder unregelmäßig sein.

Form zur vorderen Ebene hinzufügen

Das schräg stehende Shrine-Logo diente als Inspiration für die Geschichte der Shrine App. Eine Shape-Story ist die übliche Verwendung von Formen, die in einer gesamten App angewendet werden. Die Logoform wird beispielsweise in den Elementen auf der Anmeldeseite übernommen, auf die die Form angewendet wurde. In diesem Schritt gestalten Sie die vordere Ebene mit einem angewinkelten Schnitt in der oberen linken Ecke.

Fügen Sie in backdrop.dart eine neue Klasse _FrontLayer hinzu:

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

Verpacken Sie dann in der Funktion _buildStack() von "_BackdropState" die Frontebene mit einem "_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),
      ],
    );
  }

Aktualisieren.

Android

iOS

Produktseite des Schreins mit benutzerdefiniertem Format

Produktseite des Schreins mit benutzerdefiniertem Format

Wir haben der primären Oberfläche des Schreins eine benutzerdefinierte Form gegeben. Es soll jedoch eine visuelle Verbindung zur App-Leiste herstellen.

Farbe der App-Leiste ändern

Ändern Sie in app.dart die Funktion _buildShrineTheme() so:

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

Neustart. Die neue farbige App-Leiste sollte jetzt angezeigt werden.

Android

iOS

Shrine-Produktseite mit farbiger App-Leiste

Shrine-Produktseite mit farbiger App-Leiste

Durch diese Änderung können die Nutzenden sehen, dass sich direkt hinter der vorderen weißen Schicht etwas befindet. Fügen wir nun Bewegungen hinzu, damit die Nutzenden die Rückseite des Hintergrunds sehen können.

6. Bewegung hinzufügen

Bewegung ist eine Möglichkeit, Ihre App lebendiger zu gestalten. Sie kann groß und dramatisch, subtil und minimal sein oder irgendwo dazwischen sein. Denk aber daran, dass die Art der Bewegung, die du verwendest, zur jeweiligen Situation passen sollte. Bewegungen, die auf wiederholte, regelmäßige Aktionen angewendet werden, sollten klein und subtil sein, damit die Aktionen die Nutzenden nicht ablenken oder regelmäßig zu viel Zeit in Anspruch nehmen. Es gibt jedoch geeignete Situationen, wie das erste Öffnen einer App, die auffälliger sein können. Einige Animationen können die Nutzer über die Verwendung Ihrer App informieren.

Bewegung zum Anzeigen der Menüschaltfläche hinzufügen

Fügen Sie oben in backdrop.dart außerhalb des Bereichs einer Klasse oder Funktion eine Konstante hinzu, um die Geschwindigkeit darzustellen, die unsere Animation haben soll:

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

Fügen Sie ein AnimationController-Widget zu „_BackdropState“ hinzu, instanziieren Sie es in der Funktion initState() und entsorgen Sie es in der Funktion dispose() des Status:

  // 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 koordiniert Animationen und stellt Ihnen eine API zum Abspielen, Umkehren und Stoppen der Animation zur Verfügung. Jetzt brauchen wir Funktionen, die es verschieben.

Fügen Sie Funktionen hinzu, die die Sichtbarkeit des Front-Layers bestimmen und ändern:

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

Umschließen Sie das BackLayer mit einem AusschließenSemantics-Widget. Dieses Widget schließt die Menüelemente des BackLayers aus der Semantikstruktur aus, wenn die Back-Layer nicht sichtbar ist.

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

Ändern Sie die Funktion _buildStack() so, dass ein BuildContext und BoxConstraints verwendet werden. Fügen Sie außerdem einen „PositionedTransition“ ein, der eine RelativeRectTween-Animation annimmt:

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

Anstatt schließlich die Funktion "_buildStack" für den Textkörper des Scaffold aufzurufen, geben Sie ein LayoutBuilder-Widget zurück, das "_buildStack" als Builder verwendet:

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

Wir haben den Build des Front-/Back-Stapels mithilfe von LayoutBuilder bis zum Layout verzögert, damit wir die tatsächliche Gesamthöhe des Hintergrunds berücksichtigen können. LayoutBuilder ist ein spezielles Widget, dessen Builder-Callback Größenbeschränkungen bereitstellt.

Wandeln Sie in der Funktion build() das vorangestellte Menüsymbol in der App-Leiste in eine IconButton um, um die Sichtbarkeit der vorderen Ebene beim Antippen der Schaltfläche zu ändern.

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

Aktualisieren Sie die Seite und tippen Sie dann im Simulator auf die Menüschaltfläche.

Android

iOS

Leeres Shrine-Menü mit zwei Fehlern

Leeres Shrine-Menü mit zwei Fehlern

Die vordere Ebene wird animiert (Folien) nach unten. Aber wenn Sie nach unten schauen, sehen Sie einen roten und einen Überlauffehler. Dies liegt daran, dass AsymmetricView durch diese Animation zusammengedrückt und kleiner wird, wodurch die Spalten wiederum weniger Platz haben. Schließlich können die Spalten nicht mehr mit dem gegebenen Platz angeordnet werden, und sie führen zu einem Fehler. Wenn wir die Spalten durch Listenansichten ersetzen, sollte die Spaltengröße bei der Animation beibehalten werden.

Produktspalten in einer Listenansicht zusammenfassen

Ersetzen Sie in supplemental/product_columns.dart die Spalte in OneProductCardColumn durch eine Listenansicht:

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

      ],
    );
  }
}

Die Spalte enthält MainAxisAlignment.end. Wenn Sie mit dem Layout von unten beginnen möchten, markieren Sie reverse: true. Die Reihenfolge der untergeordneten Elemente wird umgekehrt, um die Änderung auszugleichen.

Aktualisieren Sie die Seite und tippen Sie auf die Menüschaltfläche.

Android

iOS

Leeres Schrein-Menü mit einem Fehler

Leeres Schrein-Menü mit einem Fehler

Die graue Überlauf-Warnung auf OneProductCardColumn ist verschwunden. Beheben wir nun das andere Problem.

Ändern Sie in supplemental/product_columns.dart die Berechnungsmethode für imageAspectRatio und ersetzen Sie die Spalte in TwoProductCardColumn durch eine 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,
            ),
          ),
        ],
      );

Außerdem haben wir die Sicherheitsfunktionen von „imageAspectRatio“ erhöht.

Aktualisieren. Tippe dann auf die Menüschaltfläche.

Android

iOS

Leeres Schrein-Menü

Leeres Schrein-Menü

Kein Überlauf mehr.

7. Menü auf der hinteren Ebene hinzufügen

Ein Menü ist eine Liste von antippbaren Textelementen, die die Zuhörer benachrichtigen, wenn die Textelemente angetippt werden. In diesem Schritt fügen Sie ein Menü zum Filtern von Kategorien hinzu.

Speisekarte hinzufügen

Fügen Sie das Menü zum vorderen Layer und die interaktiven Schaltflächen zum hinteren Layer hinzu.

Erstellen Sie eine neue Datei mit dem Namen 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()),
      ),
    );
  }
}

Es handelt sich um einen GestureDetector, der eine Spalte umschließt, deren untergeordnete Elemente die Kategorienamen sind. Die ausgewählte Kategorie wird durch eine Unterstreichung gekennzeichnet.

Konvertieren Sie in app.dart das ShrineApp-Widget von zustandslos in zustandsorientiert.

  1. ShrineApp. markieren
  2. Zeigen Sie je nach IDE Codeaktionen an:
  3. Android Studio: Drücken Sie ⌥ Eingabetaste (macOS) oder Alt + Eingabetaste.
  4. VS Code: Drücken Sie ⌘. (macOS) oder Strg+.
  5. Wählen Sie „Convert to StatefulWidget“ (In StatefulWidget konvertieren).
  6. Ändern Sie die ShrineAppState-Klasse in privat (_ShrineAppState). Klicken Sie mit der rechten Maustaste auf ShrineAppState und
  7. Android Studio: Wählen Sie „Refaktorieren“ > „Refaktorieren“ aus. Umbenennen
  8. VS Code: Wählen Sie das Symbol „Umbenennen“ aus.
  9. Geben Sie _ShrineAppState ein, um den Kurs als privat zu markieren.

Füge in app.dart eine Variable zu „_ShrineAppState“ für die ausgewählte Kategorie und einen Callback hinzu, wenn du darauf tippst:

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

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

Ändern Sie dann die Back-Layer in eine CategoryMenuPage.

Importieren Sie in app.dart die 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';

Ändern Sie in der Funktion build() das Feld „backLayer“ zu „CategoryMenuPage“ und das Feld „currentCategory“, um die Instanzvariable zu verwenden.

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

Aktualisieren Sie die Seite und tippen Sie auf die Menüschaltfläche.

Android

iOS

Schrein-Menü mit 4 Kategorien

Schrein-Menü mit 4 Kategorien

Wenn du auf eine Menüoption tippst, passiert nichts – noch nicht. Das sollte nicht sein.

Fügen Sie in home.dart eine Variable für „Kategorie“ hinzu und übergeben Sie sie an 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),
    );
  }
}

Übergeben Sie in app.dart die _currentCategory für frontLayer:

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

Aktualisieren. Tippen Sie im Simulator auf die Menüschaltfläche und wählen Sie eine Kategorie aus.

Android

iOS

Nach Schrein gefilterte Produktseite

Nach Schrein gefilterte Produktseite

Sie wurden gefiltert.

Frontebene nach einer Menüauswahl schließen

Fügen Sie in backdrop.dart eine Überschreibung für die Funktion didUpdateWidget() hinzu, die immer dann aufgerufen wird, wenn sich die Widget-Konfiguration ändert, 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);
    }
  }

Speichern Sie Ihr Projekt, um ein Hot Refresh auszulösen. Tippe auf das Menüsymbol und wähle eine Kategorie aus. Das Menü sollte automatisch geschlossen werden und Sie sollten die ausgewählte Elementkategorie sehen. Jetzt fügen Sie diese Funktion auch dem vorderen Layer hinzu.

Frontschicht ein-/ausblenden

Fügen Sie in backdrop.dart der Hintergrundebene einen Callback durch Tippen hinzu:

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;

Fügen Sie dann dem untergeordneten Element von _FrontLayer, den untergeordneten Spalten der Spalte, einen GesteDetector hinzu.

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

Implementieren Sie dann das neue Attribut onTap für _BackdropState in der Funktion _buildStack():

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

Aktualisiere die Frontebene und tippe auf den oberen Bereich. Die Ebene sollte sich jedes Mal öffnen und schließen, wenn Sie auf den oberen Bereich der vorderen Ebene tippen.

8. Markensymbol hinzufügen

Markensymbole erstrecken sich auch auf bekannte Symbole. Gestalte das Symbol zum Veröffentlichen als benutzerdefiniert und verknüpfe es mit unserem Titel, um einen einzigartigen, markenspezifischen Look zu erzeugen.

Symbol für die Menüschaltfläche ändern

Android

iOS

Shrine-Produktseite mit Markensymbol

Shrine-Produktseite mit Markensymbol

Erstellen Sie in backdrop.dart eine neue Klasse: _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 ist ein benutzerdefiniertes Widget, das das einfache Text-Widget für den Parameter title des AppBar-Widgets ersetzt. Es hat ein animiertes Menüsymbol und animierte Übergänge zwischen dem Titel und dem Hintergrund. Für das animierte Menüsymbol wird ein neues Asset verwendet. Der Verweis auf das neue slanted_menu.png muss dem pubspec.yaml hinzugefügt werden.

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

Entfernen Sie das Attribut leading im AppBar-Builder. Das benutzerdefinierte Markensymbol muss entfernt werden, damit es an der Stelle des ursprünglichen leading-Widgets gerendert wird. Die Animation listenable und der onPress-Handler für das Markensymbol werden an _BackdropTitle übergeben. frontTitle und backTitle werden ebenfalls übergeben, damit sie im Hintergrundtitel gerendert werden können. Der Parameter title von AppBar sollte so aussehen:

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

Das Markensymbol wird in der _BackdropTitle. erstellt. Es enthält ein Stack animierter Symbole: ein abgeschrägtes Menü und eine Raute, die von einem IconButton umschlossen ist, damit es gedrückt werden kann. Das IconButton wird dann von einem SizedBox umschlossen, um Platz für die horizontale Symbolbewegung zu schaffen.

„Alles ist ein Widget“ von Flutter Architektur ermöglicht das Ändern des Layouts des Standard-AppBar, ohne ein völlig neues benutzerdefiniertes AppBar-Widget erstellen zu müssen. Der Parameter title, ursprünglich ein Text-Widget, kann durch eine komplexere _BackdropTitle ersetzt werden. Da _BackdropTitle auch das benutzerdefinierte Symbol enthält, ersetzt es die Eigenschaft leading, die jetzt weggelassen werden kann. Diese einfache Widget-Ersetzung wird erreicht, ohne dass andere Parameter wie z. B. Aktionssymbole geändert werden, die weiterhin von allein funktionieren.

Verknüpfung zum Anmeldebildschirm hinzufügen

Fügen Sie in backdrop.dart, von den beiden nachgestellten Symbolen in der App-Leiste eine Verknüpfung zum Anmeldebildschirm hinzu: Ändern Sie die semantischen Labels der Symbole entsprechend.

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

Wenn Sie versuchen, die Seite neu zu laden, erhalten Sie eine Fehlermeldung. Importieren Sie login.dart, um den Fehler zu beheben:

import 'login.dart';

Aktualisiere die App und tippe auf die Schaltflächen für die Suche oder die Feinabstimmung, um zum Anmeldebildschirm zurückzukehren.

9. Glückwunsch!

Im Laufe dieser vier Codelabs haben Sie gelernt, wie Sie mithilfe von Material Components ein einzigartiges, elegantes Nutzungserlebnis schaffen, das Ihre Markenpersönlichkeit und Ihren Markenstil zum Ausdruck bringt.

Weiteres Vorgehen

Dieses Codelab, MDC-104, vervollständigt diese Abfolge von Codelabs. Im Katalog der Materialkomponenten-Widgets finden Sie noch mehr Komponenten in Material Flutter.

Wenn Sie das Ziel erweitern möchten, können Sie das Markensymbol durch ein AnimatedIcon-Element ersetzen, das zwischen zwei Symbolen animiert wird, wenn der Hintergrund sichtbar wird.

Es gibt viele weitere Flutter-Codelabs, die Sie ausprobieren können, die auf Ihre Interessen abgestimmt sind. Wir haben ein weiteres Material-spezifisches Codelab für Sie: Building Beautiful Transitions with Material Motion for Flutter.

Ich konnte dieses Codelab mit angemessenem Zeit- und Arbeitsaufwand abschließen

<ph type="x-smartling-placeholder"></ph> Stimme vollkommen zu Stimme zu Weder zufrieden noch unzufrieden Stimme nicht zu Stimme überhaupt nicht zu

Ich möchte Material Components weiterhin verwenden.

<ph type="x-smartling-placeholder"></ph> Stimme vollkommen zu Stimme zu Weder zufrieden noch unzufrieden Stimme nicht zu Stimme überhaupt nicht zu