MDC-104 Flutter: Material Advanced Komponenty

1. Wprowadzenie

logo_components_color_2x_web_96dp.png

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

aplikacja e-commerce w kolorze różowo-brązowym z górnym paskiem aplikacji i asymetryczną, przewijaną siatką pełną produktów w poziomie.

aplikacja e-commerce w kolorze różowo-brązowym z górnym paskiem aplikacji i asymetryczną, przewijaną siatką pełną produktów w poziomie.

lista menu 4 kategorie

lista menu 4 kategorie

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 Poziom średnio zaawansowany Biegły
.

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

  1. Otwórz projekt w wybranym edytorze.
  2. 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

Strona logowania do świątyni

Strona logowania do świątyni

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

Strona produktu świątyni na różowym tle

Strona produktu świątyni na różowym tle

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:

92ed338a15a074bd.png

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

Strona produktu dotycząca świątyni o niestandardowym kształcie

Strona produktu dotycząca świątyni o niestandardowym kształcie

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

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

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

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

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

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

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

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

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

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

Puste menu świątyń

Puste menu świątyń

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.

  1. Zaznacz: ShrineApp.
  2. Pokaż działania dotyczące kodu w zależności od używanego IDE:
  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,
  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ń 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

Menu świątynne z 4 kategoriami

Menu świątynne z 4 kategoriami

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

Strona produktu filtrowana przez świątynie

Strona produktu filtrowana przez świątynie

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

Strona produktu świąteczna z ikoną marki

Strona produktu świąteczna 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. 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.

Udało mi się ukończyć to ćwiczenia z programowania w rozsądny sposób i w rozsądny sposób

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

Chcę w przyszłości nadal używać komponentów Material Komponenty

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