1. مقدمة
التصميم المتعدد الأبعاد هو نظام لتصميم منتجات رقمية جريئة وجميلة. من خلال توحيد الأسلوب والعلامات التجارية والتفاعل والحركة في ظل مجموعة متسقة من المبادئ والمكونات، يمكن لفرق المنتجات تحقيق أكبر إمكانات التصميم.
تساعد المكونات المادية (MDC) المطورين على تنفيذ التصميم المتعدد الأبعاد. يضم مركز MDC، الذي أنشأه فريق من المهندسين ومصممي تجربة المستخدم في Google، عشرات من مكونات واجهة المستخدم الجميلة والعملية، وهو متاح لأجهزة Android وiOS والويب وFlutter.material.io/develop |
ما هو نظام الحركة المتعدد الأبعاد في Flutter؟
إنّ نظام الحركة المواد في تطبيق Flutter عبارة عن مجموعة من أنماط الانتقال ضمن حزمة الصور المتحركة التي يمكن أن تساعد المستخدمين في فهم التطبيق والتنقّل خلاله، كما هو موضّح في إرشادات التصميم المتعدد الأبعاد.
في ما يلي أنماط انتقال المواد الأربعة الرئيسية:
- تحويل الحاوية: الانتقالات بين عناصر واجهة المستخدم التي تحتوي على حاوية؛ إنشاء اتصال مرئي بين عنصرين متميزين لواجهة المستخدم عن طريق تحويل عنصر إلى آخر بسلاسة.
- المحور المشترك: يشير إلى الانتقالات بين عناصر واجهة المستخدم التي تربطها علاقة مكانية أو تنقل. تستخدم التحويل المشترك على المحور س أو ص أو ع لتعزيز العلاقة بين العناصر.
- التلاشي خلال: الانتقالات بين عناصر واجهة المستخدم التي لا تربطها علاقة قوية ببعضها تستخدم تلاشي متسلسل وتلاشي للداخل، بمقياس للعنصر الوارد.
- التلاشي:يُستخدَم هذا النوع لعناصر واجهة المستخدم التي تدخل أو تخرج داخل حدود الشاشة.
توفّر حزمة الصور المتحركة أدوات انتقال لهذه الأنماط تم تصميمها استنادًا إلى مكتبة الصور المتحركة في Flutter (flutter/animation.dart
) ومكتبة مواد Flutter (flutter/material.dart
):
سوف تستخدم في هذا الدرس التطبيقي حول الترميز انتقالات Materials التي تم إنشاؤها في إطار عمل Flutter ومكتبة Material، أي أنك ستتعامل مع التطبيقات المصغّرة. :)
ما الذي ستنشئه
يتضمّن هذا الدرس التطبيقي حول الترميز خطوات إرشادية لإنشاء بعض الانتقالات إلى مثال تطبيق بريد إلكتروني من Flutter يُسمى Reply (رد) باستخدام Dart لتوضيح كيفية استخدام الانتقالات من حزمة الصور المتحركة لتخصيص شكل ومظهر تطبيقك.
وسيتم توفير رمز التفعيل لتطبيق Reply، كما ستدمج انتقالات Material التالية في التطبيق، والتي يمكن الاطلاع عليها في ملف GIF للدرس التطبيقي حول الترميز الذي تم إكماله أدناه:
- نقل تحويل الحاوية من قائمة عناوين البريد الإلكتروني إلى صفحة تفاصيل البريد الإلكتروني
- نقل Container Transform من الإجراء إلى الإجراء الرئيسي (FAB) لإنشاء صفحة بريد إلكتروني.
- انتقال المحور ي المشترك من رمز البحث إلى صفحة عرض البحث
- التلاشي خلال الانتقال بين صفحات صندوق البريد
- التلاشي خلال للانتقال بين زر الإجراء الرئيسي (FAB) للإنشاء والرد
- التلاشي خلال الانتقال بين عنوان صندوق البريد المخفي
- التلاشي حتى للانتقال بين إجراءات شريط التطبيق السفلي
المتطلبات
- معرفة أساسية بتطوير 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
فتح المشروع وتشغيل التطبيق
- افتح المشروع في المحرِّر الذي تختاره.
- اتّبِع التعليمات من أجل "تشغيل التطبيق" في البدء: اختبار القيادة للمحرِّر الذي اخترته.
اكتمال عملية النقل بنجاح يجب أن يعمل رمز إجراء التفعيل للصفحة الرئيسية لـ Reply على جهازك/المحاكي. من المفترض أن يظهر لك البريد الوارد الذي يحتوي على قائمة بالرسائل الإلكترونية.
اختياري: إبطاء الصور المتحركة على الجهاز
بما أنّ هذا الدرس التطبيقي حول الترميز يتضمّن عمليات انتقال سريعة ومحسّنة في الوقت نفسه، قد يكون من المفيد إبطاء الصور المتحركة على الجهاز لملاحظة بعض التفاصيل الدقيقة لعمليات الانتقال أثناء التنفيذ. ويمكن تنفيذ ذلك من خلال إعداد داخل التطبيق يمكن الوصول إليه بالنقر على رمز الإعدادات عندما يكون الدرج السفلي مفتوحًا. ولا داعي للقلق، فلن تؤثر طريقة إبطاء الصور المتحركة على الجهاز في الصور المتحركة على الجهاز خارج تطبيق "الرد".
اختياري: الوضع المُعتِم
إذا كان المظهر الزاهي لـ "الرد" يؤثر سلبًا في عينيك، فلا داعي للقلق. يتوفّر إعداد مضمَّن في التطبيق يسمح لك بتغيير مظهر التطبيق إلى الوضع الداكن، وذلك ليلائم عينيك بشكل أفضل. يمكن الوصول إلى هذا الإعداد من خلال النقر على رمز الإعدادات عندما يكون الدرج السفلي مفتوحًا.
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 والنقر على رسالة إلكترونية. يجب أن يتم الانتقال سريعًا، أي يتم استبدال الشاشة بدون انتقال:
قبل
ابدأ بإضافة عملية استيراد لحزمة الصور المتحركة في أعلى 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,
),
);
في هذه المرحلة، يُفترض أن يكون لديك تحويل حاوية يعمل بكامل طاقته. يؤدي النقر على رسالة بريد إلكتروني إلى توسيع عنصر القائمة إلى شاشة التفاصيل مع التراجع عن قائمة رسائل البريد الإلكتروني. يؤدي الضغط على زر الرجوع إلى طي شاشة تفاصيل البريد الإلكتروني مرة أخرى إلى عنصر قائمة أثناء توسيع نطاق قائمة رسائل البريد الإلكتروني.
بعد
6- إضافة نقل Container Transform من زر الإجراء الرئيسي (FAB) لإنشاء صفحة بريد إلكتروني
لنواصل تحويل الحاوية وإضافة تأثير انتقال من زر الإجراء العائم إلى ComposePage
لتوسيع زر الإجراء الرئيسي (FAB) إلى رسالة إلكترونية جديدة ليكتبها المستخدم. أولاً، أعد تشغيل التطبيق وانقر على زر الإجراء الرئيسي (FAB) للتأكد من عدم وجود انتقال عند تشغيل شاشة إنشاء الرسالة الإلكترونية.
قبل
وستكون طريقة ضبط هذا الانتقال مشابهة جدًا للطريقة التي اتّبعناها في الخطوة الأخيرة، نظرًا لأننا نستخدم فئة الأداة نفسها 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 لإنشاء شاشة تشبه ما يلي:
بعد
7. إضافة تأثير محور ي مشترَك من رمز البحث إلى صفحة عرض البحث
في هذه الخطوة، سنضيف طريقة انتقال من رمز البحث إلى طريقة عرض البحث بملء الشاشة. ونظرًا لعدم وجود حاوية دائمة متضمنة في تغيير التنقل هذا، يمكننا استخدام انتقال المحور Z-Z المشترك لتعزيز العلاقة المكانية بين الشاشتين والإشارة إلى الانتقال بمستوى واحد لأعلى في التسلسل الهرمي للتطبيق.
قبل إضافة رمز إضافي، جرِّب تشغيل التطبيق والنقر على رمز البحث في أسفل يسار الشاشة. من المفترض أن يؤدي هذا إلى إظهار شاشة عرض البحث بدون انتقال.
قبل
للبدء، دعنا ننتقل إلى ملف 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(),
),
],
);
يمكنك الآن إعادة تشغيل التطبيق.
بدأت الأمور تبدو رائعة. عند النقر على رمز البحث في شريط التطبيق السفلي، يؤدي انتقال محور مشترك إلى تغيير حجم صفحة البحث إلى إطار العرض. مع ذلك، لاحِظ كيف لا يتم توسيع نطاق الصفحة الرئيسية وتظل ثابتة عندما يتّسع نطاق صفحة البحث. بالإضافة إلى ذلك، عند الضغط على زر الرجوع، لا يتم تغيير حجم الصفحة الرئيسية إلى وضع العرض، بل تظل ثابتة عندما يتم تكبير حجم صفحة البحث. لذلك لم ننتهي بعد.
لنصلح كلا المشكلتين من خلال إدراج 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 بعمق، مما يخلق تأثيرًا سلسًا بين الشاشتين.
بعد
8. إضافة تأثير التلاشي خلال الانتقال بين صفحات صندوق البريد
في هذه الخطوة، سنضيف إمكانية الانتقال بين صناديق البريد المختلفة. ونظرًا لأننا لا نريد التأكيد على علاقة مكانية أو هرمية، سنستخدم ميزة التلاشي لإجراء "تبديل" بسيط بين قوائم رسائل البريد الإلكتروني.
قبل إضافة أي رمز إضافي، جرّب تشغيل التطبيق والنقر على شعار "رد" في شريط التطبيق السفلي، وتبديل صناديق البريد. يجب أن تتغير قائمة عناوين البريد الإلكتروني بدون أي عمليات انتقال.
قبل
للبدء، دعنا ننتقل إلى ملف 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),
),
],
);
إعادة تشغيل التطبيق عند فتح درج التنقل السفلي وتغيير صناديق البريد، من المفترض أن تتلاشى القائمة الحالية للرسائل الإلكترونية وتتسع بينما تتلاشى القائمة الجديدة وتصبح كبيرة. أحسنت.
بعد
9. إضافة تأثير التلاشي من خلال الانتقال بين الإنشاء والرد على زر الإجراء الرئيسي (FAB)
في هذه الخطوة، سنضيف انتقالاً بين أيقونات FAB المختلفة. ونظرًا لأننا لا نريد التأكيد على علاقة مكانية أو هرمية، سنستخدم ميزة التلاشي لإجراء "تبديل" بسيط بين الأيقونات في FAB.
قبل إضافة أي رمز إضافي، جرِّب تشغيل التطبيق والنقر على رسالة إلكترونية وفتح طريقة عرض الرسالة الإلكترونية. يجب أن يتغير رمز زر الإجراء الرئيسي (FAB) بدون عملية انتقال.
قبل
وسنعمل على استخدام "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) لسياقي بالكامل. يؤدي الانتقال إلى عرض البريد الإلكتروني إلى تلاشي رمز الإجراء الرئيسي القديم وتصغير حجمه بينما يتلاشى الرمز الجديد ويتغيّر لونه.
بعد
10. إضافة تلاشي من خلال الانتقال بين عنوان صندوق البريد المخفي
في هذه الخطوة، ستتم إضافة تلاشي أثناء انتقالي، للتلاشي خلال عنوان صندوق البريد بين الحالة المرئية وغير المرئية عند استخدام عرض البريد الإلكتروني. ونظرًا لأننا لا نريد التأكيد على علاقة مكانية أو هرمية، سنستخدم ميزة التلاشي لإجراء "تبديل" بسيط بين التطبيق المصغّر "Text
" الذي يشمل عنوان صندوق البريد الإلكتروني وSizedBox
فارغ.
قبل إضافة أي رمز إضافي، جرِّب تشغيل التطبيق والنقر على رسالة إلكترونية وفتح طريقة عرض الرسالة الإلكترونية. ومن المفترض أن يختفي عنوان صندوق البريد الإلكتروني بدون عملية انتقال.
قبل
سيكون باقي هذا الدرس التطبيقي حول الترميز سريعًا بما أنّنا أجرينا معظم العمل في _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,
),
);
},
),
),
),
لقد انتهينا من هذه الخطوة!
إعادة تشغيل التطبيق عندما تفتح رسالة إلكترونية ويتم نقلك إلى عرض البريد الإلكتروني، من المفترض أن يتلاشى عنوان صندوق البريد في شريط التطبيق السفلي ويتسعل نطاقه. رائع!
بعد
11. إضافة تأثير التلاشي خلال الانتقال بين إجراءات شريط التطبيق السفلي
في هذه الخطوة، سنضيف تأثير التلاشي خلال الانتقال، للتلاشي من خلال إجراءات شريط التطبيق السفلي استنادًا إلى سياق التطبيقات. ونظرًا لأننا لا نريد التأكيد على علاقة مكانية أو هرمية، سنستخدم ميزة التلاشي لإجراء "تبديل" بسيط بين إجراءات شريط التطبيق السفلي عندما يكون التطبيق في الصفحة الرئيسية، وعندما يكون الدرج السفلي مرئيًا، وعندما نكون في عرض البريد الإلكتروني.
قبل إضافة أي رمز إضافي، جرِّب تشغيل التطبيق والنقر على رسالة إلكترونية وفتح طريقة عرض الرسالة الإلكترونية. يمكنك أيضًا تجربة النقر على شعار Reply. من المفترض أن تتغير إجراءات شريط التطبيق السفلي بدون نقل.
قبل
كما في الخطوة الأخيرة، سنستخدم _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
...
لنجربها الآن! عند فتح رسالة إلكترونية والانتقال إلى عرض الرسالة الإلكترونية، من المفترض أن تتلاشى إجراءات شريط التطبيق القديم في أسفل الشاشة وتصغر حجمها بينما تتلاشى الإجراءات الجديدة وتتوسّع. أحسنت!
بعد
12. تهانينا!
باستخدام أقل من 100 سطر من رمز Dart، ساعدتك حزمة الرسوم المتحركة في إنشاء انتقالات رائعة في أحد التطبيقات الحالية التي تتوافق مع إرشادات التصميم المتعدد الأبعاد، بالإضافة إلى مظهرها وسلوكها بشكل متسق عبر جميع الأجهزة.
الخطوات التالية
للحصول على المزيد من المعلومات حول نظام Material motion، تأكد من مراجعة الإرشادات ومستندات مطوّري البرامج الكاملة، وحاول إضافة بعض أشكال الانتقال إلى Materials إلى تطبيقك.
نشكرك على تجربة Material motion. نأمل أن تكون قد استفدت من هذا الدرس التطبيقي حول الترميز.
تمكنتُ من إكمال هذا الدرس التطبيقي حول الترميز بقدرٍ معقول من الوقت والجهد
أود مواصلة استخدام نظام Material motion في المستقبل
الاطّلاع على معرض Flutter Gallery
للاطّلاع على المزيد من العروض التوضيحية حول كيفية استخدام التطبيقات المصغّرة التي توفّرها مكتبة Material Flutter، بالإضافة إلى إطار عمل Flutter، يُرجى الانتقال إلى معرض Flutter. |