1. مقدمة
| تساعد Material Components (MDC) المطوّرين في تنفيذ التصميم المتعدد الأبعاد. تم إنشاء MDC بواسطة فريق من المهندسين ومصممي تجربة المستخدم في Google، وتتضمّن عشرات المكوّنات الجميلة والوظيفية لواجهة المستخدم، وهي متاحة على Android وiOS والويب وFlutter.material.io/develop |
في درس تطبيقي حول الترميز MDC-103، خصّصت اللون والارتفاع وخط الطباعة والشكل لمكوّنات التصميم المتعدد الأبعاد (MDC) لتصميم تطبيقك.
يؤدي المكوّن في نظام التصميم المتعدد الأبعاد مجموعة من المهام المحدّدة مسبقًا وله خصائص معيّنة، مثل الزر. ومع ذلك، فإنّ الزر ليس مجرّد طريقة تتيح للمستخدم تنفيذ إجراء، بل هو أيضًا تعبير مرئي عن الشكل والحجم واللون يتيح للمستخدم معرفة أنّه تفاعلي، وأنّ شيئًا ما سيحدث عند النقر عليه أو لمسه.
تصف إرشادات التصميم المتعدد الأبعاد المكوّنات من وجهة نظر المُصمِّم. وهي تصف مجموعة كبيرة من الوظائف الأساسية المتاحة على جميع المنصات، والعناصر التشريحية التي يتكوّن منها كل مكوّن. على سبيل المثال، تحتوي الخلفية على طبقة خلفية ومحتواها وطبقة أمامية ومحتواها وقواعد الحركة وخيارات العرض. ويمكن تخصيص كلّ من هذه المكوّنات لتلبية احتياجات كلّ تطبيق وحالات استخدامه ومحتواه.
ما ستنشئه
في هذا الدرس التطبيقي حول الترميز، ستغيّر واجهة المستخدم في تطبيق Shrine إلى عرض بمستويَين يُعرف باسم "الخلفية". تتضمّن الخلفية قائمة تعرض فئات قابلة للتحديد تُستخدَم لفلترة المنتجات المعروضة في الشبكة غير المتماثلة. في هذا الدرس التطبيقي، ستستخدم ما يلي:
- الشكل
- حركة
- عناصر واجهة مستخدم Flutter (التي استخدمتها في الدروس العملية السابقة)
Android | iOS |
|
|
|
|
مكوّنات 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
افتح المشروع وشغِّل التطبيق
- افتح المشروع في المحرِّر الذي تختاره.
- اتّبِع التعليمات الواردة في مقالة البدء: تجربة القيادة ضمن القسم "تشغيل التطبيق" في المحرّر الذي اخترته.
اكتمال النقل بنجاح من المفترض أن تظهر لك صفحة تسجيل الدخول إلى Shrine من دروس البرمجة السابقة على جهازك.
Android | iOS |
|
|
4. إضافة قائمة الخلفية
تظهر خلفية خلف كل المحتوى والمكوّنات الأخرى. تتألف من طبقتَين: طبقة خلفية (تعرض الإجراءات والفلاتر) وطبقة أمامية (تعرض المحتوى). يمكنك استخدام خلفية لعرض معلومات وإجراءات تفاعلية، مثل التنقّل أو فلاتر المحتوى.
إزالة شريط تطبيقات الشاشة الرئيسية
سيكون تطبيق HomePage المصغّر هو محتوى الطبقة الأمامية. يتضمّن حاليًا شريط تطبيقات. سننقل شريط التطبيق إلى الطبقة الخلفية، ولن تتضمّن الصفحة الرئيسية سوى AsymmetricView.
في home.dart، غيِّر الدالة build() لعرض AsymmetricView فقط:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
إضافة تطبيق 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، أضِف فئة _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.
الآن، أضِف مثيلاً من 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 المنطقة الوردية في طبقة جديدة خلف الصفحة الرئيسية frontLayer.
يمكنك استخدام Flutter Inspector للتأكّد من أنّ Stack يحتوي على Container خلف HomePage. يجب أن يكون مشابهًا لما يلي:

يمكنك الآن تعديل تصميم وطبقة المحتوى.
5- إضافة شكل
في هذه الخطوة، ستصمّم الطبقة الأمامية لإضافة قطع في الزاوية العلوية اليمنى.
يشير التصميم المتعدد الأبعاد إلى هذا النوع من التخصيص باسم "الشكل". يمكن أن تتخذ أسطح المواد أشكالاً عشوائية. تضيف الأشكال لمسة مميزة إلى مساحات العرض ويمكن استخدامها للتعبير عن العلامة التجارية. يمكن تخصيص الأشكال المستطيلة العادية بزوايا وحواف منحنية أو مائلة، وبأي عدد من الجوانب. ويمكن أن تكون متماثلة أو غير منتظمة.
إضافة شكل إلى الطبقة الأمامية
استوحينا شكل قصة تطبيق 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,
),
),
);
}
إعادة التشغيل السريع من المفترض أن يظهر شريط التطبيق الملوّن الجديد الآن.
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. أضِف أيضًا PositionedTransition يستخدِم Animation من نوع 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 كأداة إنشاء:
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 |
|
|
تتحرّك الطبقة الأمامية (تنزلق) للأسفل. ولكن إذا نظرت إلى الأسفل، سيظهر خطأ باللون الأحمر وخطأ تجاوز سعة التخزين. يرجع ذلك إلى أنّ هذه الحركة تؤدي إلى ضغط 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 من تطبيق بلا حالة إلى تطبيق ذي حالة.
- تمييز
ShrineApp. - استنادًا إلى بيئة التطوير المتكاملة، اعرض إجراءات الرمز:
- استوديو Android: اضغط على ⌥Enter (في نظام التشغيل macOS) أو alt + enter
- VS Code: اضغط على ⌘. (في نظام التشغيل macOS) أو Ctrl+.
- اختَر "التحويل إلى StatefulWidget".
- غيِّر فئة ShrineAppState إلى خاصة (_ShrineAppState). انقر بزر الماوس الأيمن على ShrineAppState، ثم
- في "استوديو Android": اختَر "تعديل التعليمات البرمجية" > "إعادة التسمية"
- في VS Code: اختَر "إعادة تسمية الرمز" (Rename Symbol)
- أدخِل _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 |
|
|
إذا نقرت على أحد خيارات القائمة، لن يحدث أي شيء...بعد. لنحاول إصلاح هذه المشكلة.
في 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;
بعد ذلك، أضِف أداة GestureDetector إلى العنصر التابع لـ _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 |
|
|
في 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 من خلال الانتقال إلى قائمة أدوات Material Components.
لتحقيق هدف إضافي، جرِّب استبدال الرمز الذي يحمل علامتك التجارية بـ AnimatedIcon الذي يتحرّك بين رمزين عندما تصبح الخلفية مرئية.
هناك الكثير من دروس Flutter البرمجية الأخرى التي يمكنك تجربتها استنادًا إلى اهتماماتك. لدينا درس تطبيقي آخر خاص بـ Material قد يهمّك: إنشاء انتقالات رائعة باستخدام Material Motion في Flutter.






















