MDC-104 Flutter: Material Advanced Components

1. Einführung

logo_components_color_2x_web_96dp.png

Material Components (MDC) helfen Entwicklern bei der Implementierung von Material Design. MDC wurde von einem Team aus Entwicklern und UX-Designern bei Google entwickelt und bietet Dutzende von ansprechenden und funktionalen UI-Komponenten. Es ist für Android, iOS, das Web und Flutter verfügbar.material.io/develop

Im Codelab MDC-103 haben Sie die Farbe, Höhe, Typografie und Form von Material-Komponenten (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. Eine Schaltfläche ist jedoch mehr als nur eine Möglichkeit für Nutzer, eine Aktion auszuführen. Sie ist auch ein visueller Ausdruck von Form, Größe und Farbe, der dem Nutzer signalisiert, dass sie interaktiv ist und dass bei Berührung oder Klick etwas passiert.

In den Material Design-Richtlinien werden Komponenten aus der Sicht eines Designers beschrieben. Sie beschreiben eine Vielzahl von grundlegenden Funktionen, die plattformübergreifend verfügbar sind, sowie die anatomischen Elemente, aus denen jede Komponente besteht. Ein Hintergrund enthält beispielsweise eine Hintergrundebene und deren Inhalt, die Vorderebene und deren Inhalt, Bewegungsregeln und Anzeigeoptionen. Jede dieser Komponenten kann an die Anforderungen, Anwendungsfälle und Inhalte der jeweiligen App angepasst werden.

Umfang

In diesem Codelab ändern Sie die Benutzeroberfläche der Shrine-App in eine zweistufige Darstellung, die als „Kulisse“ bezeichnet wird. Der Hintergrund enthält ein Menü mit auswählbaren Kategorien, mit denen die im asymmetrischen Raster angezeigten Produkte gefiltert werden. 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

Menü mit 4 Kategorien

Menü mit 4 Kategorien

Material-Flutter-Komponenten und ‑Subsysteme in diesem Codelab

  • 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 Softwarekomponenten: 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 ist und auf den Entwicklermodus eingestellt ist.
  • Der iOS-Simulator (erfordert die Installation von Xcode-Tools).
  • Android Emulator (Einrichtung in Android Studio erforderlich)
  • Ein Browser (für das Debugging ist Chrome erforderlich).
  • Als Windows-, Linux- oder macOS-Desktopanwendung. Sie müssen auf der Plattform entwickeln, auf der Sie die Bereitstellung planen. Wenn Sie also eine Windows-Desktop-App entwickeln möchten, müssen Sie unter Windows entwickeln, um auf die entsprechende Build-Kette zuzugreifen. Es gibt betriebssystemspezifische Anforderungen, die auf docs.flutter.dev/desktop ausführlich beschrieben werden.

3. Starter-App für das Codelab herunterladen

Sie haben bereits MDC-103 abgeschlossen?

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

Sie fangen gerade erst an?

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

…oder aus 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 unter Erste Schritte: Testlauf für den von Ihnen ausgewählten Editor.

Fertig! Auf Ihrem Gerät sollte die Shrine-Anmeldeseite aus den vorherigen Codelabs angezeigt werden.

Android

iOS

Anmeldeseite für Shrine

Anmeldeseite für Shrine

4. Hintergrundmenü hinzufügen

Ein Hintergrund wird hinter allen anderen Inhalten und Komponenten angezeigt. Sie besteht aus zwei Ebenen: einer Hintergrundebene (mit Aktionen und Filtern) und einer Vorderebene (mit Inhalten). Mit einem Backdrop können Sie interaktive Informationen und Aktionen wie die Navigation oder Inhaltsfilter anzeigen.

App-Leiste für Zuhause entfernen

Das HomePage-Widget ist der Inhalt unserer vorderen Ebene. Derzeit ist eine App-Leiste vorhanden. Wir verschieben die App-Leiste in die Hintergrundebene und die Startseite enthält nur noch die AsymmetricView.

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

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

Bilderrahmen-Widget hinzufügen

Erstellen Sie ein Widget mit dem Namen Backdrop, das frontLayer und backLayer enthält.

Die backLayer enthält ein Menü, in dem Sie eine Kategorie zum Filtern der Liste auswählen können (currentCategory). Da die Menüauswahl beibehalten werden soll, machen wir Backdrop zu einem zustandsbehafteten 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)

Bestimmte Properties sind mit required gekennzeichnet. Dies ist eine Best Practice für Attribute im Konstruktor, die keinen Standardwert haben und nicht null sein können. Sie sollten daher nicht vergessen werden.

Fügen Sie unter der Definition der Backdrop-Klasse 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, genau wie HomePage früher. Der Body von „Scaffold“ ist jedoch ein Stack. Die untergeordneten Elemente eines Stacks können sich überlappen. Die Größe und Position jedes untergeordneten Elements wird relativ zum übergeordneten Element des Stacks angegeben.

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

Importieren Sie in app.dart die Dateien 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 ein Backdrop zurückgeben, dessen 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 jetzt angezeigt werden:

Android

iOS

Produktseite für einen Schrein mit rosa Hintergrund

Produktseite für einen Schrein mit rosa 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 HomePage hat. Das sollte in etwa so aussehen:

92ed338a15a074bd.png

Sie können jetzt das Design und den Inhalt beider Ebenen anpassen.

5. Form hinzufügen

In diesem Schritt gestalten Sie die vordere Ebene so, dass in der oberen linken Ecke ein Schnitt entsteht.

In Material Design wird diese Art von Anpassung als „Form“ bezeichnet. Materialoberflächen können beliebige Formen haben. Formen verleihen Oberflächen mehr Ausdruck und Stil und können verwendet werden, um das Branding zu unterstreichen. Normale Rechtecke können mit abgerundeten 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 abgewinkelte Shrine-Logo diente als Inspiration für die Formgebung der Shrine App. Eine Formgebung ist die gemeinsame Verwendung von Formen, die in einer App angewendet werden. Die Form des Logos wird beispielsweise in den Elementen der Anmeldeseite wiederholt, auf die die Form angewendet wird. In diesem Schritt gestalten Sie die vordere Ebene mit einem abgeschrägten 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,
          ),
        ],
      ),
    );
  }
}

Umschließen Sie dann in der _buildStack()-Funktion von _BackdropState die Vordergrundebene mit _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 ein Shrine-Produkt mit benutzerdefinierter Form

Produktseite für ein Shrine-Produkt mit benutzerdefinierter Form

Die primäre Oberfläche von Shrine hat eine benutzerdefinierte Form. Wir möchten jedoch, dass sie optisch mit der App-Leiste verbunden ist.

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

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 etwas direkt hinter der weißen Vordergrundebene befindet. Fügen wir Bewegung hinzu, damit Nutzer die hintere Ebene des Hintergrunds sehen können.

6. Bewegung hinzufügen

Mit Animationen können Sie Ihre App zum Leben erwecken. Sie kann groß und dramatisch, dezent und minimal oder irgendetwas dazwischen sein. Denken Sie aber daran, dass die Art der Bewegung zur Situation passen sollte. Bewegungen, die auf wiederholte, regelmäßige Aktionen angewendet werden, sollten klein und subtil sein, damit die Aktionen den Nutzer nicht ablenken oder regelmäßig zu viel Zeit in Anspruch nehmen. Es gibt jedoch Situationen, in denen sie angebracht sind, z. B. wenn ein Nutzer eine App zum ersten Mal öffnet. Einige Animationen können Nutzern auch helfen, die App zu verwenden.

Enthüllungsbewegung für die 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 anzugeben, die die Animation haben soll:

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

Fügen Sie dem _BackdropState-Widget ein AnimationController-Widget hinzu, instanziieren Sie es in der Funktion initState() und geben Sie es in der Funktion dispose() des Status frei:

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

Der AnimationController koordiniert Animationen und bietet eine API zum Abspielen, Rückwärtslaufen und Stoppen der Animation. Jetzt brauchen wir Funktionen, die es bewegen.

Fügen Sie Funktionen hinzu, mit denen die Sichtbarkeit der vorderen Ebene bestimmt und geändert werden kann:

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

Schließen Sie backLayer in ein ExcludeSemantics-Widget ein. Dieses Widget schließt die Menüpunkte der Backlayer aus dem Semantikbaum aus, wenn die Backlayer 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()“, sodass sie einen BuildContext und BoxConstraints akzeptiert. Fügen Sie außerdem eine PositionedTransition hinzu, die eine RelativeRectTween-Animation verwendet:

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

Rufen Sie schließlich nicht die Funktion „_buildStack“ für den Body des Scaffold auf, sondern 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 Vorder-/Hintergrund-Layer-Stacks bis zur Layoutzeit mit LayoutBuilder 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 führende Menüsymbol in der App-Leiste in ein IconButton um und verwenden Sie es, um die Sichtbarkeit der Vordergrundebene umzuschalten, wenn auf die Schaltfläche getippt wird.

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

Laden Sie die Seite neu 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 (gleitet nach unten). Wenn Sie nach unten schauen, sehen Sie einen roten Fehler und einen Überlauffehler. Das liegt daran, dass die AsymmetricView durch diese Animation zusammengedrückt und kleiner wird, wodurch weniger Platz für die Spalten bleibt. Irgendwann können die Spalten nicht mehr im verfügbaren Bereich angeordnet werden und es kommt zu einem Fehler. Wenn wir die Spalten durch ListViews ersetzen, sollte die Spaltengröße während der Animation beibehalten werden.

Produktspalten in eine ListView einfügen

Ersetzen Sie in supplemental/product_columns.dart die Spalte in OneProductCardColumn durch eine 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,
        ),

      ],
    );
  }
}

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

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

Android

iOS

Leeres Schrein-Menü mit einem Fehler

Leeres Schrein-Menü mit einem Fehler

Die graue Überlaufwarnung in OneProductCardColumn ist verschwunden. Jetzt beheben wir das andere Problem.

Ändern Sie in supplemental/product_columns.dart die Berechnung von 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,
            ),
          ),
        ],
      );

Wir haben auch einige Sicherheitsfunktionen für imageAspectRatio hinzugefügt.

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

Android

iOS

Leeres Schreinmenü

Leeres Schreinmenü

Keine Überläufe mehr.

7. Menü auf der Rückseite hinzufügen

Ein Menü ist eine Liste von Text-Elementen, die angeklickt werden können. Wenn ein Zuhörer auf ein Text-Element tippt, wird er darüber benachrichtigt. In diesem Schritt fügen Sie ein Menü zum Filtern nach Kategorien hinzu.

Speisekarte hinzufügen

Fügen Sie das Menü der vorderen Ebene und die interaktiven Schaltflächen der hinteren Ebene 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. So rufen Sie Code-Aktionen in Ihrer IDE auf:
  3. Android Studio: Drücken Sie ⌥Enter (macOS) oder Alt + Eingabetaste.
  4. VS Code: Drücken Sie ⌘. (macOS) oder Strg +.
  5. Wählen Sie „Convert to StatefulWidget“ (In StatefulWidget konvertieren) aus.
  6. Ändern Sie die ShrineAppState-Klasse in „private“ (_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ügen Sie in app.dart eine Variable für die ausgewählte Kategorie und einen Callback hinzu, wenn darauf getippt wird:

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

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

Ändern Sie dann die Rückseite 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“ in „CategoryMenuPage“ und das Feld „currentCategory“ so, dass die Instanzvariable verwendet wird.

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

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

Android

iOS

Schreinmenü mit 4 Kategorien

Schreinmenü mit 4 Kategorien

Wenn Sie auf eine Menüoption tippen, passiert noch nichts. Das sollte nicht sein.

Fügen Sie in home.dart eine Variable für „Category“ (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

Gefilterte Produktseite im Schrein

Gefilterte Produktseite im Schrein

Sie wurden gefiltert.

Schließen der vorderen Ebene nach einer Menüauswahl

Fügen Sie in backdrop.dart eine Überschreibung für die Funktion didUpdateWidget() (wird immer aufgerufen, wenn sich die Widget-Konfiguration ändert) in _BackdropState hinzu:

  // 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 einen Hot-Reload auszulösen. Tippen Sie auf das Menüsymbol und wählen Sie eine Kategorie aus. Das Menü sollte automatisch geschlossen werden und die ausgewählte Kategorie von Artikeln sollte angezeigt werden. Jetzt fügen Sie diese Funktion auch der Front-Ebene hinzu.

Vordergrundebene ein- oder ausblenden

Fügen Sie in backdrop.dart einen On-Tap-Callback zur Hintergrundebene 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“ einen GestureDetector hinzu: „Column“ (Spalte).

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

Laden Sie die Seite neu und tippen Sie auf den oberen Teil der vorderen Ebene. Die Ebene sollte sich jedes Mal öffnen und schließen, wenn Sie auf den oberen Teil der vorderen Ebene tippen.

8. Markensymbol hinzufügen

Das gilt auch für bekannte Symbole. Wir können das Aufdeck-Symbol anpassen und mit unserem Titel kombinieren, um einen einzigartigen, markenspezifischen Look zu erzielen.

Symbol der Menüschaltfläche ändern

Android

iOS

Produktseite für ein Shrine-Produkt mit Markensymbol

Produktseite für ein Shrine-Produkt 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 title-Parameter des AppBar-Widgets ersetzt. Es hat ein animiertes Menüsymbol und animierte Übergänge zwischen den Titeln auf der Vorder- und Rückseite. 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. Die Entfernung ist erforderlich, damit das benutzerdefinierte Markensymbol 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. Die frontTitle und backTitle werden ebenfalls übergeben, damit sie im Hintergrundtitel gerendert werden können. Der Parameter title des 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 _BackdropTitle. erstellt. Es enthält eine Stack mit animierten Symbolen: ein schräges Menü und ein Diamant, der in ein IconButton eingebettet ist, damit er gedrückt werden kann. Das IconButton wird dann in ein SizedBox eingefügt, um Platz für die horizontale Bewegung des Symbols zu schaffen.

Dank der Architektur von Flutter, bei der „alles ein Widget ist“, kann das Layout des Standard-AppBar geändert werden, ohne dass ein völlig neues benutzerdefiniertes AppBar-Widget erstellt werden muss. Der Parameter title, der ursprünglich ein Text-Widget ist, kann durch ein komplexeres _BackdropTitle ersetzt werden. Da das _BackdropTitle auch das benutzerdefinierte Symbol enthält, ersetzt es das Attribut leading, das jetzt weggelassen werden kann. Diese einfache Ersetzung des Widgets erfolgt, ohne dass andere Parameter geändert werden, z. B. die Aktionssymbole, die weiterhin eigenständig funktionieren.

Verknüpfung zum Anmeldebildschirm hinzufügen

Fügen Sie in backdrop.dart,über die beiden nachfolgenden Symbole in der App-Leiste eine Verknüpfung zurück zum Anmeldebildschirm hinzu: Ändern Sie die semantischen Labels der Symbole, um ihren neuen Zweck widerzuspiegeln.

        // 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, wird eine Fehlermeldung angezeigt. So beheben Sie den Fehler:login.dart

import 'login.dart';

Lade die App neu und tippe auf die Schaltflächen „Suchen“ oder „Abstimmen“, um zum Anmeldebildschirm zurückzukehren.

9. Glückwunsch!

In diesen vier Codelabs haben Sie gelernt, wie Sie mit Material-Komponenten einzigartige, elegante Benutzeroberflächen erstellen, die die Persönlichkeit und den Stil Ihrer Marke widerspiegeln.

Weiteres Vorgehen

Dieses Codelab (MDC-104) schließt diese Reihe von Codelabs ab. Weitere Komponenten finden Sie im Katalog der Material-Komponenten-Widgets.

Als Stretch Goal kannst du versuchen, das Markensymbol durch ein AnimatedIcon zu ersetzen, das zwischen zwei Symbolen animiert wird, wenn der Hintergrund sichtbar gemacht wird.

Je nach Ihren Interessen gibt es viele weitere Flutter-Codelabs, die Sie ausprobieren können. Wir haben ein weiteres Material-spezifisches Codelab, das für Sie interessant sein könnte: Building Beautiful Transitions with Material Motion for Flutter.

Ich konnte dieses Codelab in angemessener Zeit und mit angemessenem Aufwand durcharbeiten.

Stimme vollkommen zu Stimme zu Neutral Stimme nicht zu Stimme überhaupt nicht zu

Ich möchte Material Components auch in Zukunft verwenden.

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