ساختن ترانزیشن های زیبا با حرکت مواد برای فلاتر

۱. مقدمه

طراحی متریال سیستمی برای ساخت محصولات دیجیتال جسورانه و زیبا است. با متحد کردن سبک، برندسازی، تعامل و حرکت تحت مجموعه‌ای از اصول و اجزای سازگار، تیم‌های محصول می‌توانند به بزرگترین پتانسیل طراحی خود دست یابند.

logo_components_color_2x_web_96dp.png

کامپوننت‌های متریال (MDC) به توسعه‌دهندگان در پیاده‌سازی طراحی متریال کمک می‌کنند. MDC که توسط تیمی از مهندسان و طراحان UX در گوگل ایجاد شده است، ده‌ها کامپوننت رابط کاربری زیبا و کاربردی را ارائه می‌دهد و برای اندروید، iOS، وب و Flutter.material.io/develop در دسترس است.

سیستم حرکتی متریال برای فلاتر چیست؟

سیستم حرکت متریال برای فلاتر مجموعه‌ای از الگوهای انتقال در بسته انیمیشن‌ها است که می‌تواند به کاربران در درک و پیمایش یک برنامه کمک کند، همانطور که در دستورالعمل‌های طراحی متریال توضیح داده شده است.

چهار الگوی اصلی انتقال مواد به شرح زیر است:

  • تبدیل کانتینر: انتقال بین عناصر رابط کاربری که شامل یک کانتینر هستند؛ با تبدیل یکپارچه یک عنصر به عنصر دیگر، یک اتصال قابل مشاهده بین دو عنصر رابط کاربری مجزا ایجاد می‌کند.

۱۱۸۰۷bdf36c66657.gif

  • محور مشترک: انتقال بین عناصر رابط کاربری که رابطه مکانی یا ناوبری دارند؛ از یک تبدیل مشترک روی محور x، y یا z برای تقویت رابطه بین عناصر استفاده می‌کند.

71218f390abae07e.gif

  • محو شدن تدریجی (Fade Through): انتقال بین عناصر رابط کاربری که رابطه‌ی قوی با یکدیگر ندارند؛ از محو شدن متوالی به بیرون و داخل، با مقیاسی متناسب با عنصر ورودی، استفاده می‌کند.

385ba37b8da68969.gif

  • محو شدن: برای عناصر رابط کاربری که وارد یا خارج از محدوده صفحه نمایش می‌شوند، استفاده می‌شود.

cfc40fd6e27753b6.gif

بسته انیمیشن‌ها، ویجت‌های انتقال را برای این الگوها ارائه می‌دهد که بر اساس کتابخانه انیمیشن‌های فلاتر ( flutter/animation.dart ) و کتابخانه متریال فلاتر ( flutter/material.dart ) ساخته شده‌اند:

در این آزمایشگاه کد، شما از انتقال‌های متریال ساخته شده بر روی چارچوب فلاتر و کتابخانه متریال استفاده خواهید کرد، به این معنی که با ویجت‌ها سر و کار خواهید داشت. :)

آنچه خواهید ساخت

این آزمایشگاه کد شما را در ساخت برخی از انتقال‌ها در یک برنامه ایمیل نمونه Flutter به نام Reply با استفاده از Dart راهنمایی می‌کند تا نشان دهد چگونه می‌توانید از انتقال‌ها از بسته animations برای سفارشی‌سازی ظاهر و حس برنامه خود استفاده کنید.

کد آغازین برای برنامه Reply ارائه خواهد شد و شما انتقال‌های متریال زیر را در برنامه وارد خواهید کرد که می‌توانید در GIF تکمیل‌شده codelab در زیر مشاهده کنید:

  • انتقال کانتینر از لیست ایمیل به صفحه جزئیات ایمیل
  • انتقال کانتینر از FAB به صفحه نوشتن ایمیل
  • انتقال محور Z مشترک از آیکون جستجو به صفحه نمایش جستجو
  • محو شدن در انتقال بین صفحات صندوق پستی
  • گذار محوشونده بین نوشتن و پاسخ دادن FAB
  • محو شدن از طریق گذار بین عنوان صندوق پستی ناپدید شونده
  • محو شدن در حین انتقال بین اقدامات نوار برنامه پایین

b26fe84fed12d17d.gif

آنچه نیاز دارید

  • دانش پایه در توسعه Flutter و Dart
  • یک ویرایشگر کد
  • یک شبیه‌ساز یا دستگاه اندروید/iOS
  • کد نمونه (به مرحله بعدی مراجعه کنید)

سطح تجربه خود در ساخت برنامه‌های Flutter را چگونه ارزیابی می‌کنید؟

تازه کار متوسط ماهر

دوست دارید از این آزمایشگاه کد چه چیزهایی یاد بگیرید؟

من تازه وارد این مبحث شدم و یه دید کلی و جامع می‌خوام. من یه چیزایی در مورد این موضوع می‌دونم، اما می‌خوام یه مرور کلی بکنم. دنبال یه نمونه کد هستم که تو پروژه ام ازش استفاده کنم. دنبال توضیح یه چیز خاص هستم.

۲. محیط توسعه فلاتر خود را تنظیم کنید

برای تکمیل این آزمایشگاه به دو نرم‌افزار نیاز دارید - SDK فلاتر و یک ویرایشگر .

شما می‌توانید codelab را با استفاده از هر یک از این دستگاه‌ها اجرا کنید:

  • یک دستگاه فیزیکی اندروید یا iOS که به رایانه شما متصل شده و روی حالت توسعه‌دهنده (Developer mode) تنظیم شده باشد.
  • شبیه‌ساز iOS (نیاز به نصب ابزارهای Xcode دارد).
  • شبیه‌ساز اندروید (نیاز به راه‌اندازی در اندروید استودیو دارد).
  • یک مرورگر (برای اشکال‌زدایی، کروم مورد نیاز است).
  • به عنوان یک برنامه دسکتاپ ویندوز ، لینوکس یا macOS . شما باید روی پلتفرمی که قصد استقرار آن را دارید، توسعه دهید. بنابراین، اگر می‌خواهید یک برنامه دسکتاپ ویندوز توسعه دهید، باید روی ویندوز توسعه دهید تا به زنجیره ساخت مناسب دسترسی داشته باشید. الزامات خاص سیستم عامل وجود دارد که به تفصیل در docs.flutter.dev/desktop پوشش داده شده است.

۳. اپلیکیشن شروع کدلب را دانلود کنید

گزینه ۱: کپی کردن برنامه اولیه codelab از گیت‌هاب

برای کپی کردن این codelab از گیت‌هاب، دستورات زیر را اجرا کنید:

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

گزینه ۲: فایل زیپ برنامه codelab را دانلود کنید

برنامه‌ی آغازین در دایرکتوری material-components-flutter-motion-codelab-starter قرار دارد.

وابستگی‌های پروژه را تأیید کنید

این پروژه به بسته animations وابسته است. در فایل pubspec.yaml ، توجه داشته باشید که بخش dependencies شامل موارد زیر است:

animations: ^2.0.0

پروژه را باز کنید و برنامه را اجرا کنید

  1. پروژه را در ویرایشگر دلخواه خود باز کنید.
  2. دستورالعمل‌های «اجرای برنامه» را در بخش «شروع به کار: تست درایو » برای ویرایشگر انتخابی خود دنبال کنید.

موفقیت! کد آغازین برای صفحه اصلی Reply باید روی دستگاه/شبیه‌ساز شما اجرا شود. باید صندوق ورودی حاوی لیستی از ایمیل‌ها را ببینید.

صفحه اصلی پاسخ

اختیاری: کاهش سرعت انیمیشن‌های دستگاه

از آنجایی که این آزمایشگاه کد شامل انتقال‌های سریع اما دقیق است، می‌تواند مفید باشد که انیمیشن‌های دستگاه را کند کنید تا جزئیات دقیق‌تر انتقال‌ها را هنگام پیاده‌سازی مشاهده کنید. این کار را می‌توان از طریق تنظیمات درون برنامه‌ای انجام داد که از طریق ضربه زدن روی نماد تنظیمات هنگام باز بودن کشوی پایین قابل دسترسی است. نگران نباشید، این روش کند کردن انیمیشن‌های دستگاه، انیمیشن‌های دستگاه را در خارج از برنامه پاسخ تحت تأثیر قرار نمی‌دهد.

d23a7bfacffac509.gif

اختیاری: حالت تاریک

اگر تم روشن Reply چشمان شما را اذیت می‌کند، دیگر نیازی به جستجو نیست. یک تنظیم درون برنامه‌ای وجود دارد که به شما امکان می‌دهد تم برنامه را به حالت تاریک تغییر دهید تا برای چشمان شما مناسب‌تر باشد. این تنظیم با ضربه زدن روی نماد تنظیمات هنگام باز بودن کشوی پایین قابل دسترسی است.

۸۷۶۱۸d۸۴۱۸eee۱۹e.gif

۴. با کد نمونه برنامه آشنا شوید

بیایید به کد نگاه کنیم. ما برنامه‌ای ارائه داده‌ایم که از بسته animations برای انتقال بین صفحات مختلف در برنامه استفاده می‌کند.

  • صفحه اصلی: صندوق پستی انتخاب شده را نمایش می‌دهد
  • InboxPage : فهرستی از ایمیل‌ها را نمایش می‌دهد.
  • MailPreviewCard : پیش‌نمایشی از یک ایمیل را نمایش می‌دهد.
  • MailViewPage: یک ایمیل کامل و واحد را نمایش می‌دهد.
  • ComposePage: امکان نوشتن یک ایمیل جدید را فراهم می‌کند.
  • صفحه جستجو: نمای جستجو را نمایش می‌دهد

روتر.دارت

ابتدا، برای درک نحوه تنظیم ناوبری ریشه برنامه، 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 قرار گرفته‌اند که هیچ انتقالی برای آن تعریف نشده است. این به شما یک راه برای پیمایش بین صفحات بدون هیچ انتقال سفارشی را نشان می‌دهد.

خانه.دارت

ما مسیر خود را در حالت برنامه با انجام کارهای زیر درون _BottomAppBarActionItems در home.dart به ReplySearchPath تنظیم می‌کنیم:

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 از حالت برنامه ما ایجاد شود، بازسازی می‌شود.

خانه.دارت

ما صندوق پستی فعلی خود را با انجام کارهای زیر درون _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 را در دایرکتوری lib باز کنید. کلاس _ReplyFabState را در داخل ویژگی onTap ویجت InkWell پیدا کنید، که باید به شکل زیر باشد:

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

این نشان می‌دهد که چگونه می‌توانید بدون هیچ گونه انتقال سفارشی به صفحه نوشتن ایمیل بروید. در طول این آزمایشگاه کد، به کد Reply خواهید پرداخت تا انتقال‌های Material را تنظیم کنید که همزمان با اقدامات مختلف ناوبری در سراسر برنامه کار کنند.

حالا که با کد آغازین آشنا شدید، بیایید اولین گذار خود را پیاده‌سازی کنیم.

۵. اضافه کردن کانتینر تبدیل انتقال از لیست ایمیل به صفحه جزئیات ایمیل

برای شروع، هنگام کلیک روی یک ایمیل، یک گذار اضافه خواهید کرد. برای این تغییر ناوبری، الگوی تبدیل کانتینر (container transform pattern) مناسب است، زیرا برای گذار بین عناصر رابط کاربری که شامل یک کانتینر هستند طراحی شده است. این الگو یک اتصال قابل مشاهده بین دو عنصر رابط کاربری ایجاد می‌کند.

قبل از اضافه کردن هر کدی، برنامه Reply را اجرا کنید و روی یک ایمیل کلیک کنید. باید یک پرش ساده انجام شود، به این معنی که صفحه بدون هیچ انتقالی جایگزین می‌شود:

قبل از

48b00600f73c7778.gif

با اضافه کردن یک import برای پکیج animations در بالای mail_card_preview.dart همانطور که در قطعه کد زیر نشان داده شده است، شروع کنید:

پیش‌نمایش کارت_پستی.dart

import 'package:animations/animations.dart';

حالا که پکیج animations را ایمپورت کرده‌اید، می‌توانیم شروع به اضافه کردن transitionهای زیبا به برنامه‌تان کنیم. بیایید با ایجاد یک کلاس StatelessWidget که ویجت OpenContainer ما را در خود جای می‌دهد، شروع کنیم.

در mail_card_preview.dart ، قطعه کد زیر را بعد از تعریف کلاس MailPreviewCard اضافه کنید:

پیش‌نمایش کارت_پستی.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,
       );
     },
   );
 }
}

حالا بیایید از wrapper جدیدمان استفاده کنیم. درون تعریف کلاس MailPreviewCard ویجت Material را از تابع build() خود با _OpenContainerWrapper جدیدمان wrap خواهیم کرد:

پیش‌نمایش کارت_پستی.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 را حذف کنیم. کد حاصل به صورت زیر خواهد بود:

پیش‌نمایش کارت_پستی.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

۶. اضافه کردن انتقال Container Transform از FAB به صفحه نوشتن ایمیل

بیایید با تبدیل کانتینر ادامه دهیم و یک انتقال از دکمه اکشن شناور به ComposePage اضافه کنیم تا FAB را به یک ایمیل جدید که قرار است توسط کاربر نوشته شود، گسترش دهیم. ابتدا برنامه را دوباره اجرا کنید و روی FAB کلیک کنید تا ببینید هنگام راه‌اندازی صفحه نوشتن ایمیل هیچ انتقالی وجود ندارد.

قبل از

4aa2befdc5170c60.gif

نحوه پیکربندی این انتقال بسیار شبیه به نحوه انجام آن در مرحله قبل خواهد بود، زیرا ما از همان کلاس ویجت، یعنی OpenContainer ، استفاده می‌کنیم.

در home.dart ، package:animations/animations.dart را در بالای فایل وارد می‌کنیم و متد _ReplyFabState build() را تغییر می‌دهیم. ویجت Material برگردانده شده را با یک ویجت OpenContainer پوشش می‌دهیم:

خانه.دارت

// 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 مسیریابی خود را مدیریت می‌کند.

کد حاصل به صورت زیر است:

خانه.دارت

// 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 ما اکنون از طریق onClosed ClosedCallback به ارائه‌دهنده برنامه‌مان اطلاع می‌دهد که دیگر در ComposePage نیستیم، می‌توانیم پیاده‌سازی قبلی خود را در 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

۷. اضافه کردن انتقال محور Z مشترک از آیکون جستجو به صفحه نمایش جستجو

در این مرحله، یک گذار از آیکون جستجو به نمای جستجوی تمام صفحه اضافه خواهیم کرد. از آنجایی که هیچ کانتینر پایداری در این تغییر ناوبری دخیل نیست، می‌توانیم از یک گذار Shared Z-Axis برای تقویت رابطه مکانی بین دو صفحه نمایش و نشان دادن حرکت یک سطح به سمت بالا در سلسله مراتب برنامه استفاده کنیم.

قبل از اضافه کردن کد اضافی، برنامه را اجرا کنید و روی آیکون جستجو در گوشه پایین سمت راست صفحه ضربه بزنید. این کار باید صفحه نمای جستجو را بدون هیچ انتقالی نمایش دهد.

قبل از

df7683a8ad7b920e.gif

برای شروع، به فایل router.dart خود می‌رویم. پس از تعریف کلاس ReplySearchPath ، قطعه کد زیر را اضافه می‌کنیم:

روتر.دارت

// 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 ، بیایید صفحه جستجوی خود را به جای CustomTransitionPage با یک SharedAxisTransitionPageWrapper پوشش دهیم:

روتر.دارت

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 حل کنیم:

روتر.دارت

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

۸. اضافه کردن گذار محوشونده بین صفحات صندوق پستی

در این مرحله، یک انتقال بین صندوق‌های پستی مختلف اضافه خواهیم کرد. از آنجایی که نمی‌خواهیم بر یک رابطه مکانی یا سلسله مراتبی تأکید کنیم، از یک محوشدگی برای انجام یک "جابجایی" ساده بین لیست‌های ایمیل استفاده خواهیم کرد.

قبل از اضافه کردن هرگونه کد اضافی، برنامه را اجرا کنید، روی لوگوی پاسخ در نوار پایین برنامه ضربه بزنید و صندوق‌های پستی را تغییر دهید. لیست ایمیل‌ها باید بدون هیچ تغییری تغییر کند.

قبل از

۸۹۰۳۳۹۸۸ce26b92e.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),
   ),
 ],
);

برنامه را دوباره اجرا کنید. وقتی کشوی ناوبری پایین را باز می‌کنید و صندوق‌های پستی را تغییر می‌دهید، لیست فعلی ایمیل‌ها باید محو و کوچک شود در حالی که لیست جدید محو و بزرگ می‌شود. عالی!

بعد از

۸۱۸۶۹۴۰۰۸۲b۶۳۰d.gif

۹. اضافه کردن گذار محوشونده بین نوشتن و پاسخ دادن FAB

در این مرحله، یک گذار بین آیکون‌های مختلف FAB اضافه خواهیم کرد. از آنجایی که نمی‌خواهیم بر یک رابطه مکانی یا سلسله مراتبی تأکید کنیم، از یک محوشدگی برای انجام یک "جابجایی" ساده بین آیکون‌ها در FAB استفاده خواهیم کرد.

قبل از اضافه کردن هر کد اضافی، برنامه را اجرا کنید، روی یک ایمیل ضربه بزنید و نمای ایمیل را باز کنید. آیکون FAB باید بدون هیچ تغییری تغییر کند.

قبل از

d8e3afa0447cfc20.gif

ما در ادامه‌ی کدنویسی با فایل home.dart کار خواهیم کرد، بنابراین نگران اضافه کردن فایل ایمپورت برای پکیج animations نباشید، زیرا قبلاً در مرحله‌ی ۲ این کار را برای home.dart انجام داده‌ایم.

نحوه پیکربندی چند انتقال بعدی بسیار مشابه خواهد بود، زیرا همه آنها از یک کلاس قابل استفاده مجدد، _FadeThroughTransitionSwitcher ، استفاده خواهند کرد.

در home.dart قطعه کد زیر را زیر _ReplyFabState اضافه کنید:

خانه.دارت

// 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 خود پوشش دهیم:

خانه.دارت

// 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 متنی کاملاً متحرک داشته باشید. رفتن به نمای ایمیل باعث می‌شود که آیکون FAB قدیمی محو و کوچک شود در حالی که آیکون جدید محو و بزرگ می‌شود.

بعد از

c55bacd9a144ec69.gif

۱۰. اضافه کردن گذار محوشونده بین عنوان صندوق پستی ناپدیدشونده

در این مرحله، یک گذار محوشونده اضافه خواهیم کرد تا عنوان صندوق پستی بین حالت مرئی و نامرئی در نمای ایمیل محو شود. از آنجایی که نمی‌خواهیم بر یک رابطه مکانی یا سلسله مراتبی تأکید کنیم، از یک محوشونده برای انجام یک "جابجایی" ساده بین ویجت Text که عنوان صندوق پستی را در بر می‌گیرد و یک SizedBox خالی استفاده خواهیم کرد.

قبل از اضافه کردن هر کد اضافی، برنامه را اجرا کنید، روی یک ایمیل ضربه بزنید و نمای ایمیل را باز کنید. عنوان صندوق پستی باید بدون هیچ انتقالی ناپدید شود.

قبل از

59eb57a6c71725c0.gif

بقیه‌ی این مجموعه کد سریع خواهد بود، زیرا ما بیشتر کارها را در _FadeThroughTransitionSwitcher در مرحله‌ی قبل انجام داده‌ایم.

حالا، بیایید به کلاس _AnimatedBottomAppBar در home.dart برویم تا گذار خود را اضافه کنیم. ما از _FadeThroughTransitionSwitcher از مرحله قبل دوباره استفاده خواهیم کرد و شرط onMailView خود را که یا یک SizedBox خالی برمی‌گرداند یا یک عنوان صندوق پستی که همزمان با کشوی پایینی محو می‌شود، در آن قرار می‌دهیم:

خانه.دارت

...
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

۱۱. اضافه کردن گذار محوشونده بین اکشن‌های نوار برنامه پایین

در این مرحله، یک گذار محوشونده اضافه خواهیم کرد تا اکشن‌های نوار برنامه‌ی پایینی بر اساس زمینه‌ی برنامه‌ها محو شوند. از آنجایی که نمی‌خواهیم بر یک رابطه‌ی مکانی یا سلسله مراتبی تأکید کنیم، از یک محوشونده برای انجام یک «جابجایی» ساده بین اکشن‌های نوار برنامه‌ی پایینی، زمانی که برنامه در صفحه اصلی است، زمانی که کشوی پایینی قابل مشاهده است و زمانی که در نمای ایمیل هستیم، استفاده خواهیم کرد.

قبل از اضافه کردن هر کد اضافی، برنامه را اجرا کنید، روی یک ایمیل ضربه بزنید و نمای ایمیل را باز کنید. همچنین می‌توانید روی لوگوی پاسخ ضربه بزنید. اقدامات نوار برنامه پایین باید بدون هیچ گونه انتقالی تغییر کنند.

قبل از

5f662eac19fce3ed.gif

مشابه مرحله قبل، دوباره از _FadeThroughTransitionSwitcher خود استفاده خواهیم کرد. برای دستیابی به گذار مورد نظر، به تعریف کلاس _BottomAppBarActionItems خود بروید و ویجت بازگشتی تابع build() خود را با _FadeThroughTransitionSwitcher پوشش دهید:

خانه.دارت

// 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

۱۲. تبریک می‌گویم!

با استفاده از کمتر از ۱۰۰ خط کد دارت، پکیج انیمیشن‌ها به شما کمک کرده است تا در یک برنامه موجود، انتقال‌های زیبایی ایجاد کنید که با دستورالعمل‌های طراحی متریال مطابقت داشته باشد و همچنین در همه دستگاه‌ها به طور یکسان ظاهر و رفتار کند.

d5637de49eb64d8a.gif

مراحل بعدی

برای اطلاعات بیشتر در مورد سیستم حرکت متریال، حتماً دستورالعمل‌ها و مستندات کامل توسعه‌دهنده را بررسی کنید و سعی کنید چند انتقال متریال به برنامه خود اضافه کنید!

ممنون که حرکت مواد را امتحان کردید. امیدواریم از این آزمایشگاه کدنویسی لذت برده باشید!

من توانستم این آزمایشگاه کد را با مقدار قابل توجهی از زمان و تلاش تکمیل کنم.

کاملاً موافقم موافق خنثی مخالف کاملاً مخالفم

من دوست دارم در آینده به استفاده از سیستم حرکت مواد ادامه دهم.

کاملاً موافقم موافق خنثی مخالف کاملاً مخالفم

برای مشاهده دموهای بیشتر در مورد نحوه استفاده از ویجت‌های ارائه شده توسط کتابخانه Material Flutter و همچنین فریم‌ورک Flutter، حتماً از گالری Flutter دیدن کنید.

46ba920f17198998.png

6ae8ae284bf4f9fa.png