MDC-104 Flutter: المكوّنات المتقدّمة المتعلّقة بالمواد

1. مقدمة

logo_components_color_2x_web_96dp.png

تساعد المكونات المادية (MDC) المطورين على تنفيذ التصميم المتعدد الأبعاد. يضم مركز MDC، الذي أنشأه فريق من المهندسين ومصممي تجربة المستخدم في Google، عشرات من مكونات واجهة المستخدم الجميلة والعملية، وهو متاح لأجهزة Android وiOS والويب وFlutter.material.io/develop

في الدرس التطبيقي حول الترميز MDC-103، خصّصت لون مكونات المواد (MDC) وشكلها ومستوى خطها وشكلها لتصميم تطبيقك.

ينفذ مكون في نظام Material Design مجموعة من المهام المحددة مسبقًا وله خصائص معينة، مثل الزر. ومع ذلك، فإن الزر أكثر من مجرد وسيلة للمستخدم لتنفيذ إجراء، ولكنه أيضًا تعبير مرئي للشكل والحجم واللون يتيح للمستخدم معرفة أنه تفاعلي، وأن شيئًا ما سيحدث عند اللمس أو النقر.

تصف إرشادات التصميم المتعدد الأبعاد المكونات من وجهة نظر المصمم. فهي تصف مجموعة كبيرة من الوظائف الأساسية المتاحة عبر المنصات، والعناصر التشريحية التي تشكل كل مكون. على سبيل المثال، تحتوي الصور الخلفية على طبقة خلفية ومحتواها، والطبقة الأمامية ومحتواها، وقواعد الحركة، وخيارات العرض. يمكن تخصيص كل عنصر من هذه المكوّنات وفقًا لاحتياجات كل تطبيق وحالات استخدامه ومحتواه.

ما الذي ستنشئه

في هذا الدرس التطبيقي حول الترميز، ستعمل على تغيير واجهة المستخدم في تطبيق Shrine إلى عرض تقديمي من مستويين يسمى "Backdrop". تتضمن الخلفية قائمة تسرد الفئات القابلة للاختيار المستخدمة لتصفية المنتجات المعروضة في الشبكة غير المتماثلة. في هذا الدرس التطبيقي، ستستخدم ما يلي:

  • شكل
  • الحركة
  • تطبيقات Flutter المصغّرة (التي استخدمتها في الدروس التطبيقية السابقة حول الترميز)

Android

iOS

تطبيق للتجارة الإلكترونية بطابع وردي وبني مع شريط تطبيق علوي وشبكة غير متماثلة قابلة للتمرير أفقيًا مليئة بالمنتجات

تطبيق للتجارة الإلكترونية بطابع وردي وبني مع شريط تطبيق علوي وشبكة غير متماثلة قابلة للتمرير أفقيًا مليئة بالمنتجات

قائمة تتضمّن 4 فئات

قائمة تتضمّن 4 فئات

مكوّنات Material Flutter والأنظمة الفرعية الخاصة بها في هذا الدرس التطبيقي حول الترميز

  • شكل

ما هو تقييمك لمستوى خبرتك في تطوير Flutter؟

حديث متوسط بارع

2. إعداد بيئة تطوير Flutter

لإكمال هذا التمرين، تحتاج إلى برنامجَين، وهما Flutter SDK ومحرِّر.

يمكنك تشغيل الدرس التطبيقي حول الترميز باستخدام أي من الأجهزة التالية:

  • جهاز Android أو iOS فعلي متصل بجهاز الكمبيوتر وتم ضبطه على "وضع مطور البرامج".
  • محاكي iOS (يتطلب تثبيت أدوات Xcode).
  • محاكي Android (يتطلب عملية إعداد في "استوديو Android").
  • متصفّح (يجب توفُّر متصفّح Chrome لتصحيح الأخطاء)
  • كتطبيق سطح المكتب الذي يعمل بنظام التشغيل Windows أو Linux أو macOS. يجب إجراء تطوير على النظام الأساسي الذي تخطّط لنشر الإعلان عليه. لذا، إذا كنت ترغب في تطوير تطبيق سطح مكتب Windows، ينبغي لك تطويره على Windows للوصول إلى سلسلة الإصدار المناسبة. هناك متطلبات خاصة بنظام التشغيل تم تناولها بالتفصيل على docs.flutter.dev/desktop.

3- تنزيل تطبيق بدء الدروس التطبيقية حول الترميز

هل تريد المتابعة من MDC-103؟

إذا أكملت MDC-103، يجب أن يكون الرمز الخاص بك جاهزًا لهذا الدرس التطبيقي حول الترميز. التخطّي إلى الخطوة: إضافة قائمة الصور الخلفية.

هل تريد البدء من نقطة الصفر؟

يتوفّر تطبيق إجراء التفعيل في دليل material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.

...أو استنساخها من GitHub

لاستنساخ هذا الدرس التطبيقي حول الترميز من 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 من التمارين التطبيقية السابقة حول الترميز على جهازك.

Android

iOS

صفحة تسجيل الدخول إلى الضريح

صفحة تسجيل الدخول إلى الضريح

4. إضافة قائمة الصور الخلفية

تظهر الخلفية خلف كل المحتوى والمكونات الأخرى. وهو يتكون من طبقتين: طبقة خلفية (تعرض الإجراءات والفلاتر) والطبقة الأمامية (التي تعرض المحتوى). يمكنك استخدام الصور الخلفية لعرض معلومات وإجراءات تفاعلية، مثل فلاتر التنقل أو المحتوى.

إزالة شريط تطبيق الشاشة الرئيسية

ستكون أداة الصفحة الرئيسية هي محتوى الطبقة الأمامية. يحتوي الآن على شريط تطبيقات. سننقل شريط التطبيقات إلى الطبقة الخلفية وستتضمّن الصفحة الرئيسية عرض AsymmetricView فقط.

في home.dart، غيِّر الدالة build() لإرجاع AsymmetricView فقط:

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

إضافة تطبيق الصور الخلفية

أنشئ أداة باسم الصور الخلفية التي تشتمل على 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 على بعض السمات. وهذه إحدى أفضل الممارسات للسمات في الدالة الإنشائية التي لا تتضمّن قيمة تلقائية ولا يمكن أن تكون 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() سقالة بشريط تطبيقات تمامًا مثل الصفحة الرئيسية. لكن جسد سافولد يمثل حزمة. يمكن أن تتداخل العناصر الثانوية للحزمة. ويتم تحديد حجم كل طفل وموقعه بالنسبة إلى العنصر الرئيسي للحزمة.

أضف الآن مثيل 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 المنطقة الوردية في طبقة جديدة خلف الصفحة الرئيسية في الجهة الأمامية.

يمكنك استخدام Flutter Inspector (أداة فحص Flutter) للتأكّد من أنّ الحزمة تتضمّن حاوية في الصفحة الرئيسية. يُفترض أن يشبه ما يلي:

92ed338a15a074bd.png

يمكنك الآن ضبط كلا الطبقتين التصميم والمحتوى.

5- إضافة شكل

في هذه الخطوة، أنشِئ نمطًا للطبقة الأمامية لإضافة لقطة في الزاوية العلوية اليسرى.

يشير التصميم المتعدد الأبعاد إلى هذا النوع من التخصيص على أنه شكل. قد تحتوي الأسطح المادية على أشكال عشوائية. تضيف الأشكال توكيدًا ونمطًا إلى الأسطح ويمكن استخدامها للتعبير عن العلامة التجارية. يمكن تخصيص الأشكال المستطيلة العادية باستخدام زوايا وحواف منحنية أو بزاوية، وأي عدد من الجوانب. يمكن أن تكون متماثلة أو غير منتظمة.

إضافة شكل إلى الطبقة الأمامية

ألهم شعار الضريح المائل قصة الشكل لتطبيق 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 على تنسيق الرسوم المتحركة ومنحك واجهة برمجة تطبيقات لتشغيل الرسوم المتحركة وعكسها وإيقافها. نحتاج الآن إلى دوال تجعله يتحرك.

أضف دوالاً تحدد مستوى رؤية الطبقة الأمامية وتغيرها:

  // 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 في التطبيق المصغّر ExceptionSemantics. ستستبعد هذه الأداة عناصر قائمة backLayer من شجرة الدلالة عندما لا تكون الطبقة الخلفية مرئية.

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

قم بتغيير الدالة _buildStack() لتوليد BuildContext وBoxConstraints. أيضًا، قم بتضمين نقل موضعي يأخذ صورة متحركة relatedRectTween:

  // 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 كأداة إنشاء:

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

لقد تأخرنا إنشاء حزمة الطبقة الأمامية/الخلفية حتى وقت التخطيط باستخدام LayoutBuilder كي نتمكن من دمج الارتفاع الكلي الفعلي للخلفية. LayoutBuilder هي أداة خاصة توفر معاودة الاتصال بأداة الإنشاء قيود الحجم.

في الدالة 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، فسيبقى حجم العمود كما هو متحرك.

التفاف أعمدة المنتجات في عرض على شكل قائمة

في 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

قائمة Shrine فارغة مع خطأ واحد

قائمة Shrine فارغة مع خطأ واحد

يختفي التحذير بشأن الانتقال باللون الرمادي في 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

قائمة Shrine فارغة

قائمة Shrine فارغة

ما مِن عناصر إضافية.

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": اضغط على ⌥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 للفئة المحدّدة ورقم استدعاء عند النقر عليه:

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

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

ثم قم بتغيير الطبقة الخلفية إلى category MenuPage.

في app.dart، استورِد Group ListPage:

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 إلى فئة القائمة "Category"Page والحقل "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، أضِف معاودة الاتصال عند النقر إلى طبقة الصور الخلفية:

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;

أضف بعد ذلك أداة DeviceDetector إلى عنصر _FrontLayer التابع: عناصر العمود الثانوية:.

      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 من أداة إنشاء AppBar. يجب إزالة الرمز المخصّص الذي يحمل العلامة التجارية ليتم عرضه في مكان تطبيق "leading" المصغّر الأصلي. يتم تمرير الصورة المتحركة listenable والمعالج 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. تهانينا!

على مدار هذه الدروس الأربعة حول الترميز، تعلمت كيفية استخدام مكونات المواد لإنشاء تجارب مستخدم فريدة وأنيقة تعبر عن شخصية العلامة التجارية وأسلوبها.

الخطوات التالية

يُكمل هذا الدرس التطبيقي، MDC-104، سلسلة الدروس التطبيقية حول الترميز. يمكنك استكشاف المزيد من المكوّنات في Material Flutter من خلال الانتقال إلى كتالوج التطبيقات المصغّرة لمكونات المواد.

بالنسبة إلى الهدف الموسّع، جرِّب استبدال الرمز الذي يحمل العلامة التجارية برمز AnimatedIcon الذي يتحرك بين رمزين عندما تكون الخلفية مرئية.

هناك العديد من الدروس التطبيقية حول ترميز Flutter التي يمكنك تجربتها حسب اهتماماتك. لدينا درس تطبيقي آخر حول الترميز خاص بالمواد قد يهمّك: إنشاء انتقالات رائعة باستخدام Material Motion for Flutter.

تمكنتُ من إكمال هذا الدرس التطبيقي حول الترميز بقدرٍ معقول من الوقت والجهد

أوافق بشدة أوافق ليست دقيقة ولا غير دقيقة لا أوافق لا أوافق أبدًا

أود مواصلة استخدام Material Components في المستقبل

أوافق بشدة أوافق ليست دقيقة ولا غير دقيقة لا أوافق لا أوافق أبدًا