1. Introduzione
| I componenti Material (MDC) aiutano gli sviluppatori a implementare Material Design. Creato da un team di ingegneri e progettisti UX di Google, MDC include decine di componenti UI belli e funzionali ed è disponibile per Android, iOS, web e Flutter.material.io/develop |
Nel codelab MDC-103, hai personalizzato il colore, l'elevazione, la tipografia e la forma dei componenti Material (MDC) per dare uno stile alla tua app.
Un componente del sistema Material Design esegue un insieme di attività predefinite e ha determinate caratteristiche, ad esempio un pulsante. Tuttavia, un pulsante non è solo un modo per un utente di eseguire un'azione, ma anche un'espressione visiva di forma, dimensione e colore che indica all'utente che è interattivo e che qualcosa accadrà al tocco o al clic.
Le linee guida di Material Design descrivono i componenti dal punto di vista di un designer. Descrivono un'ampia gamma di funzioni di base disponibili su tutte le piattaforme e gli elementi anatomici che compongono ogni componente. Ad esempio, uno sfondo contiene un livello posteriore e i relativi contenuti, il livello anteriore e i relativi contenuti, regole di movimento e opzioni di visualizzazione. Ciascuno di questi componenti può essere personalizzato in base alle esigenze, ai casi d'uso e ai contenuti di ogni app.
Cosa creerai
In questo codelab, modificherai la UI dell'app Shrine in una presentazione a due livelli chiamata "sfondo". Lo sfondo include un menu che elenca le categorie selezionabili utilizzate per filtrare i prodotti mostrati nella griglia asimmetrica. In questo codelab utilizzerai:
- Forma
- Movimento
- Widget Flutter (che hai utilizzato nei codelab precedenti)
Android | iOS |
|
|
|
|
Componenti e sottosistemi Material Flutter in questo codelab
- Forma
Come valuteresti il tuo livello di esperienza con lo sviluppo Flutter?
2. Configura l'ambiente di sviluppo Flutter
Per completare questo lab, hai bisogno di due software: l'SDK Flutter e un editor.
Puoi eseguire il codelab utilizzando uno qualsiasi di questi dispositivi:
- Un dispositivo fisico Android o iOS connesso al computer e impostato sulla modalità sviluppatore.
- Il simulatore iOS (richiede l'installazione degli strumenti Xcode).
- L'emulatore Android (richiede la configurazione in Android Studio).
- Un browser (Chrome è necessario per il debug).
- Come applicazione desktop Windows, Linux o macOS. Devi sviluppare sulla piattaforma in cui prevedi di eseguire il deployment. Pertanto, se vuoi sviluppare un'app desktop Windows, devi svilupparla su Windows per accedere alla catena di build appropriata. Esistono requisiti specifici del sistema operativo trattati in dettaglio su docs.flutter.dev/desktop.
3. Scarica l'app iniziale del codelab
Hai seguito il corso MDC-103?
Se hai completato MDC-103, il codice dovrebbe essere pronto per questo codelab. Vai al passaggio Aggiungere il menu dello sfondo.
Parti da zero?
L'app iniziale si trova nella directory material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.
...o clonalo da GitHub
Per clonare questo codelab da GitHub, esegui i seguenti comandi:
git clone https://github.com/material-components/material-components-flutter-codelabs.git cd material-components-flutter-codelabs/mdc_100_series git checkout 104-starter_and_103-complete
Apri il progetto ed esegui l'app
- Apri il progetto nell'editor che preferisci.
- Segui le istruzioni per "Eseguire l'app" nella sezione Guida rapida: prova per l'editor che hai scelto.
Operazione riuscita. Sul dispositivo dovrebbe essere visualizzata la pagina di accesso di Shrine delle codelab precedenti.
Android | iOS |
|
|
4. Aggiungere il menu dello sfondo
Uno sfondo viene visualizzato dietro tutti gli altri contenuti e componenti. È composto da due livelli: un livello posteriore (che mostra azioni e filtri) e un livello anteriore (che mostra i contenuti). Puoi utilizzare un backdrop per visualizzare informazioni e azioni interattive, come filtri di navigazione o contenuti.
Rimuovere la barra dell'app Home
Il widget HomePage sarà il contenuto del nostro livello frontale. Al momento ha una barra delle app. Sposteremo la barra delle app nel livello posteriore e la home page includerà solo AsymmetricView.
In home.dart, modifica la funzione build() in modo che restituisca solo un AsymmetricView:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
Aggiungere il widget Sfondo
Crea un widget denominato Sfondo che includa frontLayer e backLayer.
backLayer include un menu che consente di selezionare una categoria per filtrare l'elenco (currentCategory). Poiché vogliamo che la selezione del menu venga mantenuta, Backdrop sarà un widget stateful.
Aggiungi un nuovo file a /lib denominato backdrop.dart:
import 'package:flutter/material.dart';
import 'model/product.dart';
// TODO: Add velocity constant (104)
class Backdrop extends StatefulWidget {
final Category currentCategory;
final Widget frontLayer;
final Widget backLayer;
final Widget frontTitle;
final Widget backTitle;
const Backdrop({
required this.currentCategory,
required this.frontLayer,
required this.backLayer,
required this.frontTitle,
required this.backTitle,
Key? key,
}) : super(key: key);
@override
_BackdropState createState() => _BackdropState();
}
// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)
Nota che contrassegniamo alcune proprietà con required. Si tratta di una best practice per le proprietà nel costruttore che non hanno un valore predefinito e non possono essere null, pertanto non devono essere dimenticate.
Sotto la definizione della classe Backdrop, aggiungi la classe _BackdropState:
// TODO: Add _BackdropState class (104)
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
// TODO: Add AnimationController widget (104)
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack() {
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
widget.frontLayer,
],
);
}
@override
Widget build(BuildContext context) {
var appBar = AppBar(
elevation: 0.0,
titleSpacing: 0.0,
// TODO: Replace leading menu icon with IconButton (104)
// TODO: Remove leading property (104)
// TODO: Create title with _BackdropTitle parameter (104)
leading: Icon(Icons.menu),
title: Text('SHRINE'),
actions: <Widget>[
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
// TODO: Add open login (104)
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
// TODO: Add open login (104)
},
),
],
);
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: _buildStack(),
);
}
}
La funzione build() restituisce uno Scaffold con una barra dell'app proprio come faceva HomePage. Ma il corpo di Impalcatura è uno stack. I figli di uno Stack possono sovrapporsi. Le dimensioni e la posizione di ogni figlio sono specificate rispetto al genitore dello Stack.
Ora aggiungi un'istanza Backdrop a ShrineApp.
In app.dart, importa backdrop.dart e model/product.dart:
import 'backdrop.dart'; // New code
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // New code
import 'supplemental/cut_corners_border.dart';
In app.dart, modifica la route / restituendo un Backdrop che ha HomePage come frontLayer:
// TODO: Change to a Backdrop with a HomePage frontLayer (104)
'/': (BuildContext context) => Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: Category.all,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: Container(color: kShrinePink100),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
Salva il progetto. Dovresti vedere la nostra home page e la barra delle app:
Android | iOS |
|
|
backLayer mostra l'area rosa in un nuovo livello dietro la home page frontLayer.
Puoi utilizzare Flutter Inspector per verificare che lo stack contenga effettivamente un container dietro una home page. Dovrebbe essere simile a questo:

Ora puoi modificare il design e i contenuti di entrambi i livelli.
5. Aggiungere una forma
In questo passaggio, applicherai uno stile al livello anteriore per aggiungere un taglio nell'angolo in alto a sinistra.
Material Design si riferisce a questo tipo di personalizzazione come forma. Le superfici dei materiali possono avere forme arbitrarie. Le forme aggiungono enfasi e stile alle superfici e possono essere utilizzate per esprimere il branding. Le forme rettangolari ordinarie possono essere personalizzate con angoli e bordi curvi o angolati e con un numero qualsiasi di lati. Possono essere simmetriche o irregolari.
Aggiungere una forma al livello anteriore
Il logo angolato di Shrine ha ispirato la storia delle forme dell'app Shrine. Una storia delle forme è l'uso comune di forme applicate in tutta l'app. Ad esempio, la forma del logo viene ripresa negli elementi della pagina di accesso a cui è stata applicata la forma. In questo passaggio, modellerai lo strato anteriore con un taglio angolato nell'angolo in alto a sinistra.
In backdrop.dart, aggiungi una nuova classe _FrontLayer:
// TODO: Add _FrontLayer class (104)
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: const BeveledRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
Expanded(
child: child,
),
],
),
);
}
}
Quindi, nella funzione _buildStack() di _BackdropState, racchiudi il livello anteriore in un _FrontLayer:
Widget _buildStack() {
// TODO: Create a RelativeRectTween Animation (104)
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
// TODO: Add a PositionedTransition (104)
// TODO: Wrap front layer in _FrontLayer (104)
_FrontLayer(child: widget.frontLayer),
],
);
}
Ricarica.
Android | iOS |
|
|
Abbiamo dato alla superficie principale di Shrine una forma personalizzata. Tuttavia, vogliamo che si colleghi visivamente alla barra delle app.
Modificare il colore della barra delle app
In app.dart, modifica la funzione _buildShrineTheme() nel seguente modo:
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light(useMaterial3: true);
return base.copyWith(
colorScheme: base.colorScheme.copyWith(
primary: kShrinePink100,
onPrimary: kShrineBrown900,
secondary: kShrineBrown900,
error: kShrineErrorRed,
),
textTheme: _buildShrineTextTheme(base.textTheme),
textSelectionTheme: const TextSelectionThemeData(
selectionColor: kShrinePink100,
),
appBarTheme: const AppBarTheme(
foregroundColor: kShrineBrown900,
backgroundColor: kShrinePink100,
),
inputDecorationTheme: const InputDecorationTheme(
border: CutCornersBorder(),
focusedBorder: CutCornersBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
floatingLabelStyle: TextStyle(
color: kShrineBrown900,
),
),
);
}
Riavvio a caldo. Ora dovrebbe essere visualizzata la nuova barra delle app colorata.
Android | iOS |
|
|
A causa di questa modifica, gli utenti possono vedere che c'è qualcosa proprio dietro lo strato bianco anteriore. Aggiungiamo movimento in modo che gli utenti possano vedere il livello posteriore dello sfondo.
6. Aggiungi movimento
Il movimento è un modo per dare vita alla tua app. Può essere grande e drammatico, sottile e minimalista o una via di mezzo. Tuttavia, ricorda che il tipo di movimento che utilizzi deve essere adatto alla situazione. Il movimento applicato ad azioni ripetute e regolari deve essere piccolo e sottile, in modo che le azioni non distraggano l'utente o non richiedano troppo tempo su base regolare. Tuttavia, ci sono situazioni appropriate, come la prima volta che un utente apre un'app, che possono essere più accattivanti e alcune animazioni possono aiutare l'utente a capire come utilizzare l'app.
Aggiungere il movimento di rivelazione al pulsante del menu
Nella parte superiore di backdrop.dart, al di fuori dell'ambito di qualsiasi classe o funzione, aggiungi una costante per rappresentare la velocità che vuoi che abbia l'animazione:
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
Aggiungi un widget AnimationController a _BackdropState, istanzialo nella funzione initState() ed eliminalo nella funzione dispose() dello stato:
// TODO: Add AnimationController widget (104)
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
// TODO: Add override for didUpdateWidget (104)
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// TODO: Add functions to get and change front layer visibility (104)
AnimationController coordina le animazioni e fornisce un'API per riprodurre, invertire e interrompere l'animazione. Ora abbiamo bisogno di funzioni che lo facciano muovere.
Aggiungi funzioni che determinano e modificano la visibilità del livello anteriore:
// 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);
}
Inserisci backLayer in un widget ExcludeSemantics. Questo widget escluderà le voci di menu di backLayer dall'albero semantico quando il livello posteriore non è visibile.
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
Modifica la funzione _buildStack() in modo che accetti un BuildContext e BoxConstraints. Includi anche una PositionedTransition che accetta un'animazione RelativeRectTween:
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
// TODO: Create a RelativeRectTween Animation (104)
Animation<RelativeRect> layerAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, layerTop, 0.0, layerTop - layerSize.height),
end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
// TODO: Add a PositionedTransition (104)
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
child: widget.frontLayer,
),
),
],
);
}
Infine, anziché chiamare la funzione _buildStack per il corpo dello Scaffold, restituisci un widget LayoutBuilder che utilizza _buildStack come builder:
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
Abbiamo ritardato la creazione dello stack di livelli anteriore/posteriore fino al momento del layout utilizzando LayoutBuilder, in modo da poter incorporare l'altezza complessiva effettiva dello sfondo. LayoutBuilder è un widget speciale il cui callback del builder fornisce vincoli di dimensione.
Nella funzione build(), trasforma l'icona del menu principale nella barra delle app in un IconButton e usala per attivare/disattivare la visibilità del livello anteriore quando viene toccato il pulsante.
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
Ricarica, quindi tocca il pulsante del menu nel simulatore.
Android | iOS |
|
|
Il livello anteriore si anima (scorre) verso il basso. Se guardi in basso, però, vedrai un errore rosso e un errore di overflow. Questo perché AsymmetricView viene compresso e rimpicciolito da questa animazione, il che a sua volta lascia meno spazio alle colonne. Alla fine, le colonne non possono essere disposte nello spazio disponibile e si verifica un errore. Se sostituiamo le colonne con ListViews, le dimensioni delle colonne devono rimanere invariate durante l'animazione.
Inserire le colonne dei prodotti in un ListView
In supplemental/product_columns.dart, sostituisci la colonna in OneProductCardColumn con un ListView:
class OneProductCardColumn extends StatelessWidget {
const OneProductCardColumn({required this.product, Key? key}) : super(key: key);
final Product product;
@override
Widget build(BuildContext context) {
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
reverse: true,
children: <Widget>[
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 550,
),
child: ProductCard(
product: product,
),
),
const SizedBox(
height: 40.0,
),
],
);
}
}
La colonna include MainAxisAlignment.end. Per iniziare il layout dal basso, seleziona reverse: true. L'ordine dei bambini viene invertito per compensare la modifica.
Ricarica e tocca il pulsante del menu.
Android | iOS |
|
|
L'avviso di overflow grigio nella colonna OneProductCardColumn non è più presente. Ora risolviamo l'altro problema.
In supplemental/product_columns.dart, modifica il modo in cui viene calcolato imageAspectRatio e sostituisci la colonna in TwoProductCardColumn con una 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,
),
),
],
);
Abbiamo anche aggiunto alcune misure di sicurezza a imageAspectRatio.
Ricarica. Poi tocca il pulsante del menu.
Android | iOS |
|
|
Nessun overflow.
7. Aggiungere un menu sul livello posteriore
Un menu è un elenco di elementi di testo selezionabili che notificano agli ascoltatori quando vengono toccati. In questo passaggio, aggiungerai un menu di filtri per categoria.
Aggiungere il menu
Aggiungi il menu al livello anteriore e i pulsanti interattivi al livello posteriore.
Crea un nuovo file denominato lib/category_menu_page.dart:
import 'package:flutter/material.dart';
import 'colors.dart';
import 'model/product.dart';
class CategoryMenuPage extends StatelessWidget {
final Category currentCategory;
final ValueChanged<Category> onCategoryTap;
final List<Category> _categories = Category.values;
const CategoryMenuPage({
Key? key,
required this.currentCategory,
required this.onCategoryTap,
}) : super(key: key);
Widget _buildCategory(Category category, BuildContext context) {
final categoryString =
category.toString().replaceAll('Category.', '').toUpperCase();
final ThemeData theme = Theme.of(context);
return GestureDetector(
onTap: () => onCategoryTap(category),
child: category == currentCategory
? Column(
children: <Widget>[
const SizedBox(height: 16.0),
Text(
categoryString,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 14.0),
Container(
width: 70.0,
height: 2.0,
color: kShrinePink400,
),
],
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text(
categoryString,
style: theme.textTheme.bodyLarge!.copyWith(
color: kShrineBrown900.withAlpha(153)
),
textAlign: TextAlign.center,
),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.only(top: 40.0),
color: kShrinePink100,
child: ListView(
children: _categories
.map((Category c) => _buildCategory(c, context))
.toList()),
),
);
}
}
Si tratta di un GestureDetector che racchiude una colonna i cui elementi secondari sono i nomi delle categorie. Un trattino basso viene utilizzato per indicare la categoria selezionata.
In app.dart, converti il widget ShrineApp da stateless a stateful.
- Evidenzia
ShrineApp. - In base al tuo IDE, mostra le azioni del codice:
- Android Studio: premi ⌥Invio (macOS) o Alt + Invio
- VS Code: premi ⌘. (macOS) o Ctrl+.
- Seleziona "Converti in StatefulWidget".
- Modifica la classe ShrineAppState in privata (_ShrineAppState). Fai clic con il tasto destro del mouse su ShrineAppState e
- Android Studio: seleziona Refactor > Rename
- VS Code: seleziona Rinomina simbolo
- Inserisci _ShrineAppState per rendere privata la classe.
In app.dart, aggiungi una variabile a _ShrineAppState per la categoria selezionata e un callback quando viene toccata:
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
Poi cambia il livello posteriore in CategoryMenuPage.
In app.dart, importa CategoryMenuPage:
import 'backdrop.dart';
import 'category_menu_page.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';
Nella funzione build(), modifica il campo backLayer in CategoryMenuPage e il campo currentCategory in modo che accetti la variabile di istanza.
'/': (BuildContext context) => Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: _currentCategory,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: CategoryMenuPage(
currentCategory: _currentCategory,
onCategoryTap: _onCategoryTap,
),
frontTitle: const Text('SHRINE'),
backTitle: const Text('MENU'),
),
Ricarica e tocca il pulsante Menu.
Android | iOS |
|
|
Se tocchi un'opzione di menu, non succede nulla… per ora. Risolviamo il problema.
In home.dart, aggiungi una variabile per la categoria e passala ad AsymmetricView.
import 'package:flutter/material.dart';
import 'model/product.dart';
import 'model/products_repository.dart';
import 'supplemental/asymmetric_view.dart';
class HomePage extends StatelessWidget {
// TODO: Add a variable for Category (104)
final Category category;
const HomePage({this.category = Category.all, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO: Pass Category variable to AsymmetricView (104)
return AsymmetricView(
products: ProductsRepository.loadProducts(category),
);
}
}
In app.dart, supera l'esame _currentCategory per frontLayer:.
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),
Ricarica. Tocca il pulsante del menu nel simulatore e seleziona una categoria.
Android | iOS |
|
|
Sono filtrati.
Chiudi il livello anteriore dopo aver selezionato un menu
In backdrop.dart, aggiungi un override per la funzione didUpdateWidget() (chiamata ogni volta che la configurazione del widget cambia) in _BackdropState:
// TODO: Add override for didUpdateWidget() (104)
@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
_toggleBackdropLayerVisibility();
} else if (!_frontLayerVisible) {
_controller.fling(velocity: _kFlingVelocity);
}
}
Salva il progetto per attivare un ricaricamento rapido. Tocca l'icona del menu e seleziona una categoria. Il menu dovrebbe chiudersi automaticamente e dovresti visualizzare la categoria di elementi selezionata. Ora aggiungerai questa funzionalità anche al livello frontale.
Attiva/disattiva il livello anteriore
In backdrop.dart, aggiungi un callback on-tap al livello di sfondo:
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key? key,
this.onTap, // New code
required this.child,
}) : super(key: key);
final VoidCallback? onTap; // New code
final Widget child;
Poi aggiungi un GestureDetector al figlio di _FrontLayer: i figli di Column:.
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,
),
],
),
Quindi implementa la nuova proprietà onTap in _BackdropState nella funzione _buildStack():
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
Ricarica e tocca la parte superiore del livello anteriore. Il livello deve aprirsi e chiudersi ogni volta che tocchi la parte superiore del livello anteriore.
8. Aggiungere un'icona con brand
L'iconografia brandizzata si estende anche alle icone più comuni. Personalizziamo l'icona di rivelazione e uniamola al titolo per un aspetto unico e brandizzato.
Modificare l'icona del pulsante del menu
Android | iOS |
|
|
In backdrop.dart, crea una nuova classe _BackdropTitle.
// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
final void Function() onPress;
final Widget frontTitle;
final Widget backTitle;
const _BackdropTitle({
Key? key,
required Animation<double> listenable,
required this.onPress,
required this.frontTitle,
required this.backTitle,
}) : _listenable = listenable,
super(key: key, listenable: listenable);
final Animation<double> _listenable;
@override
Widget build(BuildContext context) {
final Animation<double> animation = _listenable;
return DefaultTextStyle(
style: Theme.of(context).textTheme.titleLarge!,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: <Widget>[
// branded icon
SizedBox(
width: 72.0,
child: IconButton(
padding: const EdgeInsets.only(right: 8.0),
onPressed: this.onPress,
icon: Stack(children: <Widget>[
Opacity(
opacity: animation.value,
child: const ImageIcon(AssetImage('assets/slanted_menu.png')),
),
FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(1.0, 0.0),
).evaluate(animation),
child: const ImageIcon(AssetImage('assets/diamond.png')),
)]),
),
),
// Here, we do a custom cross fade between backTitle and frontTitle.
// This makes a smooth animation between the two texts.
Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: const Offset(0.5, 0.0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: const Offset(-0.25, 0.0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
)
]),
);
}
}
_BackdropTitle è un widget personalizzato che sostituirà il widget Text semplice per il parametro title del widget AppBar. Ha un'icona di menu animata e transizioni animate tra i titoli anteriori e posteriori. L'icona del menu animata utilizzerà un nuovo asset. Il riferimento al nuovo slanted_menu.png deve essere aggiunto a pubspec.yaml.
assets:
- assets/diamond.png
# TODO: Add slanted menu asset (104)
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
Rimuovi la proprietà leading nel builder AppBar. La rimozione è necessaria per il rendering dell'icona personalizzata con brand al posto del widget leading originale. L'animazione listenable e l'handler onPress per l'icona brandizzata vengono passati a _BackdropTitle. Vengono passati anche frontTitle e backTitle, in modo che possano essere visualizzati nel titolo dello sfondo. Il parametro title di AppBar dovrebbe avere il seguente aspetto:
// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
L'icona brandizzata viene creata in _BackdropTitle.. Contiene un Stack di icone animate: un menu inclinato e un diamante, avvolto in un IconButton in modo che possa essere premuto. Il IconButton viene quindi racchiuso in un SizedBox per fare spazio al movimento orizzontale dell'icona.
L'architettura "tutto è un widget" di Flutter consente di modificare il layout del AppBar predefinito senza dover creare un widget AppBar personalizzato completamente nuovo. Il parametro title, originariamente un widget Text, può essere sostituito con un _BackdropTitle più complesso. Poiché _BackdropTitle include anche l'icona personalizzata, sostituisce la proprietà leading, che ora può essere omessa. Questa semplice sostituzione del widget viene eseguita senza modificare nessuno degli altri parametri, come le icone delle azioni, che continuano a funzionare autonomamente.
Aggiungere una scorciatoia per tornare alla schermata di accesso
In backdrop.dart,aggiungi una scorciatoia per tornare alla schermata di accesso dalle due icone finali nella barra delle app: modifica le etichette semantiche delle icone per riflettere il loro nuovo scopo.
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: const Icon(
Icons.search,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
),
IconButton(
icon: const Icon(
Icons.tune,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
),
Se provi a ricaricare, verrà visualizzato un errore. Importa login.dart per correggere l'errore:
import 'login.dart';
Ricarica l'app e tocca i pulsanti di ricerca o sintonizzazione per tornare alla schermata di accesso.
9. Complimenti!
Nel corso di questi quattro codelab, hai imparato a utilizzare i componenti Material per creare esperienze utente uniche ed eleganti che esprimano la personalità e lo stile del brand.
Passaggi successivi
Questo codelab, MDC-104, completa questa sequenza di codelab. Puoi esplorare altri componenti in Material Flutter visitando il catalogo dei widget dei componenti Material.
Per un obiettivo ambizioso, prova a sostituire l'icona del brand con un'AnimatedIcon che si anima tra due icone quando lo sfondo diventa visibile.
Esistono molti altri codelab di Flutter da provare, in base ai tuoi interessi. Abbiamo un altro codelab specifico per Material che potrebbe interessarti: Building Beautiful Transitions with Material Motion for Flutter.






















