Mem-build Transisi yang Indah dengan Gerakan Material untuk Flutter

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, MMD 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 antara elemen UI yang menyertakan container; membuat hubungan yang terlihat antara dua elemen UI yang berbeda dengan mengubah satu elemen menjadi elemen lainnya.

b9fd67c205755d55.gif

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

76622de33a19179.gif

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

18a525c038443492.gif

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

cd10a0580a159644.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 antara tindakan panel aplikasi bawah

5f7b8860db2c70e2.gif

Yang Anda butuhkan

  • Pengetahuan dasar tentang pengembangan Flutter dan Dart
  • Android Studio (download di sini jika Anda belum memilikinya)
  • Emulator atau perangkat Android (tersedia melalui Android Studio)
  • 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 melihat ringkasan yang bagus. 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 sesuatu yang spesifik.

Sebelum memulai

Untuk memulai mengembangkan aplikasi seluler dengan Flutter, Anda harus:

  1. Mendownload dan menginstal Flutter SDK.
  2. Mengupdate PATH Anda dengan Flutter SDK.
  3. Menginstal Android Studio dengan plugin Flutter dan Dart, atau editor favorit Anda.
  4. Menginstal emulator Android, simulator iOS (perlu Mac dengan Xcode), atau menggunakan perangkat fisik.

Untuk informasi selengkapnya terkait penginstalan Flutter, lihat Get Started: Install. Untuk menyiapkan editor, lihat Get Started: Set up an editor. Saat menginstal emulator Android, Anda dapat menggunakan opsi default seperti ponsel Pixel 3 dengan Image System terbaru. Tindakan ini disarankan, tetapi tidak diperlukan untuk mengaktifkan akselerasi VM. Setelah menyelesaikan 4 langkah di atas, Anda dapat kembali ke codelab. Untuk menyelesaikan codelab ini, cukup instal Flutter untuk satu platform (Android atau iOS).

Pastikan Flutter SDK Anda berada dalam status yang tepat

Sebelum melanjutkan codelab ini, pastikan SDK Anda berada dalam status yang tepat. Jika Flutter SDK telah diinstal sebelumnya, gunakan flutter upgrade untuk memastikan bahwa SDK berada dalam status terbaru.

 flutter upgrade

Menjalankan flutter upgrade akan otomatis menjalankan flutter doctor.. Jika penginstalan Flutter ini baru dan tidak memerlukan upgrade, jalankan flutter doctor secara manual. Anda akan mendapat laporan jika ada dependensi yang harus diinstal untuk menyelesaikan penyiapan. Anda dapat mengabaikan tanda centang yang tidak relevan bagi Anda (misalnya, Xcode jika Anda tidak bermaksud untuk mengembangkan untuk iOS).

 flutter doctor

Pertanyaan Umum (FAQ)

Memulai Android Studio

Saat Anda membuka Android Studio, jendela yang berjudul "Welcome to Android Studio" akan ditampilkan. Namun, jika ini adalah pertama kalinya Anda meluncurkan Android Studio, selesaikan langkah-langkah Wizard Penyiapan Android Studio dengan nilai default. Langkah ini dapat memerlukan waktu beberapa menit untuk mendownload dan menginstal file yang diperlukan, jadi jangan ragu untuk membiarkan proses ini berjalan di latar belakang sembari Anda melakukan bagian berikutnya.

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

Download aplikasi awal

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

Memuat kode awal di Android Studio

  1. Setelah wizard penyiapan selesai dan jendela Welcome to Android Studio ditampilkan, klik Open an existing Android Studio project.

e3f200327a67a53.png

  1. Buka direktori tempat Anda menginstal kode contoh dan pilih direktori contoh untuk membuka project.
  2. Tunggu Android Studio mem-build dan menyinkronkan project, seperti yang ditunjukkan oleh indikator aktivitas di bagian bawah jendela Android Studio.
  3. Pada tahap ini, Android Studio dapat memunculkan beberapa error build karena alat build atau Android SDK tidak ada, seperti yang ditampilkan di bawah. Ikuti petunjuk di Android Studio untuk menginstal/mengupdate versi ini dan menyinkronkan project Anda. Jika masih mengalami masalah, ikuti pedoman terkait mengupdate alat Anda dengan SDK Manager.

6e026ae171f5b1eb.png

  1. Jika diminta:
  • Instal update platform dan plugin atau FlutterRunConfigurationType.
  • Jika Dart atau Flutter SDK tidak dikonfigurasi, tetapkan jalur Flutter SDK untuk plugin Flutter.
  • Konfigurasikan framework Android.
  • Klik "Get dependencies" atau "Run ‘flutter packages get'".

Lalu mulai ulang Android Studio.

53b7195f1c1deedb.png

be5ce477ba09225e.png 24810642cf859588.png

Memverifikasi dependensi project

Project memerlukan dependensi pada paket animasi. Kode contoh yang Anda download seharusnya sudah mencantumkan dependensi ini, tetapi mari kita lihat konfigurasinya untuk memastikannya.

Buka file pubspec.yaml modul app dan pastikan bahwa bagian dependencies menyertakan dependensi pada paket animasi:

animations: ^1.1.2

Menjalankan aplikasi awal

  1. Pastikan bahwa konfigurasi build di sebelah kiri pilihan perangkat adalah app.
  2. Tekan tombol Run/Play berwarna hijau untuk mem-build dan menjalankan aplikasi.

a34cba7fab0a2af9.png

  1. Di menu dropdown Flutter Device Selection di bagian atas layar editor, jika device sudah tercantum di perangkat Anda yang tersedia, lewati ke Langkah 8. Jika tidak tercantum, klik Create New Virtual Device.
  2. Di layar Select Hardware, pilih perangkat ponsel, misalnya Pixel 3, lalu klik Next.
  3. Di layar System Image, pilih recent Android version, terutama API level yang paling tinggi. Jika tidak terinstal, klik link Download yang ditampilkan dan selesaikan proses download-nya.
  4. Klik Next.
  5. Di layar Android Virtual Device (AVD), biarkan setelan lalu klik Finish.
  6. Pilih device (misalnya, iPhone SE atau Android SDK untuk <version>) dari menu dropdown Flutter Device Selection.
  7. Tekan ikon Play ( b8c998094aa23ac2.png).
  8. Android Studio mem-build, men-deploy, dan otomatis membuka aplikasi di perangkat target.

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

Android

iOS

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

Android

iOS

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.

Android

iOS

Mari kita lihat tata 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})
     : assert(replyState != null),
       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) {
   assert(configuration != null);
   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 = 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({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.

Sekarang Anda telah memahami kode awal, mari kita implementasikan transisi pertama kita.

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

Android

iOS

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,
 })  : assert(id != null),
       assert(email != null),
       assert(closedChild != null);

 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 return 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(
     color: theme.cardColor,
     child: InkWell(
       onTap: () {
         Provider.of<EmailStore>(
           context,
           listen: false,
         ).currentlySelectedEmailId = id;

         mobileMailNavKey.currentState.push(
           PageRouteBuilder(
             pageBuilder: (BuildContext context, Animation<double> animation,
                 Animation<double> secondaryAnimation) {
               return MailViewPage(id: id, email: email);
             },
           ),
         );
       },
       child: 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.of(context).scaffoldBackgroundColor,
           iconColor: currentEmailStarred
               ? colorScheme.onSecondary
               : colorScheme.onBackground,
           alignment: Alignment.centerRight,
           padding: const EdgeInsetsDirectional.only(end: 20),
         ),
         child: mailPreview,
       ),
     ),
   ),
 );
}

Jangan lupa menghapus InkWell dari widget, karena sekarang logikanya berada di dalam class _OpenContainerWrapper kita. Kita juga dapat menghapus widget Material, karena properti warna OpenContainer menentukan warna container yang disertakan olehnya:

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

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

Android

iOS

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

Sebelum

Android

iOS

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, tambahkan cuplikan berikut ke definisi class _ReplyFabState. Proses ini memastikan package:animations/animations.dart diimpor di bagian atas file. Di sini kita menggabungkan widget tampilkan dari fungsi build() definisi class _ReplyFabState 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,
     shape: circleFabBorder,
     child: Tooltip(
       message: tooltip,
       child: InkWell(
         customBorder: circleFabBorder,
         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();
               },
             ),
           );
         },
         child: SizedBox(
           height: _mobileFabDimension,
           width: _mobileFabDimension,
           child: Center(
             child: fabSwitcher,
           ),
         ),
       ),
     ),
   );
 },
);

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 menghapus panggilan Navigator.push() kita di dalam onTap widget InkWell kita, dan menggantinya dengan openContainer() Callback yang diberikan oleh closedBuilder widget OpenContainer, karena kini widget OpenContainer menangani peruteannya sendiri.

Di home.dart dalam definisi class _ReplyFabState kita:

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>(false);

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

Setelah

Android

iOS

Di langkah ini, kita akan menambahkan transisi dari ikon penelusuran menjadi 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

Android

iOS

Untuk memulai, mari kita buka file router.dart. 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})
      : assert(screen != null),
        assert(transitionKey != null),
        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 kita inginkan. Kita akan menggabungkan layar widget dengan wrapper, sehingga akan menampilkan halaman, yang dirutekan kembali ke navigator kita, dengan transisi yang kita inginkan. Di dalam definisi class ReplyRouterDelegate kita, di bawah properti pages, daripada menggabungkan layar penelusuran dengan CustomTransitionPage, gunakan wrapper baru kita :

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(
     pageBuilder: (context, animation, secondaryAnimation) {
       return const HomePage();
     },
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('search'),
       screen: const SearchPage(),
     ),
 ],
);

Sekarang, coba jalankan ulang aplikasi.

Android

iOS

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.

Untuk memperbaiki transisi halaman utama, cukup gabungkan HomePage dengan SharedAxisTransitionWrapper kita di router.dart:

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: const HomePage(),
   ),
   if (routePath is ReplySearchPath)
     const SharedAxisTransitionPageWrapper(
       transitionKey: ValueKey('search'),
       screen: const 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

Android

iOS

Di langkah ini, kita akan menambahkan transisi 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

Android

iOS

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 {
  FadeThroughTransitionPageWrapper({
    @required this.mailbox,
    @required this.transitionKey,
  })  : assert(mailbox != null),
        assert(transitionKey != null),
        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 kita inginkan. Kita akan menggabungkan layar kotak surat dengan wrapper, sehingga akan menampilkan halaman, yang dirutekan kembali ke navigator kita, dengan transisi memudar. Di dalam definisi class MailViewRouterDelegate kita, di bawah properti pages, daripada menggabungkan layar kotak surat kita dengan CustomTransitionPage, gunakan wrapper baru kita:

mail_view_router.dart

return Navigator(
 key: navigatorKey,
 onPopPage: _handlePopPage,
 pages: [
   // TODO: Add Fade through transition between different 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

Android

iOS

Di langkah ini, kita akan menambahkan transisi 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

Android

iOS

Kita akan bekerja dalam home.dart di sisa codelab ini, jadi Anda tidak perlu khawatir tentang menambahkan 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,
 })  : assert(fillColor != null),
       assert(child != null);

 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. Widget fabSwitcher adalah yang memungkinkan FAB beralih berdasarkan konteks. fabSwitcher akan memeriksa apakah kita sedang berada di sebuah tampilan email dan akan memberikan ikon yang berbeda untuk FAB tersebut jika kita sedang berada di sebuah tampilan email.

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 antara elemen saat melakukan transisi.

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

Android

iOS

Di langkah ini, kita akan menambahkan transisi memudar, untuk memudarkan judul kotak surat antara status ketika ada 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

Android

iOS

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

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 SizedBox(width: 8),
const _ReplyLogo(),
const SizedBox(width: 10),
// TODO: Add Fade through transition between disappearing mailbox title (Motion)
_FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: onMailView
     ? const SizedBox(height: 0, 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

Android

iOS

Di langkah ini, kita akan menambahkan transisi memudar, untuk memudarkan 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

Android

iOS

Serupa 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
         ? Row(
             mainAxisSize: MainAxisSize.max,
             mainAxisAlignment: MainAxisAlignment.end,
             children: [
               IconButton(
                 icon: ImageIcon(
                   const AssetImage(
                     '$_iconAssetLocation/twotone_star.png',
                     package: _assetsPackage,
                   ),
                   color: starIconColor,
                 ),
                 onPressed: () {
                   model.starEmail(
                     model.currentlySelectedInbox,
                     model.currentlySelectedEmailId,
                   );
                   if (model.currentlySelectedInbox == 'Starred') {
                     mobileMailNavKey.currentState.pop();
                     model.currentlySelectedEmailId = -1;
                   }
                 },
                 color: ReplyColors.white50,
               ),
               IconButton(
                 icon: const ImageIcon(
                   AssetImage(
                     '$_iconAssetLocation/twotone_delete.png',
                     package: _assetsPackage,
                   ),
                 ),
                 onPressed: () {
                   model.deleteEmail(
                     model.currentlySelectedInbox,
                     model.currentlySelectedEmailId,
                   );

                   mobileMailNavKey.currentState.pop();
                   model.currentlySelectedEmailId = -1;
                 },
                 color: ReplyColors.white50,
               ),
               IconButton(
                 icon: const Icon(Icons.more_vert),
                 onPressed: () {},
                 color: ReplyColors.white50,
               ),
             ],
           )
         : Align(
             alignment: AlignmentDirectional.bottomEnd,
             child: IconButton(
               icon: const Icon(Icons.search),
               color: ReplyColors.white50,
               onPressed: () {
                 Provider.of<RouterProvider>(
                   context,
                   listen: false,
                 ).routePath = ReplySearchPath();
               },
             ),
           ),
);

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

Android

iOS

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.

Android

iOS

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 diberikan oleh library Flutter Material serta framework Flutter, pastikan untuk mengunjungi Flutter Gallery.

52f7119a30bb8f5c.png

dd11628e4c0f3fd3.png