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

1. مقدمة

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

logo_components_color_2x_web_96dp.png

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

ما هو نظام الحركة في Material لتطبيق Flutter؟

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

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

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

11807bdf36c66657.gif

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

71218f390abae07e.gif

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

385ba37b8da68969.gif

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

cfc40fd6e27753b6.gif

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

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

ما ستنشئه

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

سيتم توفير الرمز الأولي لتطبيق Reply، وستدمج انتقالات Material التالية في التطبيق، والتي يمكن رؤيتها في صورة GIF الخاصة بدرس البرمجة المكتمل أدناه:

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

b26fe84fed12d17d.gif

المتطلبات

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

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

مبتدئ متوسط متمكّن

ما الذي تريد تعلّمه من هذا الدرس العملي؟

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

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

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

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

  • جهاز 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. اتّبِع التعليمات الواردة في مقالة البدء: تجربة القيادة ضمن القسم "تشغيل التطبيق" في المحرّر الذي اخترته.

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

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

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

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

d23a7bfacffac509.gif

اختياري: الوضع الداكن

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

87618d8418eee19e.gif

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

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

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

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 لإعداد انتقالات Material التي تعمل بالتزامن مع إجراءات التنقّل المختلفة في جميع أنحاء التطبيق.

بعد أن أصبحت على دراية بالرمز الأولي، لننفّذ الآن أول انتقال.

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

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

قبل إضافة أي رمز، جرِّب تشغيل تطبيق 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. إضافة انتقال "تحويل الحاوية" من زر الإجراء الرئيسي (FAB) إلى صفحة إنشاء رسالة إلكترونية

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

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

قبل

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) بدون انتقال.

قبل

d8e3afa0447cfc20.gif

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

ستكون طريقة ضبط الانتقالات القليلة التالية مشابهة جدًا، لأنّها ستستخدم جميعًا فئة قابلة لإعادة الاستخدام، وهي _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. إضافة تأثير الانتقال "تلاشٍ" بين إجراءات شريط التطبيق السفلي

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

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

قبل

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، احرص على الاطّلاع على الإرشادات ومستندات المطوّرين الكاملة، وجرِّب إضافة بعض انتقالات Material إلى تطبيقك.

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

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

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

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

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

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

46ba920f17198998.png

6ae8ae284bf4f9fa.png