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 إلى عرض تقديمي من مستويَين يُعرف باسم "خلفية". تتضمن الخلفية قائمة تسرد الفئات القابلة للاختيار المستخدمة لتصفية المنتجات المعروضة في الشبكة غير المتماثلة. في هذا الدرس التطبيقي، ستستخدم ما يلي:

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

Android

iOS

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

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

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

قائمة الطعام التي تتضمّن 4 فئات

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

  • شكل

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

مبتدئ متوسط متقدّم

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

تحتاج إلى برنامجَين لإكمال هذا الدرس التطبيقي، وهما حزمة تطوير البرامج (SDK) من Flutter ومحرِّر.

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

  • جهاز 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

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

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

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

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

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

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

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

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

92ed338a15a074bd.png

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

5- إضافة شكل

في هذه الخطوة، ستصمّم الطبقة الأمامية لإضافة قطع في أعلى يمين الشاشة.

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

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

ألهم شعار 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

صفحة منتج تشمل ضريحًا بشكل مخصّص

صفحة منتج تشمل ضريحًا بشكل مخصّص

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

تغيير لون شريط التطبيقات

في 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 في تطبيق مصغّر 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. أيضًا، قم بتضمين انتقال موضعه يأخذ صورة متحركة نسبية:

  // 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()، يمكنك تحويل رمز القائمة الرئيسي في شريط التطبيق إلى رمز زر واستخدامه لتبديل مستوى ظهور الطبقة الأمامية عند النقر على الزر.

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

قائمة ضريح فارغة تتضمّن خطأً واحدًا

قائمة ضريح فارغة تتضمّن خطأً واحدًا

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

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

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 Code: اضغط على ‎⌘. (نظام التشغيل macOS) أو Ctrl+.
  5. اختَر "التحويل إلى StatefulWidget".
  6. غيِّر فئة ShrineAppState إلى خاصة (_ShrineAppState). انقر بزر الماوس الأيمن على ShrineAppState،
  7. "استوديو Android": اختَر إعادة التشكيل > إعادة التسمية.
  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

تمّت فلترتها.

إغلاق الطبقة الأمامية بعد تحديد قائمة

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

بعد ذلك، أضِف عنصر GestureDetector إلى عنصر _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. تهانينا!

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

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

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

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

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

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

أوافق بشدة أوافق لا أوافق ولا أعارض لا أوافق لا أوافق أبدًا

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

أوافق بشدة أوافق لا أوافق ولا أعارض لا أوافق لا أوافق أبدًا