MDC-104 Flutter: Material Advanced Komponenty

1. Wprowadzenie

logo_components_color_2x_web_96dp.png

Komponenty Material Design (MDC) pomagają deweloperom wdrażać Material Design. MDC to biblioteka stworzona przez zespół inżynierów i projektantów UX w Google. Zawiera dziesiątki pięknych i funkcjonalnych komponentów UI. Jest dostępna na Androida, iOS, web i 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 nie tylko sposób na wykonanie przez użytkownika jakiegoś działania. Jest to też wizualne wyrażenie kształtu, rozmiaru i koloru, które informuje użytkownika, że element jest interaktywny i że po jego dotknięciu lub kliknięciu nastąpi jakaś reakcja.

Wytyczne Material Design opisują komponenty z punktu widzenia projektanta. Zawierają one opisy wielu podstawowych funkcji dostępnych na różnych platformach oraz elementów anatomicznych, z których zbudowane są poszczególne komponenty. Na przykład tło zawiera warstwę tylną i jej zawartość, warstwę przednią i jej zawartość, reguły ruchu oraz opcje wyświetlania. Każdy z tych komponentów można dostosować do potrzeb, zastosowań i treści danej aplikacji.

Co utworzysz

W tym ćwiczeniu z programowania zmienisz interfejs aplikacji Shrine na dwupoziomową prezentację zwaną „tłem”. Tło zawiera menu z listą kategorii do wyboru używanych do filtrowania produktów wyświetlanych w asymetrycznej siatce. W tym ćwiczeniu będziesz używać:

  • Kształt
  • Ruch
  • widżety Flutter (używane w poprzednich ćwiczeniach z programowania);

Android

iOS

aplikacja e-commerce w kolorze różowo-brązowym z górnym paskiem aplikacji i asymetryczną siatką, którą można przewijać w poziomie, pełną produktów

aplikacja e-commerce w kolorach różowym i brązowym z górnym paskiem aplikacji i asymetryczną siatką na produkty, którą można przewijać poziomo

menu z 4 kategoriami

menu z 4 kategoriami

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?

Początkujący Średnio zaawansowany Zaawansowany

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 Android lub iOS połączone z komputerem i ustawione w trybie programisty.
  • Symulator iOS (wymaga zainstalowania narzędzi Xcode).
  • Emulator Androida (wymaga skonfigurowania w Android Studio).
  • Przeglądarka (do debugowania wymagany jest Chrome).
  • jako aplikacja na komputer z systemem Windows, Linux lub macOS; Programowanie należy tworzyć na platformie, na której zamierzasz wdrożyć usługę. Jeśli więc chcesz tworzyć aplikacje na komputery z systemem Windows, musisz to robić w Windowsie, aby uzyskać dostęp do odpowiedniego łańcucha kompilacji. Istnieją wymagania dotyczące poszczególnych systemów operacyjnych, które omówiono szczegółowo na stronie docs.flutter.dev/desktop.

3. Pobieranie aplikacji startowej ćwiczeń z programowania

Czy kontynuujesz z MDC-103?

Jeśli masz za sobą ukończenie kursu MDC-103, Twój kod powinien być gotowy do tego ćwiczenia z programowania. 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 skopiuj je z GitHuba

Aby skopiować to laboratorium programistyczne 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ę

  1. Otwórz projekt w wybranym edytorze.
  2. Wykonaj instrukcje „Uruchamianie aplikacji” opisane w artykule Pierwsze kroki: jazda testowa dla wybranego edytora.

Gotowe! Na urządzeniu powinna się wyświetlić strona logowania do Shrine z poprzednich laboratoriów kodu.

Android

iOS

Strona logowania do świątyni

Strona logowania do Shrine

4. Dodawanie menu tła

Tło jest wyświetlane za wszystkimi innymi treściami i komponentami. Składa się z 2 warstw: tylnej (wyświetlającej działania i filtry) oraz 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 głównej

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 pliku home.dart zmień funkcję build(), aby zwracała tylko element AsymmetricView:

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

Dodawanie widżetu Tło

Utwórz widget o nazwie Tło, który zawiera frontLayerbackLayer.

backLayer zawiera menu, które umożliwia wybranie kategorii do filtrowania listy (currentCategory). Ponieważ chcemy, aby wybrana opcja w menu była zachowana, utworzymy widget tła, który będzie przechowywać stan.

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ą być null, dlatego nie należy o nich zapominać.

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 element Scaffold z paskiem aplikacji, tak jak to robiła funkcja HomePage. Ale ciało Scaffold jest stosem. Elementy podrzędne grupy mogą się pokrywać. Rozmiar i lokalizacja każdego elementu podrzędnego jest określana w odniesieniu do elementu nadrzędnego stosu.

Teraz dodaj instancję tła 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ę strona główna i pasek aplikacji:

Android

iOS

Strona produktu Shrine z różowym tłem

Strona produktu Shrine z różowym tłem

Warstwowy element backLayer 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 on wyglądać mniej więcej tak:

92ed338a15a074bd.png

Możesz teraz dostosować zarówno wygląd, jak i zawartość warstw.

5. Dodawanie kształtu

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łów mogą mieć dowolne kształty. Kształty nadają powierzchniom charakter i styl oraz mogą służyć do wyrażania marki. Zwykłe prostokąty można dostosować, dodając zaokrąglone lub ukośne narożniki i krawędzie oraz dowolną liczbę boków. Mogą być symetryczne lub nieregularne.

Dodawanie kształtu do przedniej warstwy

Kształt logo aplikacji Shrine został zainspirowany kątem nachylenia logo Shrine. Kształt logo jest powtarzany w elementach strony logowania, do których został zastosowany. W tym kroku nadasz styl przedniej warstwy, wycinając pod kątem w lewym górnym rogu.

W pliku 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() obiektu _BackdropState owiń frontowy obiekt w obiekt _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),
      ],
    );
  }

Odśwież.

Android

iOS

Strona produktu Shrine o kształcie niestandardowym

Strona produktu Shrine o kształcie niestandardowym

Nadaliśmy głównej powierzchni aplikacji Shrine kształt niestandardowy. 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

Strona produktu dotycząca świątyni z kolorowym paskiem aplikacji

Strona produktu Shrine z kolorowym paskiem aplikacji

Dzięki tej zmianie użytkownicy zobaczą, że za przednią, białą warstwą Dodamy ruch, aby użytkownicy mogli zobaczyć tło 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 rodzaj ruchu powinien być odpowiedni do sytuacji. Ruchy stosowane w przypadku powtarzających się, regularnych działań powinny być małe i subtelne, aby nie rozpraszać użytkownika ani nie zajmować zbyt dużo czasu. 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

U góry funkcji backdrop.dart, poza zakresem jakiejkolwiek klasy lub funkcji, dodaj stałą wartość, która będzie reprezentować prędkość animacji:

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

AnimationController koordynuje animacje i zapewnia interfejs API do ich odtwarzania, odwracania i zatrzymywania. Teraz potrzebujemy funkcji, które sprawią, że się poruszy.

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

Zawijaj warstwę BackLayer w widżecie ExcludeSemantics. Gdy warstwa tła jest niewidoczna, widget ten wyklucza z drzewa semantycznego elementy menu warstwy tła.

    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 parametry BuildContext i BoxConstraints. Dodaj też pozycjonowaną animację przejścia, która 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 Scaffold, zwróć widżet LayoutBuilder, którego kreatorem jest _buildStack:

    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: LayoutBuilder(builder: _buildStack),
    );

Budowę zestawu warstw frontu i tyłu opóźniliśmy do momentu utworzenia układu za pomocą narzędzia LayoutBuilder, aby uwzględnić rzeczywistą wysokość tła. LayoutBuilder to specjalny widget, którego wywołanie zwrotne buildera określa ograniczenia rozmiaru.

W funkcji build() przekształc w przycisk ikony ikonę menu na pasku aplikacji, aby po jej kliknięciu można było włączać i wyłączać widoczność przedniej warstwy.

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

Puste menu świątyń z 2 błędami

Pusty panel menu świątyni z 2 błędami

Przednia warstwa przesuwa się w dół. Jeśli jednak spojrzysz w dół, zobaczysz czerwony błąd i błąd przepełnienia. Dzieje się tak, ponieważ widok AsymmetricView jest ściskany i staje się mniejszy przez tę animację, co z kolei powoduje, że kolumny mają mniej miejsca. W efekcie kolumny nie mogą się ułożyć w dostępnej przestrzeni i wywołują 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ę.

Odśwież stronę i kliknij przycisk menu.

Android

iOS

Puste menu świątyń z 1 błędem

Pusty panel menu świątyni z 1 błędem

Szare ostrzeżenie o przepełnieniu na OneProductCardColumn zniknąło! Teraz zajmijmy się drugim.

W funkcji supplemental/product_columns.dart zmień sposób obliczania funkcji imageAspectRatio i zastąp kolumnę w funkcji TwoProductCardColumn listą 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,
            ),
          ),
        ],
      );

Dodaliśmy też zabezpieczenia do aplikacji imageAspectRatio.

Załaduj ponownie. Następnie kliknij przycisk menu.

Android

iOS

Puste menu świątyń

Puste menu świątyń

Nie ma już przepełnień.

7. Dodawanie menu na 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 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 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.

  1. Zaznacz: ShrineApp.
  2. Na podstawie IDE wyświetl działania dotyczące kodu:
  3. Android Studio: naciśnij ⌥Enter (macOS) lub Alt + Enter
  4. VS Code: naciśnij ⌘ (macOS) lub Ctrl+.
  5. Wybierz „Konwertuj na StatefulWidget”.
  6. Zmień klasę ShrineAppState na prywatną (_ShrineAppState). Kliknij prawym przyciskiem myszy ShrineAppState i
  7. Android Studio: wybierz Refaktoryzacja > Zmień nazwę
  8. VS Code: wybierz Zmień nazwę symbolu
  9. 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ń warstwę tylną na stronę 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, 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'),
            ),

Odśwież stronę i kliknij przycisk Menu.

Android

iOS

Menu świątyni z 4 kategoriami

Menu świątynne z 4 kategoriami

Jeśli klikniesz opcję menu, nic się nie stanie… jeszcze. Zajmijmy się tym.

W funkcji home.dart dodaj zmienną dla pola Category i przekaż ją do widoku 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 funkcji app.dart prześlij wartość _currentCategory do funkcji frontLayer:

// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),

Odśwież. Kliknij przycisk menu w symulatorze i wybierz kategorię.

Android

iOS

Strona produktu w Shrine z filtrem

Strona produktu w ramach odfiltrowania

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 wywołać gorące przeładowanie. 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 warstwy czołowej.

Przełączanie warstwy przedniej

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 w funkcji _buildStack() zastosuj nową właściwość onTap do stanu _BackdropState:

          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. Za każdym razem, gdy klikniesz górną część przedniej warstwy, ta powinna się otworzyć i zamknąć.

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

Strona produktu Shrine z ikoną marki

Strona produktu Shrine z ikoną marki

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. 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. Do elementu pubspec.yaml należy dodać odwołanie do nowego elementu slanted_menu.png.

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 kreatorze AppBar. Usunięcie ikony marki jest niezbędne, aby została ona renderowana w miejscu oryginalnego widżetu leading. Animacja listenable i moduł obsługi onPress ikony z logo 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 element IconButton jest owijany w element SizedBox, aby zrobić miejsce na ruch ikony w poziomie.

Architektura Flutter „Wszystko jest widżetem” umożliwia zmianę układu domyślnego AppBar bez konieczności tworzenia zupełnie nowego niestandardowego widżetu AppBar. Parametr title, który był pierwotnie widżetem Text, można zastąpić bardziej złożonym parametrem _BackdropTitle. Ponieważ element _BackdropTitle zawiera też ikonę niestandardową, zastępuje on 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

backdrop.dart,dodaj skrót do ekranu logowania z 2 ostatnimi ikonami na pasku aplikacji: zmień semantyczne etykiety ikon, aby odzwierciedlały 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 modułów z programowania wiesz, jak używać komponentów Material Komponenty, aby tworzyć wyjątkowe, eleganckie treści, które oddają charakter i styl marki.

Dalsze kroki

To ćwiczenie MDC-104 zamyka sekwencję ćwiczeń. Aby poznać jeszcze więcej komponentów w Material Flutter, odwiedź katalog widżetów Material Components.

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.

W zależności od Twoich zainteresowań możesz wypróbować wiele innych ćwiczeń z programowania Fluttera. Jeśli chcesz zapoznać się z innymi ćwiczeniami z programowania dotyczącymi Material Design, z którymi warto się zapoznać: Tworzenie pięknych przejść z użyciem Material Motion dla Flutter.

Udało mi się ukończyć to ćwiczenie w rozsądnym czasie i z minimalnym wysiłkiem

Całkowicie się zgadzam Zgadzam się Nie mam zdania Nie zgadzam się Całkowicie się nie zgadzam

Chcę nadal używać komponentów Material Components

Zdecydowanie się zgadzam Zgadzam się Nie mam zdania Nie zgadzam się Zdecydowanie się nie zgadzam