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

1. مقدمة

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

logo_components_color_2x_web_96dp.png

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

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

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

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

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

11807bdf36c66657.gif

  • المحور المشترك: يُستخدَم هذا النمط لتأثيرات الانتقال بين عناصر واجهة المستخدم التي تربطها علاقة انتقال أو علاقة مكانية، ويستخدم التحويل المشترك على محور x أو y أو z لتعزيز العلاقة بين العناصر.

71218f390amae07e.gif

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

385ba37b8da68969.gif

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

cfc40fd6e27753b6.gif

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

في هذا الدرس البرمجي، ستستخدم عمليات النقل في Material المُنشأة على إطار عمل 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- تنزيل تطبيق Codelab Starter

الخيار 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. التعرّف على نموذج رمز التطبيق

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

  • HomePage: لعرض صندوق البريد الإلكتروني المحدّد
  • 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 لإعداد انتقالات المواد التي تعمل جنبًا إلى جنب مع إجراءات التنقل المختلفة في جميع أنحاء التطبيق.

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

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

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

قبل

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

قبل

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,
   );
 }
}

الآن، ابحث عن التطبيق المصغّر fabSwitcher في _ReplyFabState. يعرض الرمز 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 وربطناه بأحد الرموز.

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

بعد

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

d5637de49eb64d8a.gif

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

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

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

تمكّنت من إكمال هذا الدليل التعليمي عن البرمجة باستخدام مقدار معقول من الوقت والجهد.

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

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

أوافق بشدة أوافق محايد لا أوافق لا أوافق أبدًا

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

46ba920f17198998.png

6ae8ae284bf4f9fa.png