MDC-104 Flutter: רכיבים מתקדמים (Material Advanced)

1. מבוא

logo_components_color_2x_web_96dp.png

Material Components (MDC) עוזר למפתחים להטמיע Material Design. MDC נוצר על ידי צוות של מהנדסים ומעצבי חוויית המשתמש ב-Google, שכולל עשרות רכיבים יפים ופונקציונליים של ממשק המשתמש. זמין ל-Android, ל-iOS, לאינטרנט ול-Flutter.material.io/develop

ב-codelab MDC-103, התאמת אישית את הצבע, הגובה, הטיפוגרפיה והצורה של רכיבי החומר (MDC) כדי לעצב את האפליקציה.

רכיב במערכת Material Design מבצע סדרה של משימות מוגדרות מראש ויש לו מאפיינים מסוימים, כמו לחצן. עם זאת, לחצן הוא יותר מדרך למשתמש לבצע פעולה, הוא גם ביטוי חזותי של צורה, גודל וצבע, שמאפשר למשתמש לדעת שהוא אינטראקטיבי ושמשהו יקרה בעקבות נגיעה או לחיצה.

ההנחיות של Material Design מתארות רכיבים מנקודת מבטו של המעצב. הן מתארות מגוון רחב של פונקציות בסיסיות שזמינות בפלטפורמות שונות, ואת האלמנטים האנטומיים שמרכיבים כל רכיב. לדוגמה: רקע מכיל את השכבה האחורית ואת התוכן שלה, השכבה הקדמית והתוכן שלה, כללי התנועה ואפשרויות התצוגה. אפשר להתאים אישית כל אחד מהרכיבים האלו בהתאם לצרכים, לתרחישים לדוגמה ולתוכן של כל אפליקציה.

מה תפַתחו

ב-codelab הזה משנים את ממשק המשתמש באפליקציית ה-Shine למצגת דו-שלבית שנקראת Backdrop. הרקע כולל תפריט שבו מפורטות קטגוריות שניתן לבחור, שמשמשות לסינון המוצרים שמוצגים ברשת האסימטרית. ב-Codelab הזה משתמשים בדברים הבאים:

  • צורה
  • תנועה
  • ווידג'טים של Flutter (שהשתמשתם בהם ב-Codelabs הקודמים)

Android

iOS

אפליקציית מסחר אלקטרוני בהשראת ורוד וחום, עם סרגל אפליקציות עליון ורשת אסימטרית שאפשר לגלול בה בצורה אופקית, מלאה במוצרים

אפליקציית מסחר אלקטרוני בהשראת ורוד וחום, עם סרגל אפליקציות עליון ורשת אסימטרית שאפשר לגלול בה בצורה אופקית, מלאה במוצרים

תפריט שכולל 4 קטגוריות

תפריט שכולל 4 קטגוריות

רכיבים ומערכות משנה של Material Flutter ב-Codelab הזה

  • צורה

איזה דירוג מגיע לדעתך לרמת הניסיון שלך בפיתוח Flutter?

מתחילים בינונית בקיאים

2. הגדרת סביבת הפיתוח של Flutter

כדי להשלים את שיעור ה-Lab הזה אתם צריכים שתי תוכנות: Flutter SDK וכלי עריכה.

אפשר להריץ את Codelab באמצעות כל אחד מהמכשירים הבאים:

  • מכשיר פיזי שמשמש ל-Android או ל-iOS שמחובר למחשב ומוגדר ל'מצב פיתוח'.
  • הסימולטור של iOS (צריך להתקין כלים של Xcode).
  • האמולטור של Android (נדרשת הגדרה ב-Android Studio).
  • דפדפן (Chrome נדרש לניפוי באגים).
  • בתור אפליקציית Windows , Linux או macOS למחשב. צריך לפתח בפלטפורמה שבה אתם מתכננים לפרוס. לכן, כדי לפתח אפליקציה למחשב של Windows, צריך לפתח את האפליקציה ב-Windows כדי לגשת לשרשרת ה-build המתאימה. יש דרישות ספציפיות למערכת ההפעלה שמפורטות בהרחבה בכתובת docs.flutter.dev/desktop.

3. הורדת האפליקציה לתחילת העבודה של Codelab

ממשיכים לעבור מ-MDC-103?

אם השלמתם את MDC-103, הקוד אמור להיות מוכן ל-Codelab הזה. דלגו לשלב: מוסיפים את התפריט של ׳רקע׳.

מתחילים מאפס?

האפליקציה לתחילת הפעולה נמצאת בספרייה material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.

...או לשכפל אותו מ-GitHub

כדי לשכפל את ה-Codelab הזה מ-GitHub, מריצים את הפקודות הבאות:

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

פותחים את הפרויקט ומפעילים את האפליקציה

  1. פותחים את הפרויקט בכלי עריכה לבחירתכם.
  2. פועלים לפי ההוראות ל'הפעלת האפליקציה'. בקטע שנתחיל?: נסיעת מבחן בכלי העריכה שבחרתם.

הצלחת! אתם אמורים לראות במכשיר את דף ההתחברות ל-Shine מ-Codelabs הקודמות.

Android

iOS

דף ההתחברות למקדש

דף ההתחברות למקדש

4. הוספת תפריט הרקע

רקע מופיע מאחורי כל התוכן והרכיבים האחרים. היא מורכבת משתי שכבות: שכבה אחורית (להצגת פעולות ומסננים) ושכבה קדמית (להצגת תוכן). אפשר להשתמש ברקע כדי להציג פעולות ומידע אינטראקטיביים, כמו מסנני ניווט או תוכן.

הסרת הסרגל באפליקציית דף הבית

הווידג'ט של דף הבית יהיה התוכן של השכבה הקדמית שלנו. כרגע יש בו סרגל אפליקציות. נעביר את סרגל האפליקציה לשכבה האחורית ובדף הבית יכלול רק את AsymmetricView.

ב-home.dart, משנים את הפונקציה build() כך שתחזיר רק את ה-AsymmetricView:

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

הוספת הווידג'ט של הרקע

יוצרים ווידג'ט בשם Backdrop שכולל את השדות frontLayer ואת backLayer.

backLayer כולל תפריט שמאפשר לבחור קטגוריה לסינון הרשימה (currentCategory). מכיוון שאנחנו רוצים שבחירת התפריט תישאר קבועה, נהפוך את 'רקע' לווידג'ט עם שמירת מצב.

הוספת קובץ חדש אל /lib בשם 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)

שימו לב שאנחנו מסמנים מאפיינים מסוימים בתור required. זוהי שיטה מומלצת למאפיינים ב-constructor שאין להם ערך ברירת מחדל והם לא יכולים להיות null, ולכן אסור לשכוח אותם.

בהגדרת המחלקה Backdrop, מוסיפים את המחלקה _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(),
    );
  }
}

הפונקציה build() מחזירה Scaffold עם סרגל אפליקציות, בדיוק כמו שנעשה שימוש ב-HomePage. אבל הגוף של Scaffold הוא מקבץ. צאצאים של מקבץ יכולים להיות חופפים. הגודל והמיקום של כל ילד או ילדה מצוינים ביחס להורה של המקבץ.

עכשיו מוסיפים מופע Backdrop ל-ShrineApp.

ב-app.dart, מייבאים את backdrop.dart ואת 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';

ב-app.dart, משנים את המסלול / על ידי החזרת Backdrop ש-HomePage הוא 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'),
),

שומרים את הפרויקט. אתם אמורים לראות שדף הבית שלנו מופיע וגם סרגל האפליקציות:

Android

iOS

דף מוצר של מקדש עם רקע ורוד

דף מוצר של מקדש עם רקע ורוד

השכבה האחורית (backLayer) מציגה את האזור הוורוד בשכבה חדשה מאחורי דף הבית של ה-חזית.

תוכלו להשתמש בכלי הבדיקה כדי לוודא שבמקבץ אכן יש קונטיינר מאחורי דף הבית. היא אמורה להיראות כך:

92ed338a15a074bd.png

עכשיו אפשר לשנות את שתי השכבות לעיצוב ולתוכן.

5. הוספת צורה

בשלב הזה, תעצב את השכבה הקדמית כדי להוסיף חיתוך בפינה הימנית העליונה.

עיצוב חדשני (Material Design) מתייחס להתאמה אישית מהסוג הזה בתור צורה. משטחים חומרים יכולים להיות בעלי צורות שרירותיות. צורות מוסיפות הדגשה וסגנון למשטחים וניתן להשתמש בהן כדי לבטא את המיתוג. אפשר להתאים אישית צורות מלבניות רגילות עם פינות וקצוות עקומים או זוויתיים וכל מספר של צלעות. הם יכולים להיות סימטריים או לא סדירים.

הוספת צורה לשכבה הקדמית

לוגו המקדש הזוויתי נתן השראה לסיפור הצורה של אפליקציית Shrine. סיפור צורה הוא השימוש הנפוץ בצורות שמיושמות באפליקציה. לדוגמה, צורת הלוגו נאמרת ברכיבים של דף ההתחברות שהוחלה עליהם צורה. בשלב הזה, מעצבים את השכבה הקדמית באמצעות חיתוך זוויתי בפינה הימנית העליונה.

ב-backdrop.dart, מוסיפים מחלקה חדשה _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,
          ),
        ],
      ),
    );
  }
}

לאחר מכן, בפונקציה _buildStack() של _BackdropState, ממלאים את השכבה הקדמית ב- _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),
      ],
    );
  }

טעינה מחדש.

Android

iOS

דף מוצר של מקדש עם צורה מותאמת אישית

דף מוצר של מקדש עם צורה מותאמת אישית

החלטנו ליצור צורה מותאמת אישית למשטח הראשי של מקדש. עם זאת, אנחנו רוצים שהוא יתחבר באופן חזותי לסרגל האפליקציות.

שינוי הצבע של סרגל האפליקציות

ב-app.dart, משנים את הפונקציה _buildShrineTheme() לאפשרות הבאה:

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

הפעלה מחדש מתוך הזיכרון. סרגל האפליקציות הצבעוני החדש אמור להופיע.

Android

iOS

דף המוצר במקדש עם סרגל צבעוני באפליקציה

דף המוצר במקדש עם סרגל צבעוני באפליקציה

בשל השינוי הזה, המשתמשים יכולים לראות שיש משהו ממש מאחורי השכבה הלבנה הקדמית. כדאי להוסיף תנועה כדי שהמשתמשים יוכלו לראות את שכבת הגב של הרקע.

6. הוספת תנועה

תנועה היא דרך להפיח חיים באפליקציה. היא יכולה להיות גדולה ודרמטית, עדינה ומינימלית, או בכל מקום בין. עם זאת, חשוב לזכור שסוג התנועה שבו אתם משתמשים צריך להתאים לסיטואציה. תנועה שמיושמת בפעולות חוזרות ונשנות, צריכה להיות קטנה ועדינה, כך שהפעולות לא יסיחו את דעת המשתמש או יימשכו זמן רב מדי על בסיס קבוע. אבל יש מצבים מתאימים, כמו הפעם הראשונה שבה משתמש פותח אפליקציה, שעשויים למשוך את העין יותר, ואנימציות מסוימות יכולות ללמד את המשתמשים איך להשתמש באפליקציה.

הוספה של תנועת חשיפה ללחצן התפריט

בחלק העליון של הערך backdrop.dart, מחוץ להיקף של כל מחלקה או פונקציה, מוסיפים קבוע כדי לייצג את המהירות שאנחנו רוצים שתהיה באנימציה שלנו:

// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;

מוסיפים את הווידג'ט AnimationController אל _BackdropState, יוצרים אותו בפונקציה initState() ומסירים אותו בפונקציה dispose() של המצב:

  // 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 מארגנים את האנימציות ונותנים לכם API כדי להפעיל, להפוך את האנימציה ולהפסיק אותה. עכשיו אנחנו צריכים פונקציות שיגרמו לו לזוז.

הוספת פונקציות שקובעות וגם משנות את החשיפה של השכבה הקדמית:

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

גלישת את השכבה האחורית בווידג'ט ExcludeSemantics. הווידג'ט הזה לא יכלול את פריטי התפריט של השכבה האחורית מעץ הסמנטיקה כשהשכבה האחורית לא גלויה.

    return Stack(
      key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        ExcludeSemantics(
          child: widget.backLayer,
          excluding: _frontLayerVisible,
        ),
      ...

משנים את הפונקציה _buildStack() בשביל להשתמש ב-BuildContext וב-BoxConstraints. כמו כן, הוסיפו מעבר מבוסס-מיקום שמבצע אנימציה יחסית של יחסי גובה-רוחב:

  // 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,
          ),
        ),
      ],
    );
  }

לסיום, במקום לקרוא לפונקציה _buildStack עבור גוף ה-Scaffold, אתם צריכים להחזיר את הווידג'ט LayoutBuilder שמבוסס על _buildStack בתור ה-builder שלו:

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

דחינו את הבנייה של סטאק השכבה הקדמית/אחורית עד לזמן הפריסה באמצעות LayoutBuilder כדי שנוכל לשלב את הגובה הכולל של הרקע בפועל. LayoutBuilder הוא ווידג'ט מיוחד שהקריאה החוזרת שלו ל-builder מספקת מגבלות גודל.

בפונקציה build(), אפשר להפוך את סמל התפריט הראשי בסרגל האפליקציות ל-iconButton, ולהשתמש בו כדי להציג או להסתיר את השכבה הקדמית כשמקישים על הלחצן.

      // TODO: Replace leading menu icon with IconButton (104)
      leading: IconButton(
        icon: const Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

טוענים מחדש ומקישים על לחצן התפריט בסימולטור.

Android

iOS

תפריט ריק של מקדש עם שתי שגיאות

תפריט ריק של מקדש עם שתי שגיאות

השכבה הקדמית מציעה אנימציה (החלקה) למטה. אבל אם תסתכלו למטה, יש שגיאה אדומה ושגיאת גלישה. הסיבה לכך היא שהאנימציה הזו מצמצמת את התצוגה של AsymmetricView ומקטינה את התצוגה, וכתוצאה מכך יש פחות מקום לעמודות. בסופו של דבר, העמודות לא יכולות להתפרס על פני השטח הנתון, והן מובילות לשגיאה. אם נחליף את העמודות ב-ListViews, גודל העמודות אמור להישאר כפי שהן מופיעות באנימציה.

גלישת עמודות של מוצרים בתצוגת ListView

ב-supplemental/product_columns.dart, מחליפים את העמודה OneProductCardColumn ב-ListView:

class OneProductCardColumn extends StatelessWidget {
  const OneProductCardColumn({required this.product, Key? key}) : super(key: key);

  final Product product;

  @override
  Widget build(BuildContext context) {
    // TODO: Replace Column with a ListView (104)
    return ListView(
      physics: const ClampingScrollPhysics(),
      reverse: true,
      children: <Widget>[
        ConstrainedBox(
          constraints: const BoxConstraints(
            maxWidth: 550,
          ),
          child: ProductCard(
            product: product,
          ),
        ),
        const SizedBox(
          height: 40.0,
        ),

      ],
    );
  }
}

העמודה כוללת את MainAxisAlignment.end. כדי להתחיל את הפריסה מתחתית המסך, צריך לסמן reverse: true. סדר הילדים בוטל כדי לפצות על השינוי.

טוענים מחדש ומקישים על לחצן התפריט.

Android

iOS

התפריט &#39;ריקון&#39; ריק עם שגיאה אחת

התפריט &#39;ריקון&#39; ריק עם שגיאה אחת

האזהרה על גלישה אפורה ב-OneProductCardColumn בוטלה. עכשיו נתקן את הבעיה השנייה.

ב-supplemental/product_columns.dart, משנים את אופן החישוב של imageAspectRatio ומחליפים את העמודה ב-TwoProductCardColumn ב-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,
            ),
          ),
        ],
      );

הוספנו גם מידה מסוימת של בטיחות ל-imageAspectRatio.

טעינה מחדש. לאחר מכן מקישים על לחצן התפריט.

Android

iOS

התפריט &#39;ריקון המקדש&#39;

התפריט &#39;ריקון המקדש&#39;

לא עוד חריגות.

7. הוספת תפריט בשכבה האחורית

תפריט הוא רשימה של פריטי טקסט שאפשר להקיש עליהם כדי לקבל התראות כשנוגעים לפריטי הטקסט. בשלב הזה, תוסיפו תפריט לסינון לפי קטגוריות.

מוסיפים את התפריט

מוסיפים את התפריט לשכבה הקדמית ואת הלחצנים האינטראקטיביים לשכבה האחורית.

יוצרים קובץ חדש בשם 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()),
      ),
    );
  }
}

מדובר ב-GestureDetector שעוטף עמודה ששמות הצאצאים שלה הם שמות הקטגוריות. קו תחתון משמש לציון הקטגוריה שנבחרה.

ב-app.dart, צריך להמיר את הווידג'ט ShrineApp מ'ללא שמירת מצב' ל'מצב'.

  1. הדגשה של ShrineApp.
  2. על סמך סביבת הפיתוח המשולבת (IDE), עליך להציג את פעולות הקוד:
  3. Android Studio: מקישים על ⌥ Enter (macOS) או על Alt + Enter
  4. קוד VS: מקישים על ⌘. (macOS) או Ctrl+.
  5. בוחרים באפשרות 'המרה ל-StatefulWidget'.
  6. משנים את המחלקה ShrineAppState ל'פרטי' ( _ShrineAppState). לוחצים לחיצה ימנית על ShrineAppState,
  7. Android Studio: בוחרים באפשרות 'ארגון מחדש' > שנה שם
  8. VS Code: בוחרים באפשרות 'שינוי שם הסמל'
  9. כדי להגדיר את הכיתה כפרטית, צריך להזין _ShrineAppState.

ב-app.dart, מוסיפים משתנה ל- _ShrineAppState בשביל הקטגוריה שנבחרה, וקריאה חוזרת (callback) כשמקישים עליה:

class _ShrineAppState extends State<ShrineApp> {
  Category _currentCategory = Category.all;

  void _onCategoryTap(Category category) {
    setState(() {
      _currentCategory = category;
    });
  }

לאחר מכן משנים את השכבה האחורית ל-CategoryתפריטPage.

ב-app.dart, מייבאים את CategorymenuPage:

import 'backdrop.dart';
import 'category_menu_page.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';

בפונקציה build(), משנים את השדה BackLayer (backLayer) ל-CategoryActivityPage ואת השדה currentCategory (קטגוריה) כדי לקחת את משתנה המופע.

'/': (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'),
            ),

טוענים מחדש ומקישים על לחצן התפריט.

Android

iOS

תפריט של מקדש עם 4 קטגוריות

תפריט של מקדש עם 4 קטגוריות

אם מקישים על אפשרות בתפריט, כלום לא קורה... יחד נפתור את זה.

בפונקציה home.dart, מוסיפים משתנה לקטגוריה ומעבירים אותו ל-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),
    );
  }
}

בapp.dart, מעבירים את _currentCategory של frontLayer:.

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

טעינה מחדש. מקישים על לחצן התפריט בסימולטור ובוחרים קטגוריה.

Android

iOS

דף מוצר שעבר סינון

דף מוצר שעבר סינון

הם מסוננים!

סגירת השכבה הקדמית אחרי בחירה בתפריט

ב-backdrop.dart, מוסיפים שינוי מברירת המחדל לפונקציה didUpdateWidget() (שמתרחשת בכל פעם שתצורת הווידג'ט משתנה) ב-_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);
    }
  }

צריך לשמור את הפרויקט כדי להפעיל טעינה מחדש מתוך הזיכרון. מקישים על סמל התפריט ובוחרים קטגוריה. התפריט אמור להיסגר באופן אוטומטי אמורה להופיע הקטגוריה של הפריטים שנבחרו. עכשיו נוסיף את הפונקציונליות הזו גם לשכבה הקדמית.

החלפת המצב של השכבה הקדמית

ב-backdrop.dart, מוסיפים קריאה חוזרת (callback) בהקשה לשכבת הרקע:

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;

לאחר מכן מוסיפים TrafficDetector ברמת הצאצא של _FrontLayer: הצאצאים של 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,
          ),
        ],
      ),

לאחר מכן מטמיעים את המאפיין onTap החדש ב- _BackdropState בפונקציה _buildStack():

          PositionedTransition(
            rect: layerAnimation,
            child: _FrontLayer(
              // TODO: Implement onTap property on _BackdropState (104)
              onTap: _toggleBackdropLayerVisibility,
              child: widget.frontLayer,
            ),
          ),

טוענים מחדש ומקישים על החלק העליון של השכבה הקדמית. השכבה אמורה להיפתח ולהיסגר בכל פעם שמקישים על החלק העליון של השכבה הקדמית.

8. הוספת סמל ממותג

איקונוגרפיה ממותגת כוללת גם סמלים מוכרים. עכשיו נשנה את סמל החשיפה בהתאמה אישית ונמזג אותו עם הכותרת כדי ליצור מראה ייחודי וממותג.

שינוי הסמל של לחצן התפריט

Android

iOS

דף מוצר של מקדש עם סמל ממותג

דף מוצר של מקדש עם סמל ממותג

בכיתה backdrop.dart, יוצרים כיתה חדשה – _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 הוא ווידג'ט בהתאמה אישית שיחליף את הווידג'ט הפשוט Text של הפרמטר title של הווידג'ט AppBar. יש בו סמל תפריט מונפש ואנימציה של מעברים בין הכותרת הקדמית והאחורית. בסמל התפריט עם האנימציה יופיע נכס חדש. יש להוסיף את ההפניה אל slanted_menu.png החדש אל pubspec.yaml.

assets:
    - assets/diamond.png
    # TODO: Add slanted menu asset (104)
    - assets/slanted_menu.png
    - packages/shrine_images/0-0.jpg

צריך להסיר את הנכס leading ב-builder של AppBar. נדרשת הסרה של הסמל הממותג המותאם אישית כדי שיוצג במקום הווידג'ט המקורי של leading. האנימציה listenable וה-handler של onPress של הסמל הממותג מועברים אל _BackdropTitle. גם frontTitle ו-backTitle מועברים כדי שניתן יהיה לעבד אותם בתוך שם הרקע. הפרמטר title של AppBar אמור להיראות כך:

// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
  listenable: _controller.view,
  onPress: _toggleBackdropLayerVisibility,
  frontTitle: widget.frontTitle,
  backTitle: widget.backTitle,
),

הסמל הממותג נוצר ב-_BackdropTitle. הוא מכיל Stack של סמלים מונפשים: תפריט משופע ויהלום, שעטוף בIconButton כדי שניתן יהיה ללחוץ עליו. לאחר מכן, IconButton עטוף בתוך SizedBox כדי לפנות מקום לתנועת הסמל האופקי.

"כל דבר הוא ווידג'ט" של Flutter היא מאפשרת לשנות את פריסת ברירת המחדל של AppBar בלי ליצור ווידג'ט AppBar חדש לגמרי בהתאמה אישית. אפשר להחליף את הפרמטר title, שהוא במקור ווידג'ט Text, ברכיב _BackdropTitle מורכב יותר. מאחר ש-_BackdropTitle כולל גם את הסמל המותאם אישית, הוא מחליף את המאפיין leading, שעכשיו ניתן להשמיט. ניתן להחליף את הווידג'ט הפשוט הזה בלי לשנות אף אחד מהפרמטרים האחרים, כמו סמלי הפעולות, שממשיכים לפעול בעצמם.

הוספת קיצור דרך חזרה למסך ההתחברות

ב-backdrop.dart,מוסיפים קיצור דרך חזרה למסך ההתחברות משני הסמלים האחרונים בסרגל האפליקציה: אפשר לשנות את התוויות הסמנטיות של הסמלים כך שישקפו את המטרה החדשה שלהם.

        // 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()),
            );
          },
        ),

אם תנסו לטעון מחדש, תקבלו הודעת שגיאה. צריך לייבא את login.dart כדי לתקן את השגיאה:

import 'login.dart';

טוענים מחדש את האפליקציה ומקישים על לחצני החיפוש או הכוונון כדי לחזור למסך ההתחברות.

9. מעולה!

במהלך ארבעת ה-Codelabs האלה, למדתם איך להשתמש ברכיבי Material Material כדי ליצור חוויות משתמש ייחודיות ואלגנטיות שמשקפות את האישיות והסגנון של המותג.

השלבים הבאים

ה-Codelab הזה, MDC-104, מסיים את הרצף הזה של Codelabs. ניתן לגלות עוד רכיבים ב-Material Flutter על ידי ביקור בקטלוג הווידג'טים של רכיבי החומר.

אם רוצים למתוח את היעד, כדאי להחליף את הסמל הממותג ב-AnimatedIcon שמכיל אנימציה בין שני סמלים כשתמונת הרקע מוצגת.

יש עוד המון Flutter codelabs שתוכלו לנסות, בהתאם לתחומי העניין שלכם. יש לנו עוד Codelab שספציפי לחומר שיכול לעניין אותך: בניית מעברים יפים באמצעות Material Motion for Flutter.

הצלחתי להשלים את ה-Codelab הזה תוך השקעה של זמן ומאמץ סבירים

נכון מאוד נכון נייטרלי לא נכון לא נכון בכלל

ארצה להמשיך להשתמש ברכיבי Material Materials בעתיד

נכון מאוד נכון נייטרלי לא נכון לא נכון בכלל