Mem-build Transisi yang Indah dengan Gerakan Material untuk Flutter

1. Pengantar

Desain Material adalah sistem untuk mem-build produk digital yang menarik dan indah. Dengan menyatukan gaya, branding, interaksi, dan gerakan di bawah kumpulan prinsip dan komponen yang konsisten, tim produk dapat mewujudkan potensi desain terbesar mereka.

logo_components_color_2x_web_96dp.png

Komponen Material (MDC) membantu developer menerapkan Desain Material. Dibuat oleh tim engineer dan desainer UX di Google, MDC memiliki banyak komponen UI yang indah dan fungsional serta tersedia untuk Android, iOS, web, dan Flutter.material.io/develop

Apa itu sistem gerakan Material untuk Flutter?

Sistem gerakan Material untuk Flutter adalah pola transisi dalam paket animasi yang dapat membantu pengguna memahami dan membuka aplikasi, sebagaimana dijelaskan dalam pedoman Desain Material.

Keempat pola transisi Material utama adalah sebagai berikut:

  • Transformasi Container: transisi antarelemen UI yang menyertakan container; membuat hubungan yang terlihat antara dua elemen UI yang berbeda dengan mengubah satu elemen menjadi elemen lainnya secara lancar.

11807bdf36c66657.gif

  • Sumbu Merata: transisi antarelemen UI yang memiliki hubungan spasial atau navigasi; menggunakan transformasi yang sama pada sumbu x, y, atau z untuk memperkuat hubungan antarelemen.

71218f390abae07e.gif

  • Memudar: transisi antarelemen UI yang tidak memiliki hubungan kuat satu sama lain; menggunakan transisi memudar (jelas ke buram) dan makin jelas (buram ke jelas) yang berurutan, dengan skala elemen yang masuk.

385ba37b8da68969.gif

  • Memperjelas: digunakan untuk elemen UI yang masuk atau keluar dalam batasan layar.

cfc40fd6e27753b6.gif

Paket animasi menawarkan transisi widget untuk pola ini yang dibuat dari library animasi Flutter (flutter/animation.dart) dan library material Flutter (flutter/material.dart):

Dalam codelab ini, Anda akan menggunakan transisi Material yang dibuat dari framework Flutter dan library Material, yang berarti Anda akan menangani widget. :)

Yang akan Anda buat

Codelab ini akan memandu Anda mem-build beberapa transisi menjadi aplikasi email Flutter contoh yang disebut Reply, menggunakan Dart, untuk mendemonstrasikan cara Anda menggunakan transisi dari paket animasi untuk menyesuaikan tampilan dan nuansa aplikasi Anda.

Kode awal aplikasi Reply akan diberikan, dan Anda akan menyertakan transisi Material berikut ke dalam aplikasi, yang dapat dilihat di GIF codelab yang selesai di bawah:

  • Transisi Transformasi Container dari daftar email menjadi halaman detail email
  • Transisi Transformasi Container dari FAB menjadi halaman tulis email
  • Transisi Sumbu Z Merata dari ikon penelusuran menjadi halaman tampilan penelusuran
  • Transisi Memudar antara halaman kotak surat
  • Transisi Memudar antara FAB tulis dan balas
  • Transisi Memudar antara judul kotak surat yang menghilang
  • Transisi Memudar antartindakan panel aplikasi bawah

b26fe84fed12d17d.gif

Yang Anda butuhkan

  • Pengetahuan dasar tentang pengembangan Flutter dan Dart
  • Editor kode
  • Emulator atau perangkat Android/iOS
  • Kode contoh (lihat langkah berikutnya)

Bagaimana Anda menilai tingkat pengalaman Anda mem-build aplikasi Flutter?

Pemula Menengah Mahir

Apa yang ingin Anda pelajari dari codelab ini?

Saya baru mengenal topik ini, jadi saya ingin mendapatkan ringkasan yang bermanfaat. Saya sedikit paham soal topik ini, tetapi saya perlu mengingat kembali. Saya sedang mencari kode contoh untuk digunakan dalam project saya. Saya sedang mencari penjelasan tentang hal spesifik.

2. Menyiapkan lingkungan pengembangan Flutter Anda

Anda memerlukan dua software untuk menyelesaikan lab ini—Flutter SDK dan editor.

Anda dapat menjalankan codelab menggunakan salah satu perangkat berikut:

  • Perangkat Android atau iOS fisik yang terhubung ke komputer dan disetel ke mode Developer.
  • Simulator iOS (perlu menginstal alat Xcode).
  • Android Emulator (memerlukan penyiapan di Android Studio).
  • Browser (Chrome diperlukan untuk proses debug).
  • Aplikasi desktop Windows, Linux, atau macOS. Anda harus melakukan pengembangan di platform tempat Anda berencana men-deploy aplikasi. Jadi, jika ingin mengembangkan aplikasi desktop Windows, Anda harus mengembangkannya di Windows untuk mengakses rantai build yang sesuai. Ada persyaratan spesifik per sistem operasi yang dibahas secara mendetail di docs.flutter.dev/desktop.

3. Mendownload aplikasi awal codelab

Opsi 1: Meng-clone aplikasi codelab awal dari GitHub

Untuk meng-clone codelab ini dari GitHub, jalankan perintah berikut:

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

Opsi 2: Mendownload file ZIP aplikasi codelab awal

Aplikasi awal terletak di direktori material-components-flutter-motion-codelab-starter.

Memverifikasi dependensi project

Project ini bergantung pada paket animasi. Di pubspec.yaml, perhatikan bahwa bagian dependencies mencakup kode berikut:

animations: ^2.0.0

Membuka project dan menjalankan aplikasi

  1. Buka project di editor pilihan Anda.
  2. Ikuti petunjuk untuk "Run the app" (Jalankan aplikasi) di Get Started: Test drive pada editor pilihan Anda.

Berhasil! Kode awal untuk halaman beranda Reply akan berjalan di perangkat/emulator Anda. Anda akan melihat Kotak masuk yang berisi daftar email.

Halaman beranda Reply

Opsional: Memperlambat animasi perangkat

Karena codelab ini mencakup transisi yang cepat, tetapi belum sempurna, ada baiknya jika Anda memperlambat animasi perangkat untuk mengamati detail transisinya saat menerapkannya. Tindakan ini dapat dilakukan melalui setelan dalam aplikasi, yang dapat diakses dengan mengetuk ikon setelan saat panel samping bawah terbuka. Jangan khawatir, metode memperlambat animasi perangkat ini tidak akan memengaruhi animasi pada perangkat di luar aplikasi Reply.

d23a7bfacffac509.gif

Opsional: Mode Gelap

Jika tema terang Reply membuat mata Anda sakit, mode ini cocok untuk Anda. Terdapat setelan dalam aplikasi yang memungkinkan Anda mengubah tema aplikasi menjadi mode gelap agar tidak sakit di mata. Setelan ini dapat diakses dengan mengetuk ikon setelan saat panel samping bawah terbuka.

87618d8418eee19e.gif

4. Memahami kode aplikasi contoh

Mari kita lihat kodenya. Kita telah menyediakan aplikasi yang menggunakan paket animasi untuk transisi antara berbagai layar di aplikasi.

  • HomePage: menampilkan kotak surat tertentu
  • InboxPage: menampilkan daftar email
  • MailPreviewCard: menampilkan pratinjau email
  • MailViewPage: menampilkan satu email lengkap
  • ComposePage: memungkinkan penulisan email baru
  • SearchPage: menampilkan tampilan penelusuran

router.dart

Pertama, untuk memahami cara penyiapan navigasi root aplikasi, buka router.dart di direktori 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);
 }
}

Ini adalah navigator root kita, dan navigator ini menangani layar aplikasi kita yang menggunakan seluruh kanvas, seperti HomePage dan SearchPage. Navigator memproses status aplikasi kita, memeriksa apakah kita telah menetapkan rute ke ReplySearchPath. Jika kita telah menetapkannya, navigator akan di-build ulang dengan SearchPage di bagian atas stack. Perhatikan bahwa layar kita digabungkan dalam CustomTransitionPage tanpa transisi yang ditentukan. Hal ini menunjukkan satu cara untuk melakukan navigasi antara layar tanpa transisi kustom.

home.dart

Kita menetapkan rute ke ReplySearchPath di status aplikasi kita dengan melakukan hal berikut di dalam _BottomAppBarActionItems di 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();
   },
 ),
);

Di parameter onPressed, kita mengakses RouterProvider kita dan menetapkan routePath-nya ke ReplySearchPath. RouterProvider kita melacak status navigator root kita.

mail_view_router.dart

Sekarang mari kita lihat cara navigasi bagian dalam aplikasi kita disiapkan, buka mail_view_router.dart di direktori lib. Anda akan melihat navigator yang serupa dengan yang di atas:

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

Ini adalah navigator bagian dalam kita. Ini menangani layar bagian dalam aplikasi kita yang hanya memakai isi kanvas, seperti InboxPage. InboxPage menampilkan daftar email bergantung pada kotak surat saat ini di status aplikasi kita. Navigator di-build ulang dengan InboxPage yang benar di atas, kapan pun terdapat perubahan pada properti currentlySelectedInbox dari status aplikasi kita.

home.dart

Kita menetapkan kotak surat saat ini status aplikasi kita dengan melakukan hal berikut dalam _HomePageState di 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(() {});
}

Di fungsi _onDestinationSelected, kita mengakses EmailStore dan menetapkan currentlySelectedInbox-nya ke tujuan yang dipilih. EmailStore kita melacak status navigator bagian dalam.

home.dart

Terakhir, untuk melihat contoh perutean navigasi yang sedang digunakan, buka home.dart di direktori lib. Temukan class _ReplyFabState, di dalam properti onTap widget InkWell, yang akan terlihat seperti ini:

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

Ini menunjukkan cara Anda menuju halaman tulis email, tanpa transisi kustom apa pun. Selama codelab ini, Anda akan mempelajari kode Reply untuk menyiapkan transisi Material yang berfungsi bersama-sama dengan berbagai tindakan navigasi di seluruh aplikasi.

Setelah Anda memahami kode awal, sekarang mari kita implementasikan transisi pertama kita.

5. Menambahkan transisi Transformasi Container dari daftar email ke halaman detail email

Untuk memulai, Anda akan menambahkan transisi dengan mengklik email. Pola transformasi container sangatlah cocok untuk perubahan navigasi ini, karena pola ini didesain untuk transisi antara elemen UI yang menyertakan container. Pola ini menghasilkan hubungan yang terlihat di antara dua elemen UI.

Sebelum menambahkan kode, coba jalankan aplikasi Reply dan klik email. Aplikasi akan menampilkan animasi melompat-memotong sederhana, yang berarti layar diganti tanpa transisi:

Sebelum

48b00600f73c7778.gif

Mulai dengan menambahkan impor untuk paket animasi di atas mail_card_preview.dart seperti yang ditampilkan di cuplikan berikut:

mail_card_preview.dart

import 'package:animations/animations.dart';

Setelah Anda memiliki impor untuk paket animasi, kita dapat mulai menambahkan transisi yang indah ke aplikasi. Mari kita mulai dengan membuat class StatelessWidget yang akan menjadi rumah untuk widget OpenContainer.

Di mail_card_preview.dart, tambahkan cuplikan kode berikut setelah definisi class 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,
       );
     },
   );
 }
}

Sekarang mari kita gunakan wrapper baru kita. Di dalam definisi class MailPreviewCard, kita akan menggabungkan widget Material dari fungsi build() kita dengan _OpenContainerWrapper baru:

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 kita memiliki widget InkWell dan properti warna OpenContainer menentukan warna container yang ditutupnya. Oleh karena itu, kita dapat menghapus widget Material dan Inkwell. Kode yang dihasilkan akan terlihat seperti berikut:

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

Pada tahap ini, Anda akan memiliki transformasi container yang berfungsi sepenuhnya. Mengklik email akan meluaskan item daftar menjadi layar detail sembari menyurutkan daftar email. Menekan tombol kembali akan menutup layar detail email kembali menjadi daftar item sembari meningkatkan skala di daftar email.

Setelah

663e8594319bdee3.gif

6. Menambahkan transisi Transformasi Container dari FAB ke halaman tulis email

Mari kita lanjutkan dengan transformasi container dan kita tambahkan transisi dari Tombol Tindakan Mengambang ke ComposePage yang meluaskan FAB ke email baru yang akan ditulis oleh pengguna. Pertama-tama, jalankan ulang aplikasi klik FAB untuk melihat bahwa tidak ada transisi ketika meluncurkan layar tulis email.

Sebelum

4aa2befdc5170c60.gif

Cara mengonfigurasi transisi ini akan sangat mirip dengan cara kita melakukannya di langkah sebelumnya, karena kita menggunakan class widget yang sama, OpenContainer.

Di home.dart, mari kita impor package:animations/animations.dart di bagian paling atas file, dan kita ubah metode _ReplyFabState build(). Selanjutnya, kita gabungkan widget Material yang ditampilkan dengan widget 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,
     ...

Selain parameter yang digunakan untuk mengonfigurasi widget OpenContainer kita sebelumnya, onClosed kini juga ditetapkan. onClosed adalah ClosedCallback yang dipanggil ketika rute OpenContainer telah muncul atau telah ditampilkan ke status tertutup. Nilai pengembalian transaksi tersebut diteruskan ke fungsi ini sebagai argumen. Kita menggunakan Callback ini untuk memberi tahu penyedia aplikasi Anda bahwa kita telah meninggalkan rute ComposePage, sehingga penyedia aplikasi dapat memberi tahu semua pemroses.

Sama halnya dengan yang kita lakukan di langkah sebelumnya, kita akan menghapus widget Material dari widget kita karena widget OpenContainer menangani warna widget yang ditampilkan oleh closedBuilder dengan closedColor. Kita juga akan menghapus panggilan Navigator.push() di dalam onTap widget InkWell, dan menggantinya dengan openContainer() Callback yang diberikan oleh closedBuilder widget OpenContainer, karena kini widget OpenContainer menangani peruteannya sendiri.

Kode yang dihasilkan adalah sebagai berikut:

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

Sekarang untuk membersihkan beberapa kode lama. Karena widget OpenContainer kini menangangi pemberitahuan kepada penyedia aplikasi kita bahwa kita tidak lagi di ComposePage melalui onClosed ClosedCallback, kita dapat menghapus implementasi kita sebelumnya di 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);

Itu saja untuk langkah ini! Anda harus memiliki transisi dari FAB ke layar tulis yang terlihat seperti berikut:

Setelah

5c7ad1b4b40f9f0c.gif

7. Menambahkan transisi Sumbu Z Merata dari ikon penelusuran ke halaman tampilan penelusuran

Di langkah ini, kita akan menambahkan transisi dari ikon penelusuran ke tampilan penelusuran layar penuh. Karena tidak ada container yang dilibatkan dalam perubahan navigasi ini, kita dapat menggunakan transisi Sumbu Z Merata untuk memperkuat hubungan spasial antara dua layar dan menunjukkan pemindahan satu tingkat ke atas di hierarki aplikasi.

Sebelum menambahkan kode tambahan apa pun, coba jalankan aplikasi dan ketuk ikon penelusuran di pojok kanan bawah layar. Tindakan ini akan memunculkan layar tampilan penelusuran tanpa transisi.

Sebelum

df7683a8ad7b920e.gif

Untuk memulai, mari kita buka file router.dart kita. Setelah definisi class ReplySearchPath, tambahkan cuplikan berikut:

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

Sekarang, mari kita gunakan SharedAxisTransitionPageWrapper baru kita untuk mencapai transisi yang diinginkan. Di dalam definisi class ReplyRouterDelegate, di bagian properti pages, mari kita gabungkan layar penelusuran kita dengan SharedAxisTransitionPageWrapper, bukan 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(),
     ),
 ],
);

Sekarang, coba jalankan ulang aplikasinya.

81b3ea098926931.gif

Transisinya mulai terlihat baik. Saat Anda mengklik ikon penelusuran di panel aplikasi bawah, transisi sumbu merata menskalakan halaman penelusuran menjadi terlihat. Namun, perhatikan bagaimana halaman utama tidak diskalakan dan tetap statis saat halaman penelusuran diskalakan di atasnya. Selain itu, saat menekan tombol kembali, halaman utama tidak diskalakan menjadi terlihat, melainkan tetap statis saat halaman penelusuran diskalakan ke luar tampilan. Kita belum selesai.

Mari kita perbaiki kedua masalah itu dengan menggabungkan juga HomePage dengan SharedAxisTransitionWrapper kita, bukan dengan 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(),
     ),
 ],
);

Selesai. Sekarang coba jalankan ulang aplikasi dan ketuk ikon penelusuran. Layar utama dan tampilan penelusuran akan secara bersamaan memudar dan diskalakan searah sumbu Z ke bagian dalam, membuat efek yang mulus antara dua layar.

Setelah

462d890086a3d18a.gif

8. Menambahkan transisi Memudar antarhalaman kotak surat

Di langkah ini, kita akan menambahkan transisi di antara kotak surat yang berbeda. Karena kita tidak ingin menekankan hubungan spasial atau hierarki, kita akan menggunakan transisi memudar untuk melakukan "pertukaran" sederhana antara daftar email.

Sebelum menambahkan kode tambahan, coba jalankan aplikasi, ketuk logo Reply di Panel Aplikasi Bawah, dan beralihlah antara kotak surat. Daftar email akan berubah tanpa transisi.

Sebelum

89033988ce26b92e.gif

Untuk memulai, mari kita buka file mail_view_router.dart kita. Setelah definisi class MailViewRouterDelegate kita, tambahkan cuplikan berikut:

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

Sama seperti langkah sebelumnya, mari kita gunakan FadeThroughTransitionPageWrapper baru kita untuk mencapai transisi yang diinginkan. Di dalam definisi class MailViewRouterDelegate, di bagian properti pages, alih-alih menggabungkan layar kotak surat kita dengan CustomTransitionPage, gunakan 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),
   ),
 ],
);

Jalankan ulang aplikasi. Saat Anda membuka panel navigasi bawah dan mengubah kotak surat, daftar email yang ada saat ini akan diskalakan dan memudar, sementara daftar baru akan diskalakan dan makin jelas. Bagus!

Setelah

8186940082b630d.gif

9. Menambahkan transisi Memudar antara FAB tulis dan balas

Di langkah ini, kita akan menambahkan transisi di antara ikon FAB yang berbeda. Karena kita tidak ingin menekankan hubungan spasial atau hierarki, kita akan menggunakan transisi memudar untuk melakukan "swap" sederhana antara ikon di FAB.

Sebelum menambahkan kode tambahan apa pun, cobalah menjalankan aplikasi, mengetuk email, dan membuka tampilan email. Ikon FAB akan berubah tanpa transisi.

Sebelum

d8e3afa0447cfc20.gif

Kita akan bekerja dalam home.dart di sisa codelab ini, jadi Anda tidak perlu khawatir mengenai penambahan impor untuk paket animasi, karena kita sudah melakukannya untuk home.dart di langkah 2.

Cara kita mengonfigurasi beberapa transisi berikutnya akan sangat mirip, karena semua transisi tersebut akan memanfaatkan class yang dapat digunakan kembali, _FadeThroughTransitionSwitcher.

Di home.dart, mari kita tambahkan cuplikan berikut di bawah _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,
   );
 }
}

Sekarang, di _ReplyFabState kita, cari widget fabSwitcher. fabSwitcher menampilkan ikon yang berbeda berdasarkan apakah itu tampilan email atau bukan. Mari kita gabungkan dengan _FadeThroughTransitionSwitcher kita:

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

Kita memberikan fillColor transparan ke _FadeThroughTransitionSwitcher, sehingga tidak ada latar belakang antarelemen saat bertransisi. Kita juga membuat UniqueKey dan menetapkannya ke salah satu ikon.

Sekarang, di langkah ini, Anda akan memiliki FAB kontekstual yang memiliki animasi penuh. Membuka tampilan email menyebabkan ikon FAB yang lama diskalakan dan memudar, sementara yang baru diskalan dan makin jelas.

Setelah

c55bacd9a144ec69.gif

10. Menambahkan transisi Memudar antara judul kotak surat yang menghilang

Di langkah ini, kita akan menambahkan transisi memudar, untuk memudarkan judul kotak surat antara status terlihat dan tidak terlihat saat berada di tampilan email. Karena kita tidak ingin menekankan hubungan spasial atau hierarki, kita akan menggunakan memudar untuk melakukan "pertukaran" sederhana antara widget Text yang mencakup judul kotak surat, dan SizedBox kosong.

Sebelum menambahkan kode tambahan apa pun, cobalah menjalankan aplikasi, mengetuk email, dan membuka tampilan email. Judul kotak surat akan menghilang tanpa transisi.

Sebelum

59eb57a6c71725c0.gif

Bagian yang tersisa di codelab ini tidak akan memerlukan banyak waktu karena kita sudah melakukan sebagian besar pekerjaan di _FadeThroughTransitionSwitcher pada langkah terakhir.

Sekarang, mari kita buka class _AnimatedBottomAppBar di home.dart untuk menambahkan transisi. Kita akan menggunakan kembali _FadeThroughTransitionSwitcher dari langkah sebelumnya, dan menggabungkan onMailView bersyarat, yang menampilkan SizedBox kosong, atau judul kotak surat yang memudar bersamaan dengan panel samping bawah:

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
                   .bodyText1!
                   .copyWith(
                     color: ReplyColors.white50,
                   ),
             );
           },
         ),
       ),
),

Selesai, itu saja yang harus dilakukan di langkah ini.

Jalankan ulang aplikasi. Ketika Anda membuka email dan diarahkan ke tampilan email, judul kotak surat di panel aplikasi bawah akan diskalakan dan memudar. Keren!

Setelah

3f1a3db01a481124.gif

11. Menambahkan transisi Memudar antartindakan panel aplikasi bawah

Di langkah ini, kita akan menambahkan transisi memudar, untuk memudarkan tindakan panel aplikasi bawah berdasarkan konteks aplikasi. Karena kita tidak ingin menekankan hubungan spasial atau hierarki, kita akan menggunakan memudar untuk melakukan "pertukaran" sederhana antara tindakan panel aplikasi bawah di HomePage, ketika panel samping bawah terlihat, dan ketika sedang berada di tampilan email.

Sebelum menambahkan kode tambahan apa pun, cobalah menjalankan aplikasi, mengetuk email, dan membuka tampilan email. Anda juga dapat mencoba mengetuk logo Reply. Tindakan panel aplikasi bawah akan berubah tanpa transisi.

Sebelum

5f662eac19fce3ed.gif

Mirip dengan langkah sebelumnya, kita akan memanfaatkan _FadeThroughTransitionSwitcher kita lagi. Untuk mencapai transisi yang diinginkan, buka definisi class _BottomAppBarActionItems dan gabungkan widget kembali dari fungsi build() kita dengan _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
...

Sekarang, mari kita coba. Saat Anda membuka email dan diarahkan ke tampilan email, panel aplikasi bawah yang lama akan diskalakan dan memudar sementara tindakan baru akan diskalakan dan makin jelas. Bagus!

Setelah

cff0fa2afa1c5a7f.gif

12. Selamat!

Dengan menggunakan kurang dari 100 baris kode Dart, paket animasi telah membantu Anda membuat transisi yang indah di aplikasi yang sudah ada yang mematuhi pedoman Desain Material, dan terlihat serta berfungsi secara konsisten di seluruh perangkat.

d5637de49eb64d8a.gif

Langkah berikutnya

Untuk informasi selengkapnya mengenai sistem gerakan Material, pastikan untuk memeriksa spek dan dokumentasi developer lengkap, dan coba tambahkan beberapa transisi Material ke aplikasi Anda.

Terima kasih telah mencoba gerakan Material. Kami harap Anda menikmati codelab ini.

Saya dapat menyelesaikan codelab ini dengan upaya dan dalam durasi waktu yang wajar

Sangat setuju Setuju Netral Tidak setuju Sangat tidak setuju

Saya ingin terus menggunakan sistem gerakan Material pada masa mendatang

Sangat setuju Setuju Netral Tidak setuju Sangat tidak setuju

Untuk demo lainnya terkait cara menggunakan widget yang disediakan oleh library Flutter Material serta framework Flutter, pastikan untuk mengunjungi Flutter Gallery.

46ba920f17198998.png

6ae8ae284bf4f9fa.png