1. Wprowadzenie
| Komponenty Material (MDC) pomagają deweloperom wdrażać Material Design. MDC zostało stworzone przez zespół inżynierów i projektantów UX w Google. Zawiera dziesiątki atrakcyjnych i funkcjonalnych komponentów interfejsu, które są dostępne na platformach Android, iOS, internetowej i Flutter.material.io/develop |
W samouczku MDC-103 dostosowaliśmy kolor, wysokość, typografię i kształt komponentów Material Design (MDC), aby nadać aplikacji styl.
Komponent w systemie Material Design wykonuje zestaw wstępnie zdefiniowanych zadań i ma określone cechy, np. przycisk. Przycisk to jednak nie tylko sposób na wykonanie działania przez użytkownika, ale też wizualne wyrażenie kształtu, rozmiaru i koloru, które informuje użytkownika, że jest interaktywny i że po dotknięciu lub kliknięciu coś się stanie.
Wskazówki dotyczące Material Design opisują komponenty z perspektywy projektanta. Opisują one szeroki zakres podstawowych funkcji dostępnych na różnych platformach oraz elementy anatomiczne, z których składa się każdy komponent. Na przykład tło zawiera warstwę tylną i jej zawartość, warstwę przednią i jej zawartość, reguły ruchu i opcje wyświetlania. Każdy z tych komponentów można dostosować do potrzeb, przypadków użycia i treści poszczególnych aplikacji.
Co utworzysz
W tym ćwiczeniu z programowania zmienisz interfejs aplikacji Shrine na dwupoziomową prezentację zwaną „tłem”. W tle znajduje się menu z kategoriami, które można wybrać, aby filtrować produkty wyświetlane w asymetrycznej siatce. W tym ćwiczeniu użyjesz tych narzędzi:
- Kształt
- Ruch
- widżety Fluttera (które były używane w poprzednich ćwiczeniach);
Android | iOS |
|
|
|
|
Komponenty i podsystemy Material Flutter w tym laboratorium
- Kształt
Jak oceniasz swój poziom doświadczenia w programowaniu w Flutterze?
2. Konfigurowanie środowiska programistycznego Fluttera
Aby ukończyć ten moduł, potrzebujesz 2 programów: pakietu SDK Flutter i edytora.
Codelab możesz uruchomić na dowolnym z tych urządzeń:
- fizyczne urządzenie z Android lub iOS podłączone do komputera i ustawione w trybie deweloperskim;
- Symulator iOS (wymaga zainstalowania narzędzi Xcode).
- Android Emulator (wymaga konfiguracji w Android Studio).
- przeglądarka (do debugowania wymagana jest Chrome);
- Jako aplikacja komputerowa na Windows, Linux lub macOS. Musisz tworzyć aplikację na platformie, na której zamierzasz ją wdrożyć. Jeśli chcesz opracować aplikację na komputery z systemem Windows, musisz to zrobić na komputerze z tym systemem, aby mieć dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania dotyczące poszczególnych systemów operacyjnych, które są szczegółowo opisane na stronie docs.flutter.dev/desktop.
3. Pobierz aplikację startową do ćwiczeń z programowania
Kontynuujesz naukę z MDC-103?
Jeśli udało Ci się ukończyć MDC-103, Twój kod powinien być gotowy do tego ćwiczenia. Przejdź do kroku Dodaj menu tła.
Zaczynasz od zera?
Aplikacja startowa znajduje się w katalogu material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.
...lub sklonuj go z GitHub
Aby skopiować ten codelab z GitHuba, uruchom te polecenia:
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
Otwórz projekt i uruchom aplikację
- Otwórz projekt w wybranym edytorze.
- Postępuj zgodnie z instrukcjami w sekcji „Uruchamianie aplikacji” w artykule Wprowadzenie: testowanie dotyczącym wybranego edytora.
Gotowe! Na urządzeniu powinna być widoczna strona logowania Shrine z poprzednich ćwiczeń.
Android | iOS |
|
|
4. Dodawanie menu tła
Tło pojawia się za całą inną zawartością i komponentami. Składa się z 2 warstw: tylnej (wyświetlającej działania i filtry) i przedniej (wyświetlającej treści). Możesz użyć tła, aby wyświetlać interaktywne informacje i działania, takie jak nawigacja czy filtry treści.
Usuwanie paska aplikacji Home
Widżet HomePage będzie treścią naszej warstwy przedniej. Obecnie ma pasek aplikacji. Przeniesiemy pasek aplikacji na tylną warstwę, a strona główna będzie zawierać tylko widok AsymmetricView.
W home.dart zmień funkcję build() tak, aby zwracała tylko AsymmetricView:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
Dodawanie widżetu Tło
Utwórz widżet o nazwie Tło, który zawiera elementy frontLayer i backLayer.
backLayer zawiera menu, które umożliwia wybranie kategorii do filtrowania listy (currentCategory). Ponieważ chcemy, aby wybór w menu był trwały, ustawimy Backdrop jako widżet stanowy.
Dodaj nowy plik do /lib o nazwie 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)
Zwróć uwagę, że niektóre właściwości oznaczamy symbolem required. Jest to sprawdzona metoda w przypadku właściwości w konstruktorze, które nie mają wartości domyślnej i nie mogą być wartością null, a więc nie należy o nich zapominać.
W definicji klasy Backdrop dodaj klasę _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(),
);
}
}
Funkcja build() zwraca element Scaffold z paskiem aplikacji, tak jak to robiła wcześniej strona HomePage. Element Scaffold jest jednak Stack. Elementy stosu mogą się nakładać. Rozmiar i lokalizacja każdego elementu podrzędnego są określane względem elementu nadrzędnego Stack.
Teraz dodaj instancję Backdrop do ShrineApp.
W języku app.dart zaimportuj backdrop.dart i 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';
W app.dart, zmodyfikuj trasę /, zwracając Backdrop, którego HomePage jest 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'),
),
Zapisz projekt. Powinna się wyświetlić strona główna i pasek aplikacji:
Android | iOS |
|
|
Warstwa backLayer pokazuje różowy obszar w nowej warstwie za stroną główną frontLayer.
Za pomocą inspektora Fluttera możesz sprawdzić, czy w widżecie Stack rzeczywiście znajduje się widżet Container za widżetem HomePage. Powinien on wyglądać podobnie do tego:

Możesz teraz dostosować projekt i treść obu warstw.
5. Dodawanie kształtu
W tym kroku zastosujesz styl do warstwy przedniej, aby dodać wycięcie w lewym górnym rogu.
W Material Design ten typ dostosowywania jest określany jako kształt. Powierzchnie materiałów mogą mieć dowolne kształty. Kształty podkreślają i nadają styl powierzchniom oraz mogą być używane do wyrażania marki. Zwykłe prostokąty można dostosowywać, nadając im zaokrąglone lub skośne rogi i krawędzie oraz dowolną liczbę boków. Mogą być symetryczne lub nieregularne.
Dodawanie kształtu do warstwy przedniej
Logo Shrine pod kątem zainspirowało historię kształtów w aplikacji Shrine. Historia kształtów to powszechne użycie kształtów w całej aplikacji. Na przykład kształt logo jest powtórzony w elementach strony logowania, do których zastosowano kształt. W tym kroku nadaj przedniej warstwie styl z ukośnym wycięciem w lewym górnym rogu.
W backdrop.dart dodaj nową klasę _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,
),
],
),
);
}
}
Następnie w funkcji _buildStack() w _BackdropState umieść przednią warstwę w _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),
],
);
}
Wczytaj ponownie.
Android | iOS |
|
|
Nadaliśmy głównej platformie Shrine niestandardowy kształt. Chcemy jednak, aby wizualnie pasował do paska aplikacji.
Zmiana koloru paska aplikacji
W polu app.dart zmień funkcję _buildShrineTheme() na tę:
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,
),
),
);
}
Uruchomienie z pamięci. Powinien się teraz pojawić nowy kolorowy pasek aplikacji.
Android | iOS |
|
|
Dzięki tej zmianie użytkownicy mogą zobaczyć, że za przednią białą warstwą coś się znajduje. Dodajmy ruch, aby użytkownicy mogli zobaczyć tylną warstwę tła.
6. Dodawanie ruchu
Ruch ożywia aplikację. Może być wyrazisty i dramatyczny, subtelny i minimalistyczny lub coś pomiędzy. Pamiętaj jednak, że rodzaj ruchu, którego używasz, powinien być odpowiedni do sytuacji. Ruch stosowany w przypadku powtarzających się, regularnych działań powinien być niewielki i subtelny, aby nie rozpraszać użytkownika ani nie zajmować zbyt dużo czasu. Są jednak odpowiednie sytuacje, np. pierwsze otwarcie aplikacji przez użytkownika, które mogą być bardziej przyciągające wzrok, a niektóre animacje mogą pomóc użytkownikowi w nauczeniu się korzystania z aplikacji.
Dodawanie animacji ujawniania do przycisku menu
U góry pliku backdrop.dart, poza zakresem dowolnej klasy lub funkcji, dodaj stałą reprezentującą prędkość, z jaką ma się poruszać animacja:
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
Dodaj widżet AnimationController do _BackdropState, utwórz jego instancję w funkcji initState() i usuń ją w funkcji dispose() stanu:
// 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)
Klasa AnimationController koordynuje animacje i udostępnia interfejs API do odtwarzania, odwracania i zatrzymywania animacji. Teraz potrzebujemy funkcji, które sprawią, że będzie się poruszać.
Dodaj funkcje, które określają i zmieniają widoczność warstwy przedniej:
// 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);
}
Umieść element backLayer w widżecie ExcludeSemantics. Ten widżet wyklucza elementy menu warstwy tylnej z drzewa semantycznego, gdy warstwa tylna jest niewidoczna.
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
Zmień funkcję _buildStack(), aby przyjmowała argumenty BuildContext i BoxConstraints. Dodaj też element PositionedTransition, który przyjmuje animację 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,
),
),
],
);
}
Na koniec zamiast wywoływać funkcję _buildStack dla treści widżetu Scaffold zwróć widżet LayoutBuilder, który używa funkcji _buildStack jako konstruktora:
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
Opóźniliśmy tworzenie stosu warstw przedniej i tylnej do czasu układu za pomocą LayoutBuilder, aby można było uwzględnić rzeczywistą wysokość tła. LayoutBuilder to specjalny widżet, którego wywołanie zwrotne konstruktora udostępnia ograniczenia rozmiaru.
W funkcji build() zmień ikonę menu głównego na pasku aplikacji na IconButton i użyj jej do przełączania widoczności warstwy przedniej po kliknięciu przycisku.
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
Załaduj ponownie, a potem kliknij przycisk menu w symulatorze.
Android | iOS |
|
|
Przednia warstwa animuje (przesuwa się) w dół. Ale jeśli spojrzysz w dół, zobaczysz czerwony błąd i błąd przepełnienia. Dzieje się tak, ponieważ animacja zmniejsza widok AsymmetricView, co z kolei ogranicza miejsce na kolumny. W końcu kolumny nie mogą się ułożyć w przydzielonym miejscu i powodują błąd. Jeśli zastąpimy kolumny widokami ListViews, rozmiar kolumny powinien pozostać taki sam podczas animacji.
Zawijanie kolumn produktów w widoku listy
W supplemental/product_columns.dart zastąp kolumnę w OneProductCardColumn widokiem 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,
),
],
);
}
}
Kolumna zawiera MainAxisAlignment.end. Aby rozpocząć układ od dołu, kliknij reverse: true. Aby zrekompensować tę zmianę, zamówienie dziecka zostanie wycofane.
Załaduj ponownie i kliknij przycisk menu.
Android | iOS |
|
|
Szare ostrzeżenie o przepełnieniu w komponencie OneProductCardColumn zniknęło. Teraz naprawmy drugą.
W supplemental/product_columns.dart zmień sposób obliczania imageAspectRatio i zastąp kolumnę w TwoProductCardColumn widokiem listy:
// 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,
),
),
],
);
Dodaliśmy też pewne zabezpieczenia do imageAspectRatio.
Wczytaj ponownie. Następnie kliknij przycisk menu.
Android | iOS |
|
|
Koniec przepełnień.
7. Dodawanie menu na tylnej warstwie
Menu to lista elementów tekstowych, w które można kliknąć. Gdy użytkownik kliknie element tekstowy, menu powiadamia o tym odbiorców. W tym kroku dodasz menu filtrowania kategorii.
Dodawanie menu
Dodaj menu do warstwy przedniej, a przyciski interaktywne do warstwy tylnej.
Utwórz nowy plik o nazwie 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()),
),
);
}
}
Jest to GestureDetector otaczający kolumnę, której elementami podrzędnymi są nazwy kategorii. Podkreślenie wskazuje wybraną kategorię.
W app.dart zmień widżet ShrineApp z bezstanowego na stanowy.
- Zaznacz:
ShrineApp. - W zależności od IDE wyświetl działania dotyczące kodu:
- Android Studio: naciśnij ⌥Enter (macOS) lub Alt + Enter.
- VS Code: naciśnij ⌘. (macOS) lub Ctrl+.
- Wybierz „Convert to StatefulWidget” (Przekształć w widget stanu).
- Zmień klasę ShrineAppState na prywatną (_ShrineAppState). Kliknij prawym przyciskiem myszy ShrineAppState i
- Android Studio: wybierz Refactor > Rename (Zmień nazwę).
- VS Code: wybierz Zmień nazwę symbolu
- Wpisz _ShrineAppState, aby ustawić klasę jako prywatną.
W app.dart dodaj zmienną do _ShrineAppState dla wybranej kategorii i wywołanie zwrotne, gdy zostanie ona kliknięta:
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
Następnie zmień warstwę tylną na CategoryMenuPage.
W app.dart zaimportuj 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';
W funkcji build() zmień pole backLayer na CategoryMenuPage, a pole currentCategory na zmienną instancji.
'/': (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'),
),
Załaduj ponownie i kliknij przycisk Menu.
Android | iOS |
|
|
Jeśli dotkniesz opcji menu, nic się nie stanie… jeszcze. Zajmijmy się tym.
W home.dart dodaj zmienną dla kategorii i przekaż ją do 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),
);
}
}
W usłudze app.dart przekaż _currentCategory dla frontLayer:
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),
Wczytaj ponownie. Kliknij przycisk menu w symulatorze i wybierz kategorię.
Android | iOS |
|
|
Są filtrowane.
Zamykanie przedniej warstwy po wybraniu opcji w menu
W backdrop.dart dodaj zastąpienie funkcji didUpdateWidget() (wywoływanej za każdym razem, gdy zmienia się konfiguracja widżetu) w _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);
}
}
Zapisz projekt, aby wywołać gorące przeładowanie. Kliknij ikonę menu i wybierz kategorię. Menu powinno zamknąć się automatycznie, a wybrana kategoria produktów powinna być widoczna. Teraz dodasz tę funkcję również do warstwy przedniej.
Przełączanie warstwy przedniej
W backdrop.dart dodaj wywołanie zwrotne po kliknięciu do warstwy tła:
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;
Następnie dodaj element GestureDetector do elementu podrzędnego _FrontLayer: Column's children:.
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,
),
],
),
Następnie zaimplementuj nową właściwość onTap w obiekcie _BackdropState w funkcji _buildStack():
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
Ponownie załaduj i kliknij górną część przedniej warstwy. Warstwa powinna otwierać się i zamykać za każdym razem, gdy klikniesz górną część przedniej warstwy.
8. Dodawanie ikony marki
Ikony marki obejmują też znane ikony. Spersonalizujmy ikonę odkrywania i połączmy ją z tytułem, aby uzyskać niepowtarzalny wygląd zgodny z marką.
Zmiana ikony przycisku menu
Android | iOS |
|
|
W backdrop.dart utwórz nową klasę _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 to widżet niestandardowy, który zastąpi zwykły widżet Text w przypadku parametru title widżetu AppBar. Ma animowaną ikonę menu i animowane przejścia między tytułami z przodu i z tyłu. Animowana ikona menu będzie korzystać z nowego komponentu. Odwołanie do nowego elementu slanted_menu.png musi zostać dodane do elementu pubspec.yaml.
assets:
- assets/diamond.png
# TODO: Add slanted menu asset (104)
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
Usuń usługę leading w narzędziu do tworzenia AppBar. Usunięcie jest konieczne, aby niestandardowa ikona marki była renderowana w miejscu oryginalnego widżetu leading. Animacja listenable i onPress moduł obsługi ikony marki są przekazywane do _BackdropTitle. Przekazywane są też wartości frontTitle i backTitle, aby można było je renderować w tytule tła. Parametr title elementu AppBar powinien wyglądać tak:
// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
Ikona marki jest tworzona w _BackdropTitle.. Zawiera Stack animowanych ikon: ukośne menu i diament, który jest otoczony IconButton, aby można było go nacisnąć. Element IconButton jest następnie umieszczany w elemencie SizedBox, aby zrobić miejsce na ruch ikony w poziomie.
Architektura Fluttera, w której „wszystko jest widżetem”, pozwala zmieniać układ domyślnego widżetu AppBar bez konieczności tworzenia zupełnie nowego widżetu AppBar. Parametr title, który pierwotnie jest widżetem Text, można zastąpić bardziej złożonym widżetem _BackdropTitle. Ponieważ _BackdropTitle zawiera też ikonę niestandardową, zastępuje właściwość leading, którą można teraz pominąć. Ta prosta zamiana widżetu odbywa się bez zmiany innych parametrów, takich jak ikony działań, które nadal działają samodzielnie.
Dodawanie skrótu do ekranu logowania
backdrop.dart,Dodaj skrót do ekranu logowania z 2 ikon na końcu paska aplikacji: zmień etykiety semantyczne ikon, aby odzwierciedlały ich nowe przeznaczenie.
// 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()),
);
},
),
Jeśli spróbujesz ponownie załadować stronę, pojawi się błąd. Aby naprawić ten błąd, zaimportuj login.dart:
import 'login.dart';
Ponownie wczytaj aplikację i kliknij przyciski wyszukiwania lub strojenia, aby wrócić do ekranu logowania.
9. Gratulacje!
W trakcie tych 4 warsztatów z programowania dowiedzieliśmy się, jak używać komponentów Material do tworzenia niepowtarzalnych, eleganckich interfejsów użytkownika, które odzwierciedlają osobowość i styl marki.
Dalsze kroki
To ćwiczenie, MDC-104, zamyka tę serię ćwiczeń. Więcej komponentów Material Flutter znajdziesz w katalogu widżetów komponentów Material.
Aby osiągnąć cel dodatkowy, spróbuj zastąpić ikonę marki elementem AnimatedIcon, który animuje się między dwiema ikonami, gdy tło staje się widoczne.
Możesz wypróbować wiele innych ćwiczeń z programowania w Flutterze, które są dostosowane do Twoich zainteresowań. Mamy też inne ćwiczenia z programowania dotyczące Material Design, które mogą Cię zainteresować: Building Beautiful Transitions with Material Motion for Flutter (Tworzenie pięknych przejść za pomocą Material Motion w Flutterze).






















