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

1. مقدمه

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

logo_components_color_2x_web_96dp.png

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

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

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

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

  • Container Transform: انتقال بین عناصر UI که شامل یک ظرف است. با تبدیل یکپارچه یک عنصر به عنصر دیگر، یک ارتباط قابل مشاهده بین دو عنصر UI مجزا ایجاد می کند.

11807bdf36c66657.gif

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

71218f390abae07e.gif

  • Fade Through: انتقال بین عناصر UI که رابطه قوی با یکدیگر ندارند. از محو شدن و محو شدن متوالی با مقیاس عنصر ورودی استفاده می کند.

385ba37b8da68969.gif

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

cfc40fd6e27753b6.gif

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

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

چیزی که خواهی ساخت

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

کد شروع برای برنامه Reply ارائه می‌شود، و شما جابجایی‌های Material زیر را در برنامه قرار می‌دهید، که می‌توانید در GIF کد لبه تکمیل‌شده زیر مشاهده کنید:

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

b26fe84fed12d17d.gif

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

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

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

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

دوست دارید از این کد لبه چه چیزی یاد بگیرید؟

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

2. محیط توسعه Flutter خود را تنظیم کنید

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

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

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

3. برنامه استارتر Codelab را دانلود کنید

گزینه 1: برنامه codelab starter را از GitHub کلون کنید

برای شبیه سازی این کد لبه از GitHub، دستورات زیر را اجرا کنید:

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

گزینه 2: فایل فشرده برنامه codelab starter را دانلود کنید

برنامه شروع در دایرکتوری material-components-flutter-motion-codelab-starter قرار دارد.

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

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

animations: ^2.0.0

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

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

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

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

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

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

d23a7bfacffac509.gif

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

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

87618d8418eee19e.gif

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

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

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

روتر.دارت

ابتدا، برای درک نحوه تنظیم مسیریابی ریشه برنامه، 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 لیستی از ایمیل ها را بسته به اینکه صندوق پست فعلی در وضعیت برنامه ما قرار دارد، نمایش می دهد. هر زمان که تغییری در ویژگی currentlySelectedInbox وضعیت برنامه ما ایجاد شود، ناوبری با InboxPage صحیح در بالای پشته بازسازی می‌شود.

خانه.دارت

با انجام کارهای زیر در داخل _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 SelectedInbox آن را روی مقصد انتخابی تنظیم می کنیم. 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 را تنظیم کنید که همزمان با اقدامات پیمایش مختلف در سراسر برنامه کار کنند.

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

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

برای شروع، هنگام کلیک کردن بر روی ایمیل، یک انتقال اضافه خواهید کرد. برای این تغییر مسیریابی، الگوی تبدیل کانتینر به خوبی مناسب است، زیرا برای انتقال بین عناصر UI که شامل یک ظرف است، طراحی شده است. این الگو یک ارتباط قابل مشاهده بین دو عنصر UI ایجاد می کند.

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

قبل از

48b00600f73c7778.gif

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

mail_card_preview.dart

import 'package:animations/animations.dart';

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

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

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
class _OpenContainerWrapper extends StatelessWidget {
 const _OpenContainerWrapper({
   required this.id,
   required this.email,
   required this.closedChild,
 });

 final int id;
 final Email email;
 final Widget closedChild;

 @override
 Widget build(BuildContext context) {
   final theme = Theme.of(context);
   return OpenContainer(
     openBuilder: (context, closedContainer) {
       return MailViewPage(id: id, email: email);
     },
     openColor: theme.cardColor,
     closedShape: const RoundedRectangleBorder(
       borderRadius: BorderRadius.all(Radius.circular(0)),
     ),
     closedElevation: 0,
     closedColor: theme.cardColor,
     closedBuilder: (context, openContainer) {
       return InkWell(
         onTap: () {
           Provider.of<EmailStore>(
             context,
             listen: false,
           ).currentlySelectedEmailId = id;
           openContainer();
         },
         child: closedChild,
       );
     },
   );
 }
}

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

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
 id: id,
 email: email,
 closedChild: Material(
...

_OpenContainerWrapper ما دارای یک ویجت InkWell است و ویژگی های رنگ OpenContainer رنگ ظرفی را که محصور می کند مشخص می کند. بنابراین، می‌توانیم ویجت‌های Material و Inkwell را حذف کنیم. کد حاصل به صورت زیر است:

mail_card_preview.dart

// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
 id: id,
 email: email,
 closedChild: Dismissible(
   key: ObjectKey(email),
   dismissThresholds: const {
     DismissDirection.startToEnd: 0.8,
     DismissDirection.endToStart: 0.4,
   },
   onDismissed: (direction) {
     switch (direction) {
       case DismissDirection.endToStart:
         if (onStarredInbox) {
           onStar();
         }
         break;
       case DismissDirection.startToEnd:
         onDelete();
         break;
       default:
     }
   },
   background: _DismissibleContainer(
     icon: 'twotone_delete',
     backgroundColor: colorScheme.primary,
     iconColor: ReplyColors.blue50,
     alignment: Alignment.centerLeft,
     padding: const EdgeInsetsDirectional.only(start: 20),
   ),
   confirmDismiss: (direction) async {
     if (direction == DismissDirection.endToStart) {
       if (onStarredInbox) {
         return true;
       }
       onStar();
       return false;
     } else {
       return true;
     }
   },
   secondaryBackground: _DismissibleContainer(
     icon: 'twotone_star',
     backgroundColor: currentEmailStarred
         ? colorScheme.secondary
         : theme.scaffoldBackgroundColor,
     iconColor: currentEmailStarred
         ? colorScheme.onSecondary
         : colorScheme.onBackground,
     alignment: Alignment.centerRight,
     padding: const EdgeInsetsDirectional.only(end: 20),
   ),
   child: mailPreview,
 ),
);

در این مرحله، شما باید یک تبدیل ظرف کاملاً در حال کار داشته باشید. با کلیک بر روی یک ایمیل، آیتم لیست را در صفحه جزئیات گسترش می دهد و در عین حال لیست ایمیل ها را کاهش می دهد. با فشار دادن به عقب، صفحه جزئیات ایمیل در حالی که در لیست ایمیل‌ها افزایش می‌یابد، به یک آیتم فهرست باز می‌گردد.

بعد از

663e8594319bdee3.gif

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

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

قبل از

4aa2befdc5170c60.gif

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

در home.dart ، بیایید package:animations/animations.dart در بالای فایل وارد کنیم و متد build() _ReplyFabState را تغییر دهیم. بیایید ویجت 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 که توسط ویجت OpenContainer closedBuilder شده است جایگزین می کنیم، زیرا اکنون ویجت 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

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

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

8. انتقال Fade Through بین صفحات صندوق پست را اضافه کنید

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

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

قبل از

89033988ce26b92e.gif

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

mail_view_router.dart

// TODO: Add Fade through transition between mailbox pages (Motion)
class FadeThroughTransitionPageWrapper extends Page {
 const FadeThroughTransitionPageWrapper({
   required this.mailbox,
   required this.transitionKey,
 })  : super(key: transitionKey);

 final Widget mailbox;
 final ValueKey transitionKey;

 @override
 Route createRoute(BuildContext context) {
   return PageRouteBuilder(
       settings: this,
       transitionsBuilder: (context, animation, secondaryAnimation, child) {
         return FadeThroughTransition(
           fillColor: Theme.of(context).scaffoldBackgroundColor,
           animation: animation,
           secondaryAnimation: secondaryAnimation,
           child: child,
         );
       },
       pageBuilder: (context, animation, secondaryAnimation) {
         return mailbox;
       });
 }
}

مانند آخرین مرحله ما، بیایید از FadeThroughTransitionPageWrapper جدید خود برای رسیدن به انتقال مورد نظر خود استفاده کنیم. در داخل تعریف کلاس MailViewRouterDelegate ، در زیر ویژگی pages ، به جای اینکه صفحه صندوق پستی خود را با CustomTransitionPage بپیچید، به جای آن از FadeThroughTransitionPageWrapper استفاده کنید:

mail_view_router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Fade through transition between mailbox pages (Motion)
   FadeThroughTransitionPageWrapper(
     mailbox: InboxPage(destination: currentlySelectedInbox),
     transitionKey: ValueKey(currentlySelectedInbox),
   ),
 ],
);

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

بعد از

8186940082b630d.gif

9. انتقال Fade Through بین نوشتن و پاسخ FAB را اضافه کنید

در این مرحله، یک انتقال بین آیکون های مختلف FAB اضافه می کنیم. از آنجایی که نمی‌خواهیم بر یک رابطه فضایی یا سلسله مراتبی تأکید کنیم، از fade through برای انجام یک «تبادل» ساده بین نمادها در FAB استفاده می‌کنیم.

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

قبل از

d8e3afa0447cfc20.gif

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

روشی که ما دو ترانزیشن بعدی را پیکربندی می کنیم بسیار شبیه خواهد بود، زیرا همه آنها از یک کلاس قابل استفاده مجدد، _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

10. انتقال Fade Through بین عنوان صندوق پستی ناپدید شده را اضافه کنید

در این مرحله، یک Fade through Transition اضافه می‌کنیم، تا از طریق عنوان صندوق پستی بین حالت قابل مشاهده و نامرئی هنگام نمایش ایمیل محو شود. از آنجایی که نمی‌خواهیم بر یک رابطه فضایی یا سلسله مراتبی تأکید کنیم، از یک fade through برای انجام یک "تبادل" ساده بین ویجت 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

11. انتقال Fade Through را بین اقدامات نوار برنامه پایین اضافه کنید

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

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

قبل از

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

12. تبریک می گویم!

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

d5637de49eb64d8a.gif

مراحل بعدی

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

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

من توانستم با صرف زمان و تلاش معقول این کد لبه را تکمیل کنم

کاملا موافقم موافقم خنثی مخالفت کنید به شدت مخالفم

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

کاملا موافقم موافقم خنثی مخالفت کنید به شدت مخالفم

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

46ba920f17198998.png

6ae8ae284bf4f9fa.png