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 von Entwicklern und UX-Designern bei Google entwickelt und bietet Dutzende ansprechender und funktionaler UI-Komponenten. Es ist für Android, iOS, Web und Flutter verfügbar.material.io/develop

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

Eine Komponente im Material Design-System führt eine Reihe von vordefinierten 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 kann an die Anforderungen, Anwendungsfälle und Inhalte der jeweiligen App angepasst werden.

Inhalt

In diesem Codelab ändern Sie die Benutzeroberfläche der Shrine-App in eine zweistufige Präsentation, einen sogenannten „Hintergrund“. 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

Speisekarteneintrag mit 4 Kategorien

Speisekarteneintrag mit 4 Kategorien

Codelab: Material Flutter-Komponenten und -Subsysteme

  • Form

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

Anfänger Mittelstufe Fortgeschritten

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 einem der folgenden Geräte ausführen:

  • Ein physisches Android- oder iOS-Gerät, das mit Ihrem Computer verbunden und auf den Entwicklermodus gesetzt ist.
  • Der iOS-Simulator (erfordert die Installation von Xcode-Tools).
  • Android-Emulator (erfordert Einrichtung in Android Studio)
  • Einen Browser (für die Fehlerbehebung ist Chrome erforderlich)
  • Als Windows-, Linux- oder macOS-Desktopanwendung Die Entwicklung muss auf der Plattform erfolgen, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktopanwendung entwickeln möchten, müssen Sie die Entwicklung unter Windows durchführen, um auf die entsprechende Build-Kette zugreifen zu können. Es gibt betriebssystemspezifische Anforderungen, die unter docs.flutter.dev/desktop ausführlich beschrieben werden.

3. Codelab-Starter-App herunterladen

Fortsetzung von MDC-103?

Wenn Sie MDC-103 abgeschlossen haben, sollte Ihr Code für dieses Codelab bereit sein. Überspringen Sie diesen Schritt und springen Sie zu Hintergrundmenü hinzufügen.

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 beliebigen Editor.
  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 von Shrine

Anmeldeseite von Shrine

4. Bilderrahmenmenü hinzufügen

Hinter allen anderen Inhalten und Komponenten wird ein Hintergrund angezeigt. Sie besteht aus zwei Ebenen: einer Rückebene (mit Aktionen und Filtern) und einer Vorderebene (mit Inhalten). Sie können einen Hintergrund verwenden, um interaktive Informationen und Aktionen wie Navigation oder Inhaltsfilter anzuzeigen.

Leiste der Start-App entfernen

Das HomePage-Widget ist der Inhalt des Front-Layers. Derzeit gibt es eine App-Leiste. Wir verschieben die App-Leiste in die Back-Layer 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));

Hintergrund-Widget hinzufügen

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

Das backLayer enthält ein Menü, über das Sie eine Kategorie zum Filtern der Liste auswählen können (currentCategory). Da die Menüauswahl bestehen bleiben soll, wird der 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 Klasse _BackdropState 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, genau wie es bei der HomePage 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 Backdrop-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'),
),

Speichern Sie Ihr Projekt. Die Startseite und die App-Leiste sollten angezeigt werden:

Android

iOS

Produktseite für Schrein mit rosa Hintergrund

Shrine-Produktseite mit rosafarbenem Hintergrund

Die backLayer zeigt den rosafarbenen Bereich in einer neuen Ebene hinter der Startseite der frontLayer.

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

92ed338a15a074bd.png

Sie können jetzt sowohl das Design als auch den Inhalt der Ebenen anpassen.

5. Form hinzufügen

In diesem Schritt gestalten Sie die Vorderseite so, dass sie oben links einen Ausschnitt hat.

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 für das Branding verwendet werden. 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 gestellte Shrine-Logo diente als Inspiration für die Shape-Story der Shrine App. Eine Shape-Story ist die übliche Verwendung von Formen, die in einer App angewendet werden. Die Logoform wird beispielsweise in den Elementen auf der Anmeldeseite widergespiegelt, auf die eine 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 die 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 für Shrine mit benutzerdefinierter Form

Produktseite des Schreins mit benutzerdefiniertem Format

Wir haben der primären Oberfläche des Schreins eine benutzerdefinierte Form gegeben. Wir möchten jedoch, dass es optisch mit der App-Leiste verbunden ist.

Farbe der App-Leiste ändern

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

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

Heißstart 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 Nutzer sehen, dass sich direkt hinter der weißen Vorderseite 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. Denke jedoch 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 Nutzer nicht ablenken oder regelmäßig zu viel Zeit in Anspruch nehmen. Es gibt jedoch Situationen, in denen Animationen angebracht sind, z. B. wenn ein Nutzer eine App zum ersten Mal öffnet. Außerdem können Animationen Nutzern die Verwendung Ihrer App näherbringen.

Der Menüschaltfläche eine Auf-/Zu-Bewegung hinzufügen

Fügen Sie oben in backdrop.dart außerhalb des Gültigkeitsbereichs einer Klasse oder Funktion eine Konstante hinzu, die die Geschwindigkeit unserer Animation repräsentiert:

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

Fügen Sie _BackdropState ein AnimationController-Widget hinzu, erstellen Sie es in der Funktion initState() und löschen Sie es in der dispose()-Funktion 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 bewegen.

Fügen Sie Funktionen hinzu, die die Sichtbarkeit der Vorderansicht 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);
  }

Wickeln Sie die Hintergrundebene in ein ExcludeSemantics-Widget ein. 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 Aufbau des Front-/Back-Layer-Stacks mit LayoutBuilder bis zum Layoutzeitpunkt 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 und verwenden Sie diese, um die Sichtbarkeit der vorderen Ebene beim Antippen der Schaltfläche ein- oder auszuschalten.

      // 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. Wenn Sie jedoch nach unten schauen, sehen Sie einen roten Fehler und einen Überlauffehler. Das liegt daran, dass AsymmetricView durch diese Animation zusammengedrückt und kleiner wird, wodurch die Spalten wiederum weniger Platz haben. Die Spalten können dann nicht im vorgegebenen Bereich angeordnet werden und es kommt zu einem Fehler. Wenn wir die Spalten durch Listenansichten ersetzen, sollte die Spaltengröße bei der Animation beibehalten werden.

Produktspalten in einer Listenansicht umschließen

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.

Lade die Seite neu und tippe auf die Menüschaltfläche.

Android

iOS

Leeres Schrein-Menü mit einem Fehler

Leeres Schrein-Menü mit einem Fehler

Die graue Warnung für Überlauf bei „OneProductCardColumn“ ist nicht mehr zu sehen. Jetzt beheben wir den anderen Fehler.

Ä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 Schreinmenü

Leeres Schrein-Menü

Kein Überlauf mehr.

7. Menü auf der hinteren Ebene hinzufügen

Ein Menü ist eine Liste von anklickbaren Textelementen, die Listener benachrichtigen, wenn die Textelemente berührt werden. In diesem Schritt fügen Sie ein Menü für die Kategoriefilterung 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 zu zustandsorientiert.

  1. ShrineApp. markieren
  2. Zeigen Sie je nach IDE Codeaktionen an:
  3. Android Studio: Drücken Sie die ⌥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 Klasse „ShrineAppState“ in „privat“ (_ShrineAppState). Klicken Sie mit der rechten Maustaste auf ShrineAppState und
  7. Android Studio: Wählen Sie „Refactor“ > „Rename“ aus.
  8. VS Code: Symbol umbenennen auswählen
  9. Geben Sie „_ShrineAppState“ ein, um die Klasse privat zu machen.

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 Hintergrundebene in eine CategoryMenuPage.

Importiere unter app.dart die Seite „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“ in „CategoryMenuPage“ und das Feld „currentCategory“ in die Instanzvariable.

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

Lade die Seite neu und tippe 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 „Category“ hinzu und übergeben Sie sie an die 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

Gefilterte Produktseite von Shrine

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-Reload 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 Artikelkategorie sehen. Jetzt fügen Sie diese Funktion auch dem vorderen Layer hinzu.

Vorne-Ebene 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 die neue onTap-Property für _BackdropState in der _buildStack()-Funktion:

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

Laden Sie die Seite neu und tippen Sie auf die Oberseite der Vorderseite. Die Ebene sollte sich jedes Mal öffnen und schließen, wenn Sie oben auf die Vorderseite tippen.

8. Markensymbol hinzufügen

Markensymbole umfassen auch 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

Produktseite von Shrine mit Markensymbol

Erstellen Sie in backdrop.dart die 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,
              ),
            ),
          ],
        )
      ]),
    );
  }
}

Das _BackdropTitle ist ein benutzerdefiniertes Widget, das das einfache Text-Widget für den title-Parameter 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 Titel des Hintergrunds gerendert werden können. Der title-Parameter der 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 in ein SizedBox gewickelt, um Platz für die horizontale Symbolbewegung zu schaffen.

Dank der Architektur von Flutter, bei der alles ein Widget ist, kann das Layout des Standard-AppBar geändert werden, 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 erfolgt, ohne dass andere Parameter wie die Aktionssymbole geändert werden müssen. Diese funktionieren weiterhin wie gewohnt.

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 zu aktualisieren, 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. Weitere Komponenten in Material Flutter finden Sie im Katalog der Material Components-Widgets.

Als zusätzliche Herausforderung können Sie versuchen, das Markensymbol durch ein AnimatedIcon zu ersetzen, das zwischen zwei Symbolen animiert wird, wenn der Hintergrund sichtbar wird.

Je nach Ihren Interessen können Sie auch viele andere Flutter-Codelabs ausprobieren. Wir haben ein weiteres Material-spezifisches Codelab für Sie: Building Beautiful Transitions with Material Motion for Flutter.

Ich konnte dieses Codelab mit angemessenem Zeitaufwand und Mühe abschließen.

Stimme voll zu Stimme zu Neutral Stimme nicht zu Stimme überhaupt nicht zu

Ich möchte Material Components weiterhin verwenden.

Stimme voll und ganz zu Stimme zu Neutral Stimme nicht zu Stimme überhaupt nicht zu