1. مقدمة
Material Design هو نظام لإنشاء منتجات رقمية جريئة وجميلة. من خلال الجمع بين الأسلوب والعلامة التجارية والتفاعل والحركة ضمن مجموعة متسقة من المبادئ والمكونات، يمكن لفِرق المنتجات تحقيق أقصى إمكانات التصميم.
تساعد المكونات المادية (MDC) المطورين على تنفيذ التصميم المتعدد الأبعاد. تم إنشاء MDC من قِبل فريق من المهندسين ومصممي تجربة المستخدم في Google، ويضمّ عشرات مكوّنات واجهة المستخدم الجميلة والوظيفية، وهو متاح لنظام التشغيل Android وiOS والويب وFlutter.material.io/develop |
ما هو نظام الحركة في Material لتطبيق Flutter؟
إنّ نظام الحركة المواد في تطبيق Flutter هو مجموعة من أنماط الانتقال ضمن حزمة الصور المتحركة التي يمكن أن تساعد المستخدمين في فهم التطبيق والتنقّل خلاله، كما هو موضّح في إرشادات التصميم المتعدد الأبعاد.
في ما يلي أنماط الانتقال الأربعة الرئيسية في Material:
- تحويل الحاوية: يُستخدم هذا التأثير للانتقال بين عناصر واجهة المستخدم التي تتضمّن حاوية، وينشئ رابطًا مرئيًا بين عنصرَين مختلفَين في واجهة المستخدم من خلال تحويل أحد العنصرَين بسلاسة إلى الآخر.
- المحور المشترك: يُستخدَم هذا النمط لتأثيرات الانتقال بين عناصر واجهة المستخدم التي تربطها علاقة انتقال أو علاقة مكانية، ويستخدم التحويل المشترك على محور x أو y أو z لتعزيز العلاقة بين العناصر.
- التلاشي التدريجي: يشير إلى الانتقالات بين عناصر واجهة المستخدم التي لا تربطها علاقة قوية ببعضها، ويستخدم التلاشي التدريجي للظهور والتلاشي التدريجي للاختفاء، مع مقياس للعنصر القادم.
- التلاشي: يُستخدَم لعناصر واجهة المستخدم التي تدخل أو تخرج داخل حدود الشاشة.
توفّر حزمة الرسوم المتحرّكة تطبيقات مصغّرة للتأثيرات الانتقالية لهذه الأنماط، وهي مبنية على كلّ من مكتبة الرسوم المتحرّكة في Flutter (flutter/animation.dart
) ومكتبة Flutter المادية (flutter/material.dart
):
في هذا الدرس البرمجي، ستستخدم عمليات النقل في Material المُنشأة على إطار عمل 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- تنزيل تطبيق 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
فتح المشروع وتشغيل التطبيق
- افتح المشروع في المحرِّر الذي تختاره.
- اتّبِع تعليمات "تشغيل التطبيق" في البدء: تجربة التطبيق للمحرِّر الذي اخترته.
اكتمال عملية النقل بنجاح يجب تشغيل رمز التفعيل للصفحة الرئيسية في Reply على جهازك أو المحاكي. من المفترض أن يظهر لك البريد الوارد الذي يحتوي على قائمة بالرسائل الإلكترونية.
اختياري: إبطاء الصور المتحركة على الجهاز
بما أنّ هذا الدليل التعليمي للترميز يتضمّن انتقالات سريعة ومهذَّبة، قد يكون من المفيد إبطاء الرسوم المتحرّكة على الجهاز لملاحظة بعض التفاصيل الدقيقة للانتقالات أثناء تنفيذها. ويمكن تنفيذ ذلك من خلال إعداد داخل التطبيق يمكن الوصول إليه بالنقر على رمز الإعدادات عندما يكون الدرج السفلي مفتوحًا. ولا داعي للقلق، فلن تؤثر طريقة إبطاء الصور المتحركة على الجهاز في الصور المتحركة على الجهاز خارج تطبيق "الرد".
اختياري: الوضع الداكن
إذا كان المظهر المشرق لتطبيق "ردّ" يؤذي عينيك، إليك الحلّ. يتوفّر إعداد مضمّن داخل التطبيق يتيح لك تغيير مظهر التطبيق إلى الوضع الداكن، بما يناسب عينيك بشكلٍ أفضل. يمكن الوصول إلى هذا الإعداد من خلال النقر على رمز الإعدادات عندما يكون الدرج السفلي مفتوحًا.
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 والنقر على رسالة إلكترونية. يجب أن يتم الانتقال سريعًا، أي يتم استبدال الشاشة بدون انتقال:
قبل
ابدأ بإضافة استيراد لحزمة الرسوم المتحرّكة في أعلى 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- إضافة انتقال تحويل الحاوية من التطبيق المصغّر في الشريط الجانبي إلى صفحة كتابة الرسالة الإلكترونية
لنواصل تحويل الحاوية ونضيف انتقالًا من زرّ الإجراء العائم إلى ComposePage
توسيع زرّ الإجراء العائم إلى رسالة إلكترونية جديدة ليكتبها المستخدم. أولاً، أعِد تشغيل التطبيق وانقر على التطبيق المصغّر في الشريط الجانبي للتأكّد من عدم حدوث انتقال عند فتح شاشة كتابة الرسالة الإلكترونية.
قبل
ستكون طريقة ضبط هذا الانتقال مشابهة جدًا للطريقة التي اتّبعناها في الخطوة الأخيرة، لأنّنا نستخدم فئة التطبيق المصغّر نفسها، وهي 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) بدون عملية انتقال.
قبل
سنستخدم 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
وربطناه بأحد الرموز.
في هذه المرحلة، من المفترض أن يكون لديك رمز عملي للتطبيق في سياق متحرك بالكامل. يؤدي الانتقال إلى عرض البريد الإلكتروني إلى تلاشي رمز الإجراء الرئيسي القديم وتصغير حجمه بينما يتلاشى الرمز الجديد ويتغيّر لونه.
بعد
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. إضافة انتقال تمويه بين إجراءات شريط التطبيقات السفلي
في هذه الخطوة، سنضيف انتقالًا مموّهًا، لتتم محو إجراءات شريط التطبيقات السفلي استنادًا إلى سياق التطبيقات. بما أنّنا لا نريد التأكيد على علاقة مكانية أو هرمية، سنستخدم ميزة التمويه لإجراء "تبديل" بسيط بين إجراءات شريط التطبيقات السفلي عندما يكون التطبيق في الصفحة الرئيسية، وعندما يكون الدرج السفلي مرئيًا، وعندما نكون في عرض الرسائل الإلكترونية.
قبل إضافة أي رمز إضافي، جرِّب تشغيل التطبيق والنقر على رسالة إلكترونية وفتح طريقة عرض الرسالة الإلكترونية. يمكنك أيضًا محاولة النقر على شعار الردّ. يجب أن تتغيّر إجراءات شريط التطبيقات السفلي بدون انتقال.
قبل
كما في الخطوة الأخيرة، سنستخدم _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 Design، كما أنّه يبدو ويتصرف بشكلٍ متسق على جميع الأجهزة.
الخطوات التالية
للحصول على المزيد من المعلومات حول نظام Material motion، تأكد من مراجعة الإرشادات ومستندات مطوّري البرامج الكاملة، وحاول إضافة بعض أشكال الانتقال إلى Materials إلى تطبيقك.
نشكرك على تجربة ميزة "الحركة في واجهة المستخدم". نأمل أن تكون قد استفدت من هذا الدرس التطبيقي حول الترميز.
تمكّنت من إكمال هذا الدليل التعليمي عن البرمجة باستخدام مقدار معقول من الوقت والجهد.
أريد مواصلة استخدام نظام الحركة في Material في المستقبل
الاطّلاع على "معرض Flutter"
لمزيد من العروض التوضيحية حول كيفية استخدام التطبيقات المصغّرة التي تقدّمها مكتبة Material Flutter، بالإضافة إلى إطار عمل Flutter، يُرجى زيارة معرض Flutter. |