استخدام Flutter لإنشاء مؤثرات انتقالية رائعة باستخدام ميزة "الحركة المتعددة"

1. مقدمة

التصميم المتعدد الأبعاد هو نظام لتصميم منتجات رقمية جريئة وجميلة. من خلال توحيد الأسلوب والعلامات التجارية والتفاعل والحركة في ظل مجموعة متسقة من المبادئ والمكونات، يمكن لفرق المنتجات تحقيق أكبر إمكانات التصميم.

logo_components_color_2x_web_96dp.png

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

ما هو نظام الحركة المتعدد الأبعاد في Flutter؟

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

في ما يلي أنماط انتقال المواد الأربعة الرئيسية:

  • تحويل الحاوية: الانتقالات بين عناصر واجهة المستخدم التي تحتوي على حاوية؛ إنشاء اتصال مرئي بين عنصرين متميزين لواجهة المستخدم عن طريق تحويل عنصر إلى آخر بسلاسة.

11807bdf36c66657.gif

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

71218f390amae07e.gif

  • التلاشي خلال: الانتقالات بين عناصر واجهة المستخدم التي لا تربطها علاقة قوية ببعضها تستخدم تلاشي متسلسل وتلاشي للداخل، بمقياس للعنصر الوارد.

385ba37b8da68969.gif

  • التلاشي:يُستخدَم هذا النوع لعناصر واجهة المستخدم التي تدخل أو تخرج داخل حدود الشاشة.

cfc40fd6e27753b6.gif

توفّر حزمة الصور المتحركة أدوات انتقال لهذه الأنماط تم تصميمها استنادًا إلى مكتبة الصور المتحركة في Flutter (flutter/animation.dart) ومكتبة مواد Flutter (flutter/material.dart):

سوف تستخدم في هذا الدرس التطبيقي حول الترميز انتقالات Materials التي تم إنشاؤها في إطار عمل Flutter ومكتبة Material، أي أنك ستتعامل مع التطبيقات المصغّرة. :)

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

يتضمّن هذا الدرس التطبيقي حول الترميز خطوات إرشادية لإنشاء بعض الانتقالات إلى مثال تطبيق بريد إلكتروني من Flutter يُسمى Reply (رد) باستخدام Dart لتوضيح كيفية استخدام الانتقالات من حزمة الصور المتحركة لتخصيص شكل ومظهر تطبيقك.

وسيتم توفير رمز التفعيل لتطبيق Reply، كما ستدمج انتقالات Material التالية في التطبيق، والتي يمكن الاطلاع عليها في ملف GIF للدرس التطبيقي حول الترميز الذي تم إكماله أدناه:

  • نقل تحويل الحاوية من قائمة عناوين البريد الإلكتروني إلى صفحة تفاصيل البريد الإلكتروني
  • نقل Container Transform من الإجراء إلى الإجراء الرئيسي (FAB) لإنشاء صفحة بريد إلكتروني.
  • انتقال المحور ي المشترك من رمز البحث إلى صفحة عرض البحث
  • التلاشي خلال الانتقال بين صفحات صندوق البريد
  • التلاشي خلال للانتقال بين زر الإجراء الرئيسي (FAB) للإنشاء والرد
  • التلاشي خلال الانتقال بين عنوان صندوق البريد المخفي
  • التلاشي حتى للانتقال بين إجراءات شريط التطبيق السفلي

b26fe84fed12d17d.gif

المتطلبات

  • معرفة أساسية بتطوير Flutter واستخدام لعبة Dart
  • أداة تعديل الرموز
  • جهاز أو مُحاكي Android/iOS
  • نموذج الرمز (انظر الخطوة التالية)

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

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

ما الذي تريد تعلّمه من هذا الدرس التطبيقي حول الترميز؟

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

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

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

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

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

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

الخيار 1: استنساخ تطبيق الدرس التطبيقي للمبتدئين من GitHub

لنسخ الدرس التطبيقي حول الترميز هذا من GitHub، شغِّل الأوامر التالية:

git clone https://github.com/material-components/material-components-flutter-motion-codelab.git
cd material-components-flutter-motion-codelab

الخيار 2: تنزيل ملف ZIP المتعلق بتطبيق "الدرس التطبيقي للمبتدئين"

يتوفّر تطبيق إجراء التفعيل في دليل material-components-flutter-motion-codelab-starter.

التحقق من تبعيات المشروع

يعتمد المشروع على حزمة الصور المتحركة. في pubspec.yaml، لاحِظ أنّ القسم dependencies يتضمّن ما يلي:

animations: ^2.0.0

فتح المشروع وتشغيل التطبيق

  1. افتح المشروع في المحرِّر الذي تختاره.
  2. اتّبِع التعليمات من أجل "تشغيل التطبيق" في البدء: اختبار القيادة للمحرِّر الذي اخترته.

اكتمال عملية النقل بنجاح يجب أن يعمل رمز إجراء التفعيل للصفحة الرئيسية لـ Reply على جهازك/المحاكي. من المفترض أن يظهر لك البريد الوارد الذي يحتوي على قائمة بالرسائل الإلكترونية.

الصفحة الرئيسية للرد

اختياري: إبطاء الصور المتحركة على الجهاز

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

d23a7bfacffac509.gif

اختياري: الوضع المُعتِم

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

87618d8418eee19e.gif

4. التعرّف على نموذج رمز التطبيق

لنلقِ نظرة على الرمز. لقد وفّرنا تطبيقًا يستخدم حزمة الصور المتحركة للانتقال بين الشاشات المختلفة في التطبيق.

  • الصفحة الرئيسية: تعرض صندوق البريد المحدد
  • InboxPage: عرض قائمة بالرسائل الإلكترونية
  • MailPreviewCard: تعرض معاينة لرسالة إلكترونية
  • MailViewPage: يعرض رسالة إلكترونية واحدة كاملة
  • ComposePage: تسمح بإنشاء رسالة إلكترونية جديدة
  • صفحة البحث: تعرض طريقة عرض بحث

router.dart

أولاً، لفهم طريقة إعداد شريط التنقّل الجذر في التطبيق، افتح router.dart في الدليل lib:

class ReplyRouterDelegate extends RouterDelegate<ReplyRoutePath>
   with ChangeNotifier, PopNavigatorRouterDelegateMixin<ReplyRoutePath> {
 ReplyRouterDelegate({required this.replyState})
     : navigatorKey = GlobalObjectKey<NavigatorState>(replyState) {
   replyState.addListener(() {
     notifyListeners();
   });
 }

 @override
 final GlobalKey<NavigatorState> navigatorKey;

 RouterProvider replyState;

 @override
 void dispose() {
   replyState.removeListener(notifyListeners);
   super.dispose();
 }

 @override
 ReplyRoutePath get currentConfiguration => replyState.routePath!;

 @override
 Widget build(BuildContext context) {
   return MultiProvider(
     providers: [
       ChangeNotifierProvider<RouterProvider>.value(value: replyState),
     ],
     child: Selector<RouterProvider, ReplyRoutePath?>(
       selector: (context, routerProvider) => routerProvider.routePath,
       builder: (context, routePath, child) {
         return Navigator(
           key: navigatorKey,
           onPopPage: _handlePopPage,
           pages: [
             // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
             const CustomTransitionPage(
               transitionKey: ValueKey('Home'),
               screen: HomePage(),
             ),
             if (routePath is ReplySearchPath)
               const CustomTransitionPage(
                 transitionKey: ValueKey('Search'),
                 screen: SearchPage(),
               ),
           ],
         );
       },
     ),
   );
 }

 bool _handlePopPage(Route<dynamic> route, dynamic result) {
   // _handlePopPage should not be called on the home page because the
   // PopNavigatorRouterDelegateMixin will bubble up the pop to the
   // SystemNavigator if there is only one route in the navigator.
   assert(route.willHandlePopInternally ||
       replyState.routePath is ReplySearchPath);

   final bool didPop = route.didPop(result);
   if (didPop) replyState.routePath = const ReplyHomePath();
   return didPop;
 }

 @override
 Future<void> setNewRoutePath(ReplyRoutePath configuration) {
   replyState.routePath = configuration;
   return SynchronousFuture<void>(null);
 }
}

هذا هو المتصفح الجذري الذي يتعامل مع شاشات التطبيق التي تستهلك اللوحة بالكامل، مثل HomePage وSearchPage. ويستمع النظام إلى حالة تطبيقنا للتحقّق مما إذا كنا قد حدّدنا المسار إلى ReplySearchPath. وفي هذه الحالة، سيُعيد إنشاء أداة التنقّل باستخدام SearchPage في أعلى الحزمة. تجدر الإشارة إلى أنّ شاشاتنا يتم التفافها في CustomTransitionPage بدون تحديد أي انتقالات. يوضح لك هذا طريقة واحدة للتنقل بين الشاشات بدون أي انتقال مخصص.

home.dart

تم ضبط مسارنا إلى ReplySearchPath في حالة تطبيقنا من خلال تنفيذ ما يلي داخل _BottomAppBarActionItems في home.dart:

Align(
 alignment: AlignmentDirectional.bottomEnd,
 child: IconButton(
   icon: const Icon(Icons.search),
   color: ReplyColors.white50,
   onPressed: () {
     Provider.of<RouterProvider>(
       context,
       listen: false,
     ).routePath = const ReplySearchPath();
   },
 ),
);

في معلمة onPressed، يمكننا الوصول إلى RouterProvider وتعيين routePath على ReplySearchPath. يتتبّع RouterProvider حالة أدوات التنقّل الجذر لدينا.

mail_view_router.dart

والآن، لنتعرّف على طريقة إعداد التنقّل الداخلي في تطبيقنا. افتح mail_view_router.dart في دليل lib. سيظهر لك متصفّح مشابه للمتصفّح أعلاه:

class MailViewRouterDelegate extends RouterDelegate<void>
   with ChangeNotifier, PopNavigatorRouterDelegateMixin {
 MailViewRouterDelegate({required this.drawerController});

 final AnimationController drawerController;

 @override
 Widget build(BuildContext context) {
   bool _handlePopPage(Route<dynamic> route, dynamic result) {
     return false;
   }

   return Selector<EmailStore, String>(
     selector: (context, emailStore) => emailStore.currentlySelectedInbox,
     builder: (context, currentlySelectedInbox, child) {
       return Navigator(
         key: navigatorKey,
         onPopPage: _handlePopPage,
         pages: [
           // TODO: Add Fade through transition between mailbox pages (Motion)
           CustomTransitionPage(
             transitionKey: ValueKey(currentlySelectedInbox),
             screen: InboxPage(
               destination: currentlySelectedInbox,
             ),
           )
         ],
       );
     },
   );
 }
...
}

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

home.dart

تم ضبط صندوق البريد الحالي في حالة تطبيقنا من خلال إجراء ما يلي داخل _HomePageState في home.dart:

void _onDestinationSelected(String destination) {
 var emailStore = Provider.of<EmailStore>(
   context,
   listen: false,
 );

 if (emailStore.onMailView) {
   emailStore.currentlySelectedEmailId = -1;
 }

 if (emailStore.currentlySelectedInbox != destination) {
   emailStore.currentlySelectedInbox = destination;
 }

 setState(() {});
}

في دالة _onDestinationSelected، يمكننا الوصول إلى EmailStore وتعيين currentlySelectedInbox إلى الوجهة المحددة. يتتبّع "EmailStore" حالة المتنقّلين الداخليين.

home.dart

وأخيرًا، للاطّلاع على مثال على مسار تنقُّل قيد الاستخدام، افتح home.dart في الدليل lib. حدِّد الفئة _ReplyFabState داخل السمة onTap في التطبيق المصغّر InkWell، على النحو التالي:

onTap: () {
 Provider.of<EmailStore>(
   context,
   listen: false,
 ).onCompose = true;
 Navigator.of(context).push(
   PageRouteBuilder(
     pageBuilder: (
       BuildContext context,
       Animation<double> animation,
       Animation<double> secondaryAnimation,
     ) {
       return const ComposePage();
     },
   ),
 );
},

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

والآن بعد أن تعرفت على رمز إجراء التفعيل، لنبدأ في تنفيذ أول عملية انتقال.

5- إضافة نقل الحاوية من قائمة عناوين البريد الإلكتروني إلى صفحة تفاصيل البريد الإلكتروني

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

قبل إضافة أي رمز، حاول تشغيل تطبيق Reply والنقر على رسالة إلكترونية. يجب أن يتم الانتقال سريعًا، أي يتم استبدال الشاشة بدون انتقال:

قبل

48b00600f73c7778.gif

ابدأ بإضافة عملية استيراد لحزمة الصور المتحركة في أعلى mail_card_preview.dart كما هو موضّح في المقتطف التالي:

mail_card_preview.dart

import 'package:animations/animations.dart';

الآن بعد أن انتهيت من استيراد حزمة الرسوم المتحركة، يمكننا البدء في إضافة انتقالات رائعة إلى تطبيقك. لنبدأ بإنشاء صف StatelessWidget يضم تطبيق OpenContainer المصغّر.

في mail_card_preview.dart، أضِف مقتطف الرمز التالي بعد تعريف فئة MailPreviewCard:

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
class _OpenContainerWrapper extends StatelessWidget {
 const _OpenContainerWrapper({
   required this.id,
   required this.email,
   required this.closedChild,
 });

 final int id;
 final Email email;
 final Widget closedChild;

 @override
 Widget build(BuildContext context) {
   final theme = Theme.of(context);
   return OpenContainer(
     openBuilder: (context, closedContainer) {
       return MailViewPage(id: id, email: email);
     },
     openColor: theme.cardColor,
     closedShape: const RoundedRectangleBorder(
       borderRadius: BorderRadius.all(Radius.circular(0)),
     ),
     closedElevation: 0,
     closedColor: theme.cardColor,
     closedBuilder: (context, openContainer) {
       return InkWell(
         onTap: () {
           Provider.of<EmailStore>(
             context,
             listen: false,
           ).currentlySelectedEmailId = id;
           openContainer();
         },
         child: closedChild,
       );
     },
   );
 }
}

الآن لنستخدم برنامج تضمين جديد. داخل تعريف الفئة MailPreviewCard، سنلفي التطبيق المصغَّر Material من دالة build() مع _OpenContainerWrapper الجديد:

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
 id: id,
 email: email,
 closedChild: Material(
...

يتضمن _OpenContainerWrapper التطبيق المصغّر InkWell، وتحدّد خصائص اللون في OpenContainer لون الحاوية التي تتضمّنها. ومن ثم، يمكننا إزالة أدوات Material وInkwell. تظهر التعليمة البرمجية الناتجة على النحو التالي:

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
 id: id,
 email: email,
 closedChild: Dismissible(
   key: ObjectKey(email),
   dismissThresholds: const {
     DismissDirection.startToEnd: 0.8,
     DismissDirection.endToStart: 0.4,
   },
   onDismissed: (direction) {
     switch (direction) {
       case DismissDirection.endToStart:
         if (onStarredInbox) {
           onStar();
         }
         break;
       case DismissDirection.startToEnd:
         onDelete();
         break;
       default:
     }
   },
   background: _DismissibleContainer(
     icon: 'twotone_delete',
     backgroundColor: colorScheme.primary,
     iconColor: ReplyColors.blue50,
     alignment: Alignment.centerLeft,
     padding: const EdgeInsetsDirectional.only(start: 20),
   ),
   confirmDismiss: (direction) async {
     if (direction == DismissDirection.endToStart) {
       if (onStarredInbox) {
         return true;
       }
       onStar();
       return false;
     } else {
       return true;
     }
   },
   secondaryBackground: _DismissibleContainer(
     icon: 'twotone_star',
     backgroundColor: currentEmailStarred
         ? colorScheme.secondary
         : theme.scaffoldBackgroundColor,
     iconColor: currentEmailStarred
         ? colorScheme.onSecondary
         : colorScheme.onBackground,
     alignment: Alignment.centerRight,
     padding: const EdgeInsetsDirectional.only(end: 20),
   ),
   child: mailPreview,
 ),
);

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

بعد

663e8594319bdee3.gif

6- إضافة نقل Container Transform من زر الإجراء الرئيسي (FAB) لإنشاء صفحة بريد إلكتروني

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

قبل

4aa2befdc5170c60.gif

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

في home.dart، لنستورد package:animations/animations.dart في أعلى الملف ونعدِّل طريقة _ReplyFabState build(). لنختتم تطبيق Material المصغّر الذي تم عرضه باستخدام التطبيق المصغّر OpenContainer:

home.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
 openBuilder: (context, closedContainer) {
   return const ComposePage();
 },
 openColor: theme.cardColor,
 onClosed: (success) {
   Provider.of<EmailStore>(
     context,
     listen: false,
   ).onCompose = false;
 },
 closedShape: circleFabBorder,
 closedColor: theme.colorScheme.secondary,
 closedElevation: 6,
 closedBuilder: (context, openContainer) {
   return Material(
     color: theme.colorScheme.secondary,
     ...

بالإضافة إلى المعلَمات المُستخدَمة في ضبط تطبيق OpenContainer المصغّر السابق، يتم الآن ضبط onClosed أيضًا. onClosed هو ClosedCallback يتم طلبه عند انحراف مسار OpenContainer أو عودته إلى حالة الإغلاق. يتم تمرير القيمة المعروضة لتلك المعاملة إلى هذه الدالة كوسيطة. ونستخدم هذا الحقل "Callback" لإعلام موفِّر التطبيق أنّنا غادرنا المسار ComposePage، حتى يتمكّن من إشعار جميع المستمعين.

مثلما فعلنا في خطوتنا الأخيرة، سنزيل التطبيق المصغّر Material من التطبيق المصغّر لأنّ التطبيق المصغّر OpenContainer يتعامل مع لون الأداة الذي يعرضه closedBuilder مع closedColor. سنزيل أيضًا طلب Navigator.push() من onTap في تطبيق InkWell المصغّر، واستبدله بـ openContainer() Callback الذي تم الحصول عليه من closedBuilder في تطبيق OpenContainer المصغّر، وذلك لأنّ التطبيق المصغّر OpenContainer سيتعامل الآن مع مسار التوجيه الخاص به.

التعليمة البرمجية الناتجة هي كما يلي:

home.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
return OpenContainer(
 openBuilder: (context, closedContainer) {
   return const ComposePage();
 },
 openColor: theme.cardColor,
 onClosed: (success) {
   Provider.of<EmailStore>(
     context,
     listen: false,
   ).onCompose = false;
 },
 closedShape: circleFabBorder,
 closedColor: theme.colorScheme.secondary,
 closedElevation: 6,
 closedBuilder: (context, openContainer) {
   return Tooltip(
     message: tooltip,
     child: InkWell(
       customBorder: circleFabBorder,
       onTap: () {
         Provider.of<EmailStore>(
           context,
           listen: false,
         ).onCompose = true;
         openContainer();
       },
       child: SizedBox(
         height: _mobileFabDimension,
         width: _mobileFabDimension,
         child: Center(
           child: fabSwitcher,
         ),
       ),
     ),
   );
 },
);

الآن لتنظيف بعض التعليمات البرمجية القديمة. بما أنّ تطبيق OpenContainer المصغّر الآن يتعامل مع إشعار موفِّر التطبيق أنّنا لم نعُد نستخدم ComposePage من خلال onClosed ClosedCallback، يمكننا إزالة عملية التنفيذ السابقة في mail_view_router.dart:

mail_view_router.dart

// TODO: Add Container Transform from FAB to compose email page (Motion)
emailStore.onCompose = false; /// delete this line
return SynchronousFuture<bool>(true);

هذا كل شيء في هذه الخطوة! يُفترض أن يكون لديك انتقال من FAB لإنشاء شاشة تشبه ما يلي:

بعد

5c7ad1b4b40f9f0c.gif

7. إضافة تأثير محور ي مشترَك من رمز البحث إلى صفحة عرض البحث

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

قبل إضافة رمز إضافي، جرِّب تشغيل التطبيق والنقر على رمز البحث في أسفل يسار الشاشة. من المفترض أن يؤدي هذا إلى إظهار شاشة عرض البحث بدون انتقال.

قبل

df7683a8ad7b920e.gif

للبدء، دعنا ننتقل إلى ملف router.dart. بعد تعريف الفئة ReplySearchPath، أضِف المقتطف التالي:

router.dart

// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
class SharedAxisTransitionPageWrapper extends Page {
 const SharedAxisTransitionPageWrapper(
     {required this.screen, required this.transitionKey})
     : super(key: transitionKey);

 final Widget screen;
 final ValueKey transitionKey;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder(
       settings: this,
       transitionsBuilder: (context, animation, secondaryAnimation, child) {
         return SharedAxisTransition(
           fillColor: Theme.of(context).cardColor,
           animation: animation,
           secondaryAnimation: secondaryAnimation,
           transitionType: SharedAxisTransitionType.scaled,
           child: child,
         );
       },
       pageBuilder: (context, animation, secondaryAnimation) {
         return screen;
       });
 }
}

والآن، لنستخدم SharedAxisTransitionPageWrapper الجديدة لتنفيذ عملية النقل التي نريدها. داخل تعريف الفئة ReplyRouterDelegate، ضمن السمة pages، لنختتم شاشة البحث باستخدام SharedAxisTransitionPageWrapper بدلاً من CustomTransitionPage:

router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
   const CustomTransitionPage(
     transitionKey: ValueKey('Home'),
     screen: HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('Search'),
       screen: SearchPage(),
     ),
 ],
);

يمكنك الآن إعادة تشغيل التطبيق.

81b3ea098926931.gif

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

لنصلح كلا المشكلتين من خلال إدراج HomePage أيضًا في SharedAxisTransitionWrapper بدلاً من CustomTransitionPage:

router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
   const SharedAxisTransitionPageWrapper(
     transitionKey: ValueKey('home'),
     screen: HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('search'),
       screen: SearchPage(),
     ),
 ],
);

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

بعد

462d890086a3d18a.gif

8. إضافة تأثير التلاشي خلال الانتقال بين صفحات صندوق البريد

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

قبل إضافة أي رمز إضافي، جرّب تشغيل التطبيق والنقر على شعار "رد" في شريط التطبيق السفلي، وتبديل صناديق البريد. يجب أن تتغير قائمة عناوين البريد الإلكتروني بدون أي عمليات انتقال.

قبل

89033988ce26b92e.gif

للبدء، دعنا ننتقل إلى ملف mail_view_router.dart. بعد تعريف الفئة MailViewRouterDelegate، أضِف المقتطف التالي:

mail_view_router.dart

// TODO: Add Fade through transition between mailbox pages (Motion)
class FadeThroughTransitionPageWrapper extends Page {
 const FadeThroughTransitionPageWrapper({
   required this.mailbox,
   required this.transitionKey,
 })  : super(key: transitionKey);

 final Widget mailbox;
 final ValueKey transitionKey;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder(
       settings: this,
       transitionsBuilder: (context, animation, secondaryAnimation, child) {
         return FadeThroughTransition(
           fillColor: Theme.of(context).scaffoldBackgroundColor,
           animation: animation,
           secondaryAnimation: secondaryAnimation,
           child: child,
         );
       },
       pageBuilder: (context, animation, secondaryAnimation) {
         return mailbox;
       });
 }
}

على غرار خطوتنا الأخيرة، يمكننا استخدام FadeThroughTransitionPageWrapper الجديد لتنفيذ عملية النقل التي نريدها. داخل تعريف فئة MailViewRouterDelegate، ضمن السمة pages، بدلاً من التفاف شاشة صندوق البريد الإلكتروني باستخدام CustomTransitionPage، استخدِم FadeThroughTransitionPageWrapper بدلاً من ذلك:

mail_view_router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Fade through transition between mailbox pages (Motion)
   FadeThroughTransitionPageWrapper(
     mailbox: InboxPage(destination: currentlySelectedInbox),
     transitionKey: ValueKey(currentlySelectedInbox),
   ),
 ],
);

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

بعد

8186940082b630d.gif

9. إضافة تأثير التلاشي من خلال الانتقال بين الإنشاء والرد على زر الإجراء الرئيسي (FAB)

في هذه الخطوة، سنضيف انتقالاً بين أيقونات FAB المختلفة. ونظرًا لأننا لا نريد التأكيد على علاقة مكانية أو هرمية، سنستخدم ميزة التلاشي لإجراء "تبديل" بسيط بين الأيقونات في FAB.

قبل إضافة أي رمز إضافي، جرِّب تشغيل التطبيق والنقر على رسالة إلكترونية وفتح طريقة عرض الرسالة الإلكترونية. يجب أن يتغير رمز زر الإجراء الرئيسي (FAB) بدون عملية انتقال.

قبل

d8e3afa0447cfc20.gif

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

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

في home.dart، سنُضيف المقتطف التالي ضمن _ReplyFabState:

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
class _FadeThroughTransitionSwitcher extends StatelessWidget {
 const _FadeThroughTransitionSwitcher({
   required this.fillColor,
   required this.child,
 });

 final Widget child;
 final Color fillColor;

 @override
 Widget build(BuildContext context) {
   return PageTransitionSwitcher(
     transitionBuilder: (child, animation, secondaryAnimation) {
       return FadeThroughTransition(
         fillColor: fillColor,
         child: child,
         animation: animation,
         secondaryAnimation: secondaryAnimation,
       );
     },
     child: child,
   );
 }
}

والآن، في _ReplyFabState، ابحث عن التطبيق المصغَّر "fabSwitcher". ويعرض fabSwitcher رمزًا مختلفًا استنادًا إلى ما إذا كان في عرض البريد الإلكتروني أم لا. لنختتمه مع _FadeThroughTransitionSwitcher:

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
static final fabKey = UniqueKey();
static const double _mobileFabDimension = 56;

@override
Widget build(BuildContext context) {
 final theme = Theme.of(context);
 final circleFabBorder = const CircleBorder();

 return Selector<EmailStore, bool>(
   selector: (context, emailStore) => emailStore.onMailView,
   builder: (context, onMailView, child) {
     // TODO: Add Fade through transition between compose and reply FAB (Motion)
     final fabSwitcher = _FadeThroughTransitionSwitcher(
       fillColor: Colors.transparent,
       child: onMailView
           ? Icon(
               Icons.reply_all,
               key: fabKey,
               color: Colors.black,
             )
           : const Icon(
               Icons.create,
               color: Colors.black,
             ),
     );
...

يتم منح _FadeThroughTransitionSwitcher fillColor شفاف، حتى لا تكون هناك خلفية بين العناصر عند النقل. ننشئ أيضًا UniqueKey ونخصّصه لأحد الرموز.

والآن، في هذه الخطوة، من المفترض أن يكون لديك زر الإجراء الرئيسي (FAB) لسياقي بالكامل. يؤدي الانتقال إلى عرض البريد الإلكتروني إلى تلاشي رمز الإجراء الرئيسي القديم وتصغير حجمه بينما يتلاشى الرمز الجديد ويتغيّر لونه.

بعد

c55bacd9a144ec69.gif

10. إضافة تلاشي من خلال الانتقال بين عنوان صندوق البريد المخفي

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

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

قبل

59eb57a6c71725c0.gif

سيكون باقي هذا الدرس التطبيقي حول الترميز سريعًا بما أنّنا أجرينا معظم العمل في _FadeThroughTransitionSwitcher ضمن الخطوة الأخيرة.

والآن، دعنا ننتقل إلى صف _AnimatedBottomAppBar في home.dart لإضافة العنصر الانتقالي. سنُعيد استخدام _FadeThroughTransitionSwitcher من خطوتنا الأخيرة، ونختتم شرط onMailView المشروط الذي يؤدي إلى عرض SizedBox فارغ أو عنوان صندوق بريد يتلاشى مع الدرج السفلي:

home.dart

...
const _ReplyLogo(),
const SizedBox(width: 10),
// TODO: Add Fade through transition between disappearing mailbox title (Motion)
_FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: onMailView
     ? const SizedBox(width: 48)
     : FadeTransition(
         opacity: fadeOut,
         child: Selector<EmailStore, String>(
           selector: (context, emailStore) =>
               emailStore.currentlySelectedInbox,
           builder: (
             context,
             currentlySelectedInbox,
             child,
           ) {
             return Text(
               currentlySelectedInbox,
               style: Theme.of(context)
                   .textTheme
                   .bodyMedium!
                   .copyWith(
                     color: ReplyColors.white50,
                   ),
             );
           },
         ),
       ),
),

لقد انتهينا من هذه الخطوة!

إعادة تشغيل التطبيق عندما تفتح رسالة إلكترونية ويتم نقلك إلى عرض البريد الإلكتروني، من المفترض أن يتلاشى عنوان صندوق البريد في شريط التطبيق السفلي ويتسعل نطاقه. رائع!

بعد

3f1a3db01a481124.gif

11. إضافة تأثير التلاشي خلال الانتقال بين إجراءات شريط التطبيق السفلي

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

قبل إضافة أي رمز إضافي، جرِّب تشغيل التطبيق والنقر على رسالة إلكترونية وفتح طريقة عرض الرسالة الإلكترونية. يمكنك أيضًا تجربة النقر على شعار Reply. من المفترض أن تتغير إجراءات شريط التطبيق السفلي بدون نقل.

قبل

5f662eac19fce3ed.gif

كما في الخطوة الأخيرة، سنستخدم _FadeThroughTransitionSwitcher مرة أخرى. لتحقيق عملية النقل المطلوبة، انتقِل إلى تعريف الفئة _BottomAppBarActionItems وادمج التطبيق المصغّر لدالة build() باستخدام _FadeThroughTransitionSwitcher:

home.dart

// TODO: Add Fade through transition between bottom app bar actions (Motion)
return _FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: drawerVisible
     ? Align(
         key: UniqueKey(),
         alignment: AlignmentDirectional.bottomEnd,
         child: IconButton(
           icon: const Icon(Icons.settings),
           color: ReplyColors.white50,
           onPressed: () async {
             drawerController.reverse();
             showModalBottomSheet(
               context: context,
               shape: RoundedRectangleBorder(
                 borderRadius: modalBorder,
               ),
               builder: (context) => const SettingsBottomSheet(),
             );
           },
         ),
       )
     : onMailView
...

لنجربها الآن! عند فتح رسالة إلكترونية والانتقال إلى عرض الرسالة الإلكترونية، من المفترض أن تتلاشى إجراءات شريط التطبيق القديم في أسفل الشاشة وتصغر حجمها بينما تتلاشى الإجراءات الجديدة وتتوسّع. أحسنت!

بعد

cff0fa2afa1c5a7f.gif

12. تهانينا!

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

d5637de49eb64d8a.gif

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

للحصول على المزيد من المعلومات حول نظام Material motion، تأكد من مراجعة الإرشادات ومستندات مطوّري البرامج الكاملة، وحاول إضافة بعض أشكال الانتقال إلى Materials إلى تطبيقك.

نشكرك على تجربة Material motion. نأمل أن تكون قد استفدت من هذا الدرس التطبيقي حول الترميز.

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

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

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

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

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

46ba920f17198998.png

6ae8ae284bf4f9fa.png