1. Wprowadzenie
Material Komponenty (MDC) pomagają deweloperom wdrażać interfejs Material Design. MDC, stworzona przez zespół inżynierów i projektantów UX w Google, zawiera dziesiątki pięknych i funkcjonalnych komponentów interfejsu. Jest dostępny na Androida, iOS, internet oraz Flutter.material.io/develop |
W ćwiczeniu w Codelabs MDC-103 udało Ci się dostosować kolor, wysokość, typografię i kształt komponentów Material Komponenty (MDC), aby nadać aplikacji styl.
Komponent w systemie Material Design wykonuje zestaw wstępnie zdefiniowanych zadań i ma pewną cechę, np. przycisk. Przycisk to jednak coś więcej niż tylko sposób na wykonanie działania – jest to także wizualny wyraz kształtu, rozmiaru i koloru, który sygnalizuje użytkownikowi, że jest interaktywny i że coś się stanie po jego dotknięciu lub kliknięciu.
Wytyczne Material Design opisują komponenty z punktu widzenia projektanta. Opisują one szeroki zakres podstawowych funkcji dostępnych na różnych platformach oraz elementy anatomiczne, które składają się na każdy komponent. Na przykład tło zawiera warstwę tylną i jej zawartość, warstwę przednią z zawartością, reguły ruchu i opcje wyświetlania. Każdy z nich można dostosować do potrzeb, zastosowania i zawartości danej aplikacji.
Co utworzysz
W ramach tego ćwiczenia w programie zmienisz interfejs aplikacji Shrine na dwupoziomową prezentację o nazwie „tło”. Tło zawiera menu z listą kategorii do wyboru używanych do filtrowania produktów wyświetlanych w asymetrycznej siatce. W ramach tego ćwiczenia w Codelabs będziesz wykorzystywać:
- Kształt
- Ruch
- widżety Flutter (używane w poprzednich ćwiczeniach z programowania);
Android | iOS |
Komponenty i podsystemy Material Flutter dostępne w tym ćwiczeniu z programowania
- Kształt
Jak oceniasz swój poziom doświadczenia w programowaniu w usłudze Flutter?
2. Konfigurowanie środowiska programistycznego Flutter
Aby ukończyć ten moduł, potrzebujesz 2 oprogramowania: pakietu SDK Flutter i edytora.
Ćwiczenie z programowania możesz uruchomić na dowolnym z tych urządzeń:
- Fizyczne urządzenie z Androidem lub iOS podłączone do komputera i ustawione w trybie programisty.
- Symulator iOS (wymaga zainstalowania narzędzi Xcode).
- Emulator Androida (wymaga skonfigurowania Android Studio).
- Przeglądarka (do debugowania wymagany jest Chrome).
- Aplikacja komputerowa w systemie Windows, Linux lub macOS Programowanie należy tworzyć na platformie, na której zamierzasz wdrożyć usługę. Jeśli więc chcesz opracować aplikację komputerową dla systemu Windows, musisz to zrobić w tym systemie, aby uzyskać dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania związane z konkretnymi systemami operacyjnymi, które zostały szczegółowo omówione na stronie docs.flutter.dev/desktop.
3. Pobierz aplikację startową w Codelabs
Przechodzisz z MDC-103?
Jeśli masz ukończone MDC-103, Twój kod powinien być gotowy do tego ćwiczenia z programowania. Przejdź do kroku: Dodawanie 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 skopiuj je z GitHuba
Aby skopiować to ćwiczenia z programowania 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
Otwieranie projektu i uruchamianie aplikacji
- Otwórz projekt w wybranym edytorze.
- Postępuj zgodnie z instrukcjami „Uruchom aplikację” w artykule Rozpocznij: jazdę próbną dla wybranego edytora.
Gotowe! Na urządzeniu powinna wyświetlić się strona logowania do Shrine z poprzednich ćwiczeń z programowania.
Android | iOS |
4. Dodaj menu tła
Tło jest wyświetlane za wszystkimi innymi treściami i komponentami. Składa się z 2 warstw: warstwy tylnej (zawierającej działania i filtry) oraz warstwy przedniej (zawierającej treść). W tle możesz wyświetlać interaktywne informacje i działania, np. elementy nawigacyjne czy filtry treści.
Usuwanie paska aplikacji ekranu głównego
Widżet strony głównej będzie treścią naszej warstwy frontowej. Obecnie zawiera 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 obiekt AsymmetricView:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
Dodawanie widżetu Tła
Utwórz widżet o nazwie Tło, który zawiera elementy frontLayer
oraz backLayer
.
backLayer
zawiera menu, które pozwala wybrać kategorię filtrowania listy (currentCategory
). Chcemy, aby wybór menu pozostawał trwały, więc ustawimy Tło jako widżet stanowy.
Dodaj do folderu /lib
nowy plik 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 jako required
. Jest to sprawdzona metoda w przypadku właściwości w konstruktorze, które nie mają wartości domyślnej i nie mogą mieć wartości null
, więc nie należy jej zapomnieć.
W definicji klasy tła 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 Scaffold z paskiem aplikacji (tak jak dawniej strona główna). Ale ciało Scaffold ma Stos. Elementy podrzędne stosu mogą się nakładać. Rozmiar i lokalizacja każdego elementu podrzędnego jest określana w odniesieniu do elementu nadrzędnego stosu.
Teraz dodaj instancję Tło do ShrineApp.
W usłudze 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 sekcji app.dart,
zmodyfikuj trasę /
, zwracając wartość Backdrop
, dla której HomePage
jest już funkcją 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 wyświetlić się nasza strona główna i pasek aplikacji:
Android | iOS |
Warstwa wsteczna pokazuje różowy obszar w nowej warstwie za stroną główną frontLayer.
Aby sprawdzić, czy Stos rzeczywiście ma kontener za stroną główną, możesz użyć Inspektora Flutter. Powinien wyglądać podobnie do tego:
Możesz teraz dostosować położenie obu warstw projektowaniem i treścią.
5. Dodaj kształt
W tym kroku określisz styl przedniej warstwy, aby dodać wycięcie w lewym górnym rogu.
W stylu Material Design ten typ dostosowania jest nazywany kształtem. Powierzchnie materiałowe mogą mieć dowolne kształty. Kształty dodają charakteru powierzchni i dopełniają ich styl. Można ich używać do budowania marki. Zwykłe prostokątne kształty można dostosowywać za pomocą zakrzywionych lub skośnych narożników i krawędzi oraz dowolnej liczby boków. Mogą być symetryczne lub nieregularne.
Dodawanie kształtu do warstwy początkowej
Ustawione pod kątem logo Shrine było inspiracją do nawiązania historii związanej z kształtem aplikacji Shrine. Historia kształtów to powszechny przypadek użycia kształtów, które są stosowane w różnych aplikacjach. Na przykład kształt logo jest odczytywany w elementach strony logowania, do których zastosowano kształt. W tym kroku nadasz styl przedniej warstwy, wycinając pod kątem 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()
funkcji _BackdropState zapakuj 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),
],
);
}
Załaduj ponownie.
Android | iOS |
Nadaliśmy główną powierzchnię świątyni niestandardowy kształt. Chcemy jednak, aby był to wizualnie pasek aplikacji.
Zmienianie koloru paska aplikacji
W app.dart
zmień funkcję _buildShrineTheme()
na:
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,
),
),
);
}
Ponowne uruchomienie z pamięci. Powinien pojawić się nowy, kolorowy pasek aplikacji.
Android | iOS |
Dzięki tej zmianie użytkownicy zauważą, że za przednią, białą warstwą Dodajmy ruch, aby użytkownicy widzieli tylną warstwę tła.
6. Dodaj ruch
Ruch to sposób na ożywienie aplikacji. Może być duży i dramatyczny, subtelny, minimalistyczny lub pomiędzy nimi. Pamiętaj jednak, że wybrany rodzaj ruchu powinien być odpowiedni do danej sytuacji. Ruch, który jest stosowany do powtarzających się, regularnych działań, powinien być niewielki i subtelny, aby nie rozpraszały użytkownika ani nie zajęły zbyt wiele czasu, gdy wykonuje się je regularnie. Są jednak sytuacje, gdy np. pierwsze uruchomienie aplikacji przez użytkownika może być bardziej atrakcyjne. Niektóre animacje mogą też informować użytkownika, jak korzystać z aplikacji.
Dodawanie ruchu odkrywania do przycisku menu
Na górze backdrop.dart
, poza zakresem żadnej klasy lub funkcji, dodaj stałą reprezentującą prędkość, jaką ma mieć animacja:
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
Dodaj widżet AnimationController do _BackdropState, utwórz jego wystąpienie w funkcji initState()
i usuń go 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)
Element AnimationController koordynuje animacje i udostępnia interfejs API umożliwiający odtwarzanie, cofanie i zatrzymywanie animacji. Teraz potrzebujemy funkcji, które pozwolą jej poruszać.
Dodaj funkcje, które określają i zmieniają widoczność warstwy frontowej:
// 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);
}
Opakuj warstwę wsteczną w widżecie ExcludeSemantics. Ten widżet wykluczy z drzewa semantyki pozycje menu BackLayer, gdy warstwa tylna nie będzie widoczna.
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
Zmień funkcję _buildStack(), aby zastosować BuildContext i BoxConstraints. Uwzględnij też efekt Positionedprzejść, w którym zastosowano 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 Scaffold, zwróć widżet LayoutBuilder, którego kreatorem jest _buildStack:
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
Przesunęliśmy kompilację stosu warstw przednich i tylnych do czasu użycia narzędzia LayoutBuilder, aby uwzględnić rzeczywistą ogólną wysokość tła. LayoutBuilder to specjalny widżet, którego wywołanie zwrotne kreatora udostępnia ograniczenia rozmiaru.
W funkcji build()
zmień ikonę menu wiodącego na pasku aplikacji w iconButton i używaj go, aby przełączać widoczność warstwy przedniej po naciśnięciu przycisku.
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
Załaduj ponownie, a następnie kliknij przycisk menu w symulatorze.
Android | iOS |
Warstwa przednia przesuwa się w dół (slajdy). Jeśli jednak spojrzysz w dół, zobaczysz czerwony błąd i błąd przepełnienia. Dzieje się tak, ponieważ animacja AsymmetricView zostaje ściśnięta i pomniejsza się, co z kolei zmniejsza ilość miejsca na kolumny. W efekcie kolumny nie mogą się rozmieścić z uwzględnieniem dostępnego miejsca, co powoduje błąd. Jeśli zastąpimy kolumny Kolumnymi obiektami ListView, rozmiar kolumny nie powinien się zmieniać.
Zawijanie kolumn produktów w widoku listy
W tabeli supplemental/product_columns.dart
zastąp kolumnę w elemencie OneProductCardColumn
elementem listy:
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, oznacz element reverse: true
. Kolejność wyświetlania dzieci jest odwrócona, aby zrekompensować tę zmianę.
Załaduj ponownie stronę i kliknij przycisk menu.
Android | iOS |
Szare ostrzeżenie o przepełnieniu na OneProductCardColumn zniknął(-a). Teraz zajmijmy się drugim.
W narzędziu supplemental/product_columns.dart
zmień sposób obliczania wartości imageAspectRatio
i zastąp kolumnę w elemencie TwoProductCardColumn
elementem widoku 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ż zabezpieczenia do aplikacji imageAspectRatio
.
Załaduj ponownie. Następnie kliknij przycisk menu.
Android | iOS |
Koniec z nadmiarami.
7. Dodawanie menu w warstwie tylnej
Menu to lista elementów tekstowych, które można kliknąć. Powiadomienia powiadamiają słuchaczy, gdy elementy tekstowe zostaną dotknięte. W tym kroku dodasz menu filtrowania kategorii.
Dodawanie menu
Dodaj menu do przedniej warstwy, a interaktywne przyciski 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 funkcja GestureDetector pakujący kolumnę, której elementy podrzędne są nazwami kategorii. Podkreślenie wskazuje wybraną kategorię.
W narzędziu app.dart
przekonwertuj widżet ShrineApp z bezstanowego na stanowy.
- Zaznacz:
ShrineApp.
- Pokaż działania dotyczące kodu w zależności od używanego IDE:
- Android Studio: naciśnij ⌥Enter (macOS) lub Alt + Enter
- VS Code: naciśnij ⌘. (macOS) lub Ctrl+.
- Wybierz „Konwertuj na StatefulWidget”.
- Zmień klasę ShrineAppState na prywatną (_ShrineAppState). Kliknij prawym przyciskiem myszy ShrineAppState,
- Android Studio: wybierz Refaktoryzacja > 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 po jej kliknięciu:
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
Następnie zmień tylną warstwę na CategoryMenuPage.
W programie app.dart
zaimportuj stronę CategoryMenu:
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 i pole currentCategory, aby przyjąć 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 stronę i kliknij przycisk Menu.
Android | iOS |
Po dotknięciu opcji menu nic się jeszcze nie dzieje. Zajmijmy się tym.
W funkcji home.dart
dodaj zmienną do pola Kategoria i przekaż ją do obiektu 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 app.dart
przekaż _currentCategory
dla frontLayer
:.
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),
Załaduj ponownie. Kliknij przycisk menu w symulatorze i wybierz kategorię.
Android | iOS |
Są filtrowane.
Zamknij przednią warstwę po wybraniu menu
W backdrop.dart
dodaj zastąpienie funkcji didUpdateWidget()
(wywoływanej po zmianie konfiguracji 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 aktywować ponowne wczytywanie z pamięci. Kliknij ikonę menu i wybierz kategorię. Menu powinno zamknąć się automatycznie i powinna wyświetlić się kategoria wybranych elementów. Teraz dodasz tę funkcję również do przedniej warstwy.
Przełącz warstwę przednią
W usłudze 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 obiekt gclidDetector do elementów podrzędnych _FrontLayer: Column's 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,
),
],
),
Następnie zaimplementuj nową właściwość onTap
z parametrem _BackdropState w funkcji _buildStack()
:
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
Załaduj ponownie stronę i kliknij górną część przedniej warstwy. Warstwa powinna otwierać się i zamykać za każdym razem, gdy dotykasz górnej części przedniej warstwy.
8. Dodaj ikonę marki
Ikona symbolizująca markę obejmuje też znane ikony. Spersonalizujmy ikonę odkrywania i połączmy ją z nazwą tytułu, aby nadać jej niepowtarzalny wygląd.
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ępuje zwykły widżet Text
dla parametru title
widżetu AppBar
. Zawiera animowaną ikonę menu oraz animowane przejścia między tytułami z przodu i z tyłu. Animowana ikona menu będzie korzystać z nowego komponentu. Odwołania do nowego dokumentu slanted_menu.png
należy dodać do pubspec.yaml
.
assets:
- assets/diamond.png
# TODO: Add slanted menu asset (104)
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
Usuń właściwość leading
w kreatorze AppBar
. Usunięcie ikony marki jest konieczne, aby została ona renderowana w miejsce oryginalnego widżetu leading
. Animacja listenable
oraz moduł obsługi onPress
ikony marki są przekazywane do _BackdropTitle
. Przekazujemy także identyfikatory frontTitle
i backTitle
, aby mogły być renderowane 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 obszarze _BackdropTitle.
. Zawiera ona animowane ikony (Stack
): skośne menu i romb, który jest zawinięty w element IconButton
, aby można było go nacisnąć. Następnie obiekt IconButton
zawija się w obiekt SizedBox
, aby zrobić miejsce na ruch ikony w poziomie.
Flutter – „Wszystko to jest widżet” architektura umożliwia zmianę układu domyślnego AppBar
bez konieczności tworzenia nowego niestandardowego widżetu AppBar
. Parametr title
, który był pierwotnie widżetem Text
, można zastąpić bardziej złożonym parametrem _BackdropTitle
. Element _BackdropTitle
zawiera też ikonę niestandardową, dlatego zastępuje właściwość leading
, którą można teraz pominąć. To proste zastępowanie widżetów jest możliwe bez zmiany innych parametrów, takich jak ikony działań, które działają samodzielnie.
Dodaj skrót z powrotem do ekranu logowania
W sekcji backdrop.dart,
dodaj skrót z powrotem do ekranu logowania z poziomu 2 końcowych ikon na pasku aplikacji: zmień etykiety semantyczne ikon, aby odzwierciedlić 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 załadować ponownie, pojawi się błąd. Aby naprawić błąd, zaimportuj plik login.dart
:
import 'login.dart';
Załaduj ponownie aplikację i kliknij przycisk wyszukiwania lub dostrajania, aby wrócić do ekranu logowania.
9. Gratulacje!
W trakcie tych 4 ćwiczeń w Codelabs wiesz, jak używać komponentów Material Komponenty do tworzenia wyjątkowych, eleganckich treści dla użytkowników, które odzwierciedlają osobowość i styl marki.
Dalsze kroki
To ćwiczenie w Codelabs (MDC-104) kończy tę sekwencję ćwiczeń z programowania. Jeszcze więcej komponentów w Material Flutter znajdziesz w katalogu widżetów Material Komponenty.
Jeśli chcesz osiągnąć ambitniejszy cel, spróbuj zastąpić ikonę marki elementem AnimatedIcon, który wyświetla się między 2 ikonami, gdy tło jest widoczne.
Do dyspozycji masz wiele innych ćwiczeń z programowania Flutter, które możesz wypróbować na podstawie swoich zainteresowań. Jeśli chcesz zapoznać się z innymi ćwiczeniami z programowania dotyczącymi Material Design, z którymi możesz się zapoznać: Tworzenie pięknych przejść z użyciem Material Motion dla Flutter.