1. Einführung
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 |
Codelab: Material Flutter-Komponenten und -Subsysteme
- Form
Wie würden Sie Ihre Erfahrung mit der Flutter-Entwicklung bewerten?
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
- Öffnen Sie das Projekt in einem beliebigen Editor.
- 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 |
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 |
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:
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 |
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 |
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 |
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 |
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 |
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.
ShrineApp.
markieren- Zeigen Sie je nach IDE Codeaktionen an:
- Android Studio: Drücken Sie die ⌥Eingabetaste (macOS) oder Alt + Eingabetaste.
- VS-Code: Drücken Sie ⌘. (macOS) oder Strg+.
- Wählen Sie „Convert to StatefulWidget“ (In StatefulWidget konvertieren).
- Ändern Sie die Klasse „ShrineAppState“ in „privat“ (_ShrineAppState). Klicken Sie mit der rechten Maustaste auf ShrineAppState und
- Android Studio: Wählen Sie „Refactor“ > „Rename“ aus.
- VS Code: Symbol umbenennen auswählen
- 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 |
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 |
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 |
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.