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

1. מבוא

logo_components_color_2x_web_96dp.png

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

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

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

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

מה תפַתחו

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

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

Android

iOS

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

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

קטגוריות בתפריט 4

קטגוריות בתפריט 4

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

  • צורה

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

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

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

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

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

  • מכשיר פיזי עם Android או iOS שמחובר למחשב ומוגדר למצב פיתוח.
  • סימולטור iOS (נדרשת התקנה של כלי Xcode).
  • אמולטור Android (נדרשת הגדרה ב-Android Studio).
  • דפדפן (חובה להשתמש ב-Chrome לצורך ניפוי באגים).
  • כאפליקציה למחשב Windows,‏ Linux או macOS. אתם צריכים לפתח בפלטפורמה שבה אתם מתכננים לבצע פריסה. לכן, אם רוצים לפתח אפליקציה למחשב שולחני עם Windows, צריך לפתח ב-Windows כדי לגשת לשרשרת הבנייה המתאימה. יש דרישות ספציפיות למערכות הפעלה שמוסברות בפירוט במאמר 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. פועלים לפי ההוראות שבקטע תחילת העבודה: ניסיון נסיעה כדי להפעיל את האפליקציה בעורך שבחרתם.

הצלחת! בדף הכניסה של Shrine אמור להופיע במכשיר שלכם קוד שנוצר ב-codelab הקודם.

Android

iOS

דף ההתחברות של Shrine

דף ההתחברות של Shrine

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

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

הסרת סרגל האפליקציות במסך הבית

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

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

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

הוספת הווידג'ט Backdrop

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

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

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

בקטע Backdrop class definition, מוסיפים את המחלקה _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 הוא Stack. האלמנטים הצאצאים של Stack יכולים לחפוף. הגודל והמיקום של כל רכיב צאצא נקבעים ביחס לרכיב האב Stack.

עכשיו מוסיפים מופע של 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

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

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

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

אפשר להשתמש בכלי Flutter Inspector כדי לוודא שרכיב ה-Stack כולל רכיב Container מאחורי רכיב ה-HomePage. הוא אמור להיראות כך:

92ed338a15a074bd.png

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

5. הוספת צורה

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

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

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

הלוגו של Shrine, שמופיע בזווית, היווה השראה לסיפור הצורות באפליקציית 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

דף מוצר של מזבח עם צורה בהתאמה אישית

דף מוצר של מזבח עם צורה בהתאמה אישית

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

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

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

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

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

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

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

משנים את הפונקציה _buildStack()‎ כך שתקבל BuildContext ו-BoxConstraints. בנוסף, כוללים PositionedTransition שמקבל אנימציה מסוג 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,
          ),
        ),
      ],
    );
  }

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

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

עיכבנו את בניית ערימת השכבות הקדמית/האחורית עד לזמן הפריסה באמצעות LayoutBuilder, כדי שנוכל לשלב את הגובה הכולל בפועל של הרקע. ‫LayoutBuilder הוא ווידג'ט מיוחד שהקריאה החוזרת (callback) שלו מספקת אילוצי גודל.

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

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

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

Android

iOS

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

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

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

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

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

האזהרה על גלישת תוכן באפור ב-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

תפריט ריק של מקדש

תפריט ריק של מקדש

לא יהיו יותר הצפות.

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 שעוטף רכיב Column שהרכיבים הצאצא שלו הם שמות הקטגוריות. קו תחתון מציין את הקטגוריה שנבחרה.

ב- app.dart, ממירים את הווידג'ט ShrineApp מ-stateless ל-stateful.

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

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

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

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

אחר כך משנים את השכבה האחורית ל-CategoryMenuPage.

ב-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 ל-CategoryMenuPage ואת השדה 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

דף מוצר מסונן ב-Shrine

דף מוצר מסונן ב-Shrine

הם מסוננים!

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

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

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

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

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

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;

לאחר מכן מוסיפים GestureDetector ל-child של ‎_FrontLayer: Column's children:‎.

      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          // TODO: Add a GestureDetector (104)
          GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: onTap,
            child: Container(
              height: 40.0,
              alignment: AlignmentDirectional.centerStart,
            ),
          ),
          Expanded(
            child: child,
          ),
        ],
      ),

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

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

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

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

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

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

Android

iOS

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

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

ב-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 בכלי ליצירת 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. מעולה!

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

השלבים הבאים

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

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

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

הצלחתי להשלים את ה-codelab הזה בזמן סביר ובמאמץ סביר

נכון מאוד נכון אין לי דעה לכאן או לכאן לא נכון לא נכון בכלל

אני רוצה להמשיך להשתמש ב-Material Components בעתיד

נכון מאוד נכון אין לי דעה לכאן או לכאן לא נכון לא נכון בכלל