Tạo hiệu ứng chuyển đổi đẹp mắt bằng Material Motion cho Flutter

1. Giới thiệu

Material Design là hệ thống để tạo các sản phẩm kỹ thuật số đẹp mắt và ấn tượng. Bằng cách hợp nhất phong cách, cách xây dựng thương hiệu, sự tương tác và chuyển động theo một bộ nguyên tắc và thành phần nhất quán, các nhóm phụ trách sản phẩm có thể phát hiện ra tiềm năng thiết kế lớn nhất của họ.

logo_components_color_2x_web_96dp.png

Thành phần Material (MDC) giúp nhà phát triển triển khai Material Design. Được tạo bởi một nhóm các kỹ sư và nhà thiết kế trải nghiệm người dùng tại Google, MDC có nhiều thành phần giao diện người dùng đẹp mắt, dễ sử dụng và có sẵn cho Android, iOS, web và Flutter.material.io/develop

Hệ thống chuyển động của Material dành cho Flutter là gì?

Hệ thống chuyển động Material dành cho Flutter là một tập hợp các mẫu chuyển đổi trong gói ảnh động có thể giúp người dùng hiểu và thao tác trong ứng dụng, như mô tả trong nguyên tắc Material Design.

Sau đây là 4 mẫu chuyển đổi chính của Material:

  • Chuyển đổi vùng chứa: chuyển đổi giữa các thành phần giao diện người dùng có chứa một vùng chứa; tạo kết nối rõ ràng giữa hai phần tử riêng biệt trên giao diện người dùng bằng cách chuyển đổi liền mạch một phần tử thành phần tử khác.

11807bdf36c66657.gif

  • Trục chung: chuyển đổi giữa các thành phần giao diện người dùng có mối quan hệ về không gian hoặc điều hướng; sử dụng phép biến đổi chung trên trục x, y hoặc z để củng cố mối quan hệ giữa các phần tử.

71218f390abae07e.gif

  • Fade Through: chuyển đổi giữa các phần tử giao diện người dùng không có mối quan hệ chặt chẽ với nhau; sử dụng hiệu ứng làm mờ và hiện dần theo tuần tự theo tỷ lệ của phần tử chuyển đến.

385ba37b8da68969.gif

  • Độ mờ: dùng cho các thành phần trên giao diện người dùng đi vào hoặc thoát ra trong giới hạn màn hình.

cfc40fd6e27753b6.gif

Gói ảnh động cung cấp các tiện ích chuyển đổi cho các mẫu này, được xây dựng dựa trên cả thư viện ảnh động Flutter (flutter/animation.dart) và thư viện Material Design (flutter/material.dart):

Trong lớp học lập trình này, bạn sẽ sử dụng hiệu ứng chuyển đổi Material được xây dựng dựa trên khung Flutter và thư viện Material, nghĩa là bạn sẽ xử lý các tiện ích. :)

Sản phẩm bạn sẽ tạo ra

Lớp học lập trình này sẽ hướng dẫn bạn cách xây dựng một số hiệu ứng chuyển đổi thành một ứng dụng email Flutter mẫu có tên là Trả lời bằng Dart, để minh hoạ cách bạn có thể sử dụng các hiệu ứng chuyển đổi từ gói ảnh động để tuỳ chỉnh giao diện của ứng dụng.

Chúng tôi sẽ cung cấp đoạn mã khởi đầu cho ứng dụng Reply (Trả lời) và bạn sẽ kết hợp các hiệu ứng chuyển đổi Material sau đây vào ứng dụng. Bạn có thể xem ảnh GIF đã hoàn thành của lớp học lập trình dưới đây:

  • Chuyển đổi Chuyển đổi vùng chứa từ danh sách email sang trang chi tiết email
  • Chuyển đổi Chuyển đổi vùng chứa từ FAB sang trang email Compose
  • Chuyển đổi Trục Z được chia sẻ từ biểu tượng tìm kiếm sang trang chế độ xem tìm kiếm
  • Chuyển đổi Fade through (Làm mờ) giữa các trang hộp thư
  • Chuyển đổi Fade Through giữa soạn thư và FAB trả lời
  • Chuyển đổi Fade Through giữa tiêu đề hộp thư biến mất
  • Chuyển đổi Fade through (Làm mờ) giữa các thao tác trên thanh ứng dụng ở dưới cùng

b26fe84fed12d17d.gif

Bạn cần có

  • Kiến thức cơ bản về cách phát triển Flutter và Dart
  • Trình soạn thảo mã
  • Một thiết bị hoặc trình mô phỏng Android/iOS
  • Mã mẫu (xem bước tiếp theo)

Bạn đánh giá thế nào về mức độ kinh nghiệm tạo ứng dụng Flutter của mình?

Người mới tập Trung cấp Thành thạo

Bạn muốn tìm hiểu gì từ lớp học lập trình này?

Tôi mới biết đến chủ đề này và tôi muốn có thông tin tổng quan đầy đủ. Tôi đã biết đôi chút về chủ đề này, nhưng tôi muốn ôn lại kiến thức. Tôi đang tìm mã mẫu để sử dụng trong dự án của mình. Tôi muốn được giải thích cụ thể hơn.

2. Thiết lập môi trường phát triển Flutter

Bạn cần có 2 phần mềm để hoàn thành phòng thí nghiệm này – Flutter SDKtrình chỉnh sửa.

Bạn có thể chạy lớp học lập trình bằng bất kỳ thiết bị nào sau đây:

  • Một thiết bị Android hoặc iOS thực kết nối với máy tính của bạn và được đặt ở Chế độ nhà phát triển.
  • Trình mô phỏng iOS (yêu cầu cài đặt công cụ Xcode).
  • Trình mô phỏng Android (yêu cầu thiết lập trong Android Studio).
  • Trình duyệt (cần có Chrome để gỡ lỗi).
  • Dưới dạng ứng dụng Windows, Linux hoặc macOS. Bạn phải phát triển trên nền tảng mà bạn dự định triển khai. Vì vậy, nếu muốn phát triển một ứng dụng Windows dành cho máy tính, bạn phải phát triển trên Windows để truy cập vào chuỗi bản dựng phù hợp. Có các yêu cầu cụ thể theo hệ điều hành được đề cập chi tiết trên docs.flutter.dev/desktop.

3. Tải ứng dụng khởi đầu của lớp học lập trình

Cách 1: Sao chép ứng dụng của lớp học lập trình khởi đầu trên GitHub

Để sao chép lớp học lập trình này từ GitHub, hãy chạy các lệnh sau:

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

Cách 2: Tải tệp zip của ứng dụng dành cho lớp học lập trình ban đầu

Ứng dụng khởi đầu nằm trong thư mục material-components-flutter-motion-codelab-starter.

Xác minh các phần phụ thuộc của dự án

Dự án phụ thuộc vào gói ảnh động. Trong pubspec.yaml, hãy lưu ý rằng phần dependencies bao gồm những nội dung sau:

animations: ^2.0.0

Mở dự án và chạy ứng dụng

  1. Mở dự án trong trình chỉnh sửa mà bạn chọn.
  2. Làm theo hướng dẫn để "Chạy ứng dụng" trong phần Bắt đầu: Dùng thử cho trình chỉnh sửa mà bạn đã chọn.

Thành công! Đoạn mã khởi đầu cho trang chủ của Reply (Trả lời) sẽ chạy trên thiết bị/trình mô phỏng của bạn. Bạn sẽ thấy Hộp thư đến chứa danh sách các email.

Trang chủ Trả lời

Không bắt buộc: Làm chậm ảnh động trên thiết bị

Vì lớp học lập trình này liên quan đến các hiệu ứng chuyển đổi nhanh chóng nhưng chỉn chu, nên có thể hữu ích khi bạn làm chậm ảnh động của thiết bị để quan sát một số chi tiết nhỏ hơn của các hiệu ứng chuyển đổi khi bạn đang triển khai. Bạn có thể thực hiện việc này thông qua chế độ cài đặt trong ứng dụng. Bạn chỉ cần nhấn vào biểu tượng cài đặt khi ngăn dưới cùng đang mở. Đừng lo, phương pháp làm chậm ảnh động trên thiết bị này sẽ không ảnh hưởng đến ảnh động trên thiết bị bên ngoài ứng dụng Reply (Trả lời).

d23a7bfacffac509.gif

Không bắt buộc: Chế độ tối

Nếu chủ đề tươi sáng của Reply (Trả lời) làm bạn tổn thương mắt, thì bạn cũng không cần làm gì khác nữa. Ứng dụng có một chế độ cài đặt giúp bạn thay đổi giao diện ứng dụng thành chế độ tối cho phù hợp hơn với mắt. Bạn có thể truy cập chế độ cài đặt này bằng cách nhấn vào biểu tượng cài đặt khi ngăn dưới cùng đang mở.

87618d8418eee19e.gif

4. Làm quen với mã ứng dụng mẫu

Hãy cùng xem mã. Chúng tôi đã cung cấp một ứng dụng sử dụng gói ảnh động để chuyển đổi giữa các màn hình trong ứng dụng.

  • HomePage:hiển thị hộp thư đã chọn
  • InboxPage: hiển thị một danh sách các email
  • MailPreviewCard: hiển thị bản xem trước của một email
  • MailViewPage:hiển thị một email đầy đủ
  • ComposePage: cho phép soạn email mới
  • SearchPage:hiển thị chế độ xem tìm kiếm

router.dart

Trước tiên, để tìm hiểu cách thiết lập điều hướng gốc của ứng dụng, hãy mở router.dart trong thư mục 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);
 }
}

Đây là trình điều hướng gốc và xử lý các màn hình của ứng dụng sử dụng toàn bộ canvas, chẳng hạn như HomePageSearchPage. API này theo dõi trạng thái của ứng dụng để kiểm tra xem chúng ta đã thiết lập tuyến đến ReplySearchPath hay chưa. Nếu đã tạo thì công cụ này sẽ xây dựng lại trình điều hướng bằng SearchPage ở đầu ngăn xếp. Lưu ý rằng màn hình của chúng ta được gói trong một CustomTransitionPage mà chưa xác định hiệu ứng chuyển đổi. Hướng dẫn này chỉ cho bạn một cách di chuyển giữa các màn hình mà không cần chuyển đổi tuỳ chỉnh.

home.dart

Chúng ta thiết lập tuyến đường đến ReplySearchPath ở trạng thái ứng dụng bằng cách thực hiện các thao tác sau bên trong _BottomAppBarActionItems trong 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();
   },
 ),
);

Trong tham số onPressed, chúng ta truy cập vào RouterProvider và đặt routePath của nó thành ReplySearchPath. RouterProvider của chúng tôi theo dõi trạng thái của trình điều hướng gốc.

mail_view_router.dart

Bây giờ, hãy xem cách thiết lập thành phần điều hướng bên trong của ứng dụng, hãy mở mail_view_router.dart trong thư mục lib. Bạn sẽ thấy một trình điều hướng tương tự như trình điều hướng ở trên:

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

Đây là công cụ điều hướng bên trong của chúng tôi. Tệp này xử lý các màn hình bên trong của ứng dụng chỉ sử dụng phần thân của canvas, chẳng hạn như InboxPage. InboxPage cho thấy danh sách email, tuỳ thuộc vào hộp thư hiện tại trong trạng thái của ứng dụng. Trình điều hướng được xây dựng lại bằng InboxPage chính xác ở đầu ngăn xếp, mỗi khi có thay đổi về thuộc tính currentlySelectedInbox của trạng thái ứng dụng.

home.dart

Chúng ta đặt hộp thư hiện tại ở trạng thái của ứng dụng bằng cách thực hiện thao tác sau đây bên trong _HomePageState trong 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(() {});
}

Trong hàm _onDestinationSelected, chúng ta truy cập vào EmailStore và đặt currentlySelectedInbox của nó thành đích đến đã chọn. EmailStore của chúng tôi theo dõi trạng thái của các trình điều hướng bên trong.

home.dart

Cuối cùng, để xem ví dụ về thao tác định tuyến điều hướng đang được sử dụng, hãy mở home.dart trong thư mục lib. Tìm lớp _ReplyFabState bên trong thuộc tính onTap của tiện ích InkWell. Lớp này có dạng như sau:

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

Hình ảnh này cho biết cách bạn có thể điều hướng đến trang soạn email mà không có bất kỳ hiệu ứng chuyển đổi tuỳ chỉnh nào. Trong lớp học lập trình này, bạn sẽ tìm hiểu sâu hơn về mã của Reply (Trả lời) để thiết lập các hiệu ứng chuyển đổi Material hoạt động song song với các thao tác điều hướng khác nhau trong ứng dụng.

Giờ bạn đã quen với mã khởi đầu, hãy triển khai hiệu ứng chuyển đổi đầu tiên.

5. Thêm hiệu ứng chuyển đổi Biến đổi vùng chứa từ danh sách email vào trang chi tiết email

Để bắt đầu, bạn sẽ thêm hiệu ứng chuyển đổi khi nhấp vào một email. Đối với thay đổi về điều hướng này, mẫu biến đổi vùng chứa là rất phù hợp, vì mẫu này được thiết kế để chuyển đổi giữa các phần tử trên giao diện người dùng có chứa vùng chứa. Mẫu này tạo ra sự kết nối rõ ràng giữa 2 thành phần trên giao diện người dùng.

Trước khi thêm mã, hãy thử chạy ứng dụng Reply (Trả lời) và nhấp vào một email. Thao tác này sẽ thực hiện một cú nhảy đơn giản, nghĩa là màn hình được thay thế mà không có hiệu ứng chuyển đổi:

Trước

48b00600f73c7778.gif

Bắt đầu bằng cách thêm lệnh nhập cho gói ảnh động ở đầu mail_card_preview.dart như trong đoạn mã sau:

mail_card_preview.dart

import 'package:animations/animations.dart';

Bây giờ, bạn đã nhập gói ảnh động, chúng ta có thể bắt đầu thêm các hiệu ứng chuyển đổi đẹp mắt vào ứng dụng của bạn. Hãy bắt đầu bằng cách tạo một lớp StatelessWidget chứa tiện ích OpenContainer.

Trong mail_card_preview.dart, hãy thêm đoạn mã sau đây vào sau phần định nghĩa lớp của 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,
       );
     },
   );
 }
}

Bây giờ, hãy sử dụng trình bao bọc mới. Bên trong định nghĩa lớp MailPreviewCard, chúng ta sẽ gói tiện ích Material của hàm build() bằng _OpenContainerWrapper mới:

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 của chúng ta có một tiện ích InkWell và các thuộc tính màu của OpenContainer xác định màu của vùng chứa mà nó đính kèm. Do đó, chúng ta có thể xoá các tiện ích Material và Inkwell. Mã kết quả có dạng như sau:

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

Ở giai đoạn này, bạn sẽ phải biến đổi vùng chứa hoạt động hoàn toàn. Khi nhấp vào một email, mục danh sách sẽ mở rộng sang màn hình chi tiết trong khi xem lại danh sách email. Thao tác nhấn quay lại sẽ thu gọn màn hình chi tiết email trở lại một mục danh sách trong khi vẫn mở rộng danh sách email.

Sau

663e8594319bdee3.gif

6. Thêm hiệu ứng chuyển đổi Biến đổi vùng chứa từ FAB vào trang email Compose

Hãy tiếp tục chuyển đổi vùng chứa và thêm hiệu ứng chuyển đổi từ Nút hành động nổi sang ComposePage để mở rộng FAB sang một email mới do người dùng viết. Trước tiên, chạy lại ứng dụng rồi nhấp vào nút hành động nổi để đảm bảo không có hiệu ứng chuyển đổi khi khởi chạy màn hình soạn email.

Trước

4aa2befdc5170c60.gif

Cách chúng ta định cấu hình hiệu ứng chuyển đổi này sẽ rất giống với cách chúng ta thực hiện trong bước trước, vì chúng ta đang sử dụng cùng một lớp tiện ích là OpenContainer.

Trong home.dart, hãy nhập package:animations/animations.dart ở đầu tệp và sửa đổi phương thức _ReplyFabState build(). Hãy gói tiện ích Material được trả về bằng một tiện ích 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,
     ...

Ngoài các tham số dùng để định cấu hình tiện ích OpenContainer trước đó, onClosed hiện cũng đang được thiết lập. onClosed là một ClosedCallback được gọi khi tuyến OpenContainer đã được bật lên hoặc đã quay lại trạng thái đóng. Giá trị trả về của giao dịch đó được truyền vào hàm này dưới dạng đối số. Chúng ta dùng Callback này để thông báo cho nhà cung cấp ứng dụng rằng chúng ta đã rời khỏi tuyến ComposePage để có thể thông báo cho tất cả trình nghe.

Tương tự như bước trước, chúng ta sẽ xoá tiện ích Material khỏi tiện ích, vì tiện ích OpenContainer xử lý màu của tiện ích do closedBuilder trả về bằng closedColor. Chúng ta cũng sẽ xoá lệnh gọi Navigator.push() bên trong onTap của tiện ích InkWell và thay thế bằng openContainer() Callback do closedBuilder của tiện ích OpenContainer cung cấp, vì hiện tại tiện ích OpenContainer đang xử lý quy trình định tuyến riêng.

Mã kết quả có dạng như sau:

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

Bây giờ, hãy dọn dẹp một số mã cũ. Vì tiện ích OpenContainer hiện xử lý việc thông báo cho nhà cung cấp ứng dụng rằng chúng ta không còn sử dụng ComposePage thông qua onClosed ClosedCallback nữa, nên chúng ta có thể xoá phương thức triển khai trước đó trong 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);

Vậy là xong! Bạn sẽ có hiệu ứng chuyển đổi từ FAB sang màn hình soạn thư như sau:

Sau

5c7ad1b4b40f9f0c.gif

7. Thêm hiệu ứng chuyển đổi Trục Z dùng chung từ biểu tượng tìm kiếm vào trang xem tìm kiếm

Trong bước này, chúng ta sẽ thêm hiệu ứng chuyển đổi từ biểu tượng tìm kiếm sang chế độ xem tìm kiếm toàn màn hình. Vì không có vùng chứa lâu dài nào liên quan đến sự thay đổi về cách điều hướng này, nên chúng ta có thể sử dụng hiệu ứng chuyển đổi Trục Z dùng chung để củng cố mối quan hệ không gian giữa hai màn hình và cho biết việc di chuyển một cấp lên trên trong hệ phân cấp của ứng dụng.

Trước khi thêm mã bổ sung, hãy thử chạy ứng dụng và nhấn vào biểu tượng tìm kiếm ở góc dưới cùng bên phải màn hình. Thao tác này sẽ làm xuất hiện màn hình chế độ xem tìm kiếm mà không có hiệu ứng chuyển đổi.

Trước

df7683a8ad7b920e.gif

Để bắt đầu, hãy chuyển đến tệp router.dart. Sau phần định nghĩa lớp ReplySearchPath, hãy thêm đoạn mã sau:

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

Bây giờ, hãy sử dụng SharedAxisTransitionPageWrapper mới để đạt được hiệu ứng chuyển đổi mà chúng ta muốn. Bên trong định nghĩa lớp ReplyRouterDelegate, trong thuộc tính pages, hãy gói màn hình tìm kiếm bằng SharedAxisTransitionPageWrapper thay vì 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(),
     ),
 ],
);

Bây giờ, hãy thử chạy lại ứng dụng.

81b3ea098926931.gif

Mọi thứ đang bắt đầu ổn! Khi bạn nhấp vào biểu tượng tìm kiếm trong thanh ứng dụng ở dưới cùng, chuyển đổi trục chung sẽ chia tỷ lệ trang tìm kiếm vào chế độ xem. Tuy nhiên, hãy lưu ý cách trang chủ không thu nhỏ và thay vào đó trang chủ vẫn tĩnh khi trang tìm kiếm phóng to trên đó. Ngoài ra, khi người dùng nhấn vào nút quay lại, trang chủ sẽ không thu nhỏ ở chế độ xem, mà sẽ ở trạng thái tĩnh khi trang tìm kiếm thu nhỏ so với chế độ xem. Vậy là chúng ta vẫn chưa hoàn tất.

Hãy khắc phục cả hai vấn đề bằng cách gói HomePage bằng SharedAxisTransitionWrapper thay vì 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(),
     ),
 ],
);

Vậy là xong! Bây giờ, hãy thử chạy lại ứng dụng rồi nhấn vào biểu tượng tìm kiếm. Màn hình chính và màn hình chế độ xem tìm kiếm phải mờ dần và mở rộng đồng thời theo chiều sâu trục Z, tạo ra hiệu ứng liền mạch giữa hai màn hình.

Sau

462d890086a3d18a.gif

8. Thêm hiệu ứng chuyển đổi mờ dần giữa các trang hộp thư

Trong bước này, chúng ta sẽ thêm hiệu ứng chuyển đổi giữa các hộp thư khác nhau. Vì không muốn nhấn mạnh mối quan hệ không gian hoặc phân cấp, chúng ta sẽ sử dụng hiệu ứng làm mờ để thực hiện một thao tác "hoán đổi" đơn giản giữa các danh sách email.

Trước khi thêm bất kỳ mã bổ sung nào, hãy thử chạy ứng dụng, nhấn vào biểu trưng Reply (Trả lời) trong Thanh ứng dụng ở dưới cùng và chuyển đổi hộp thư. Danh sách email sẽ thay đổi mà không có hiệu ứng chuyển đổi.

Trước

89033988ce26b92e.gif

Để bắt đầu, hãy chuyển đến tệp mail_view_router.dart. Sau phần định nghĩa lớp MailViewRouterDelegate, hãy thêm đoạn mã sau:

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

Tương tự như bước cuối cùng, hãy sử dụng FadeThroughTransitionPageWrapper mới để đạt được hiệu ứng chuyển đổi mà chúng ta muốn. Bên trong định nghĩa lớp MailViewRouterDelegate, trong thuộc tính pages, thay vì gói màn hình hộp thư bằng CustomTransitionPage, hãy sử dụng 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),
   ),
 ],
);

Chạy lại ứng dụng. Khi bạn mở ngăn điều hướng ở dưới cùng và thay đổi hộp thư, danh sách email hiện tại sẽ mờ dần và thu nhỏ trong khi danh sách mới mờ dần và thu nhỏ. Tuyệt vời!

Sau

8186940082b630d.gif

9. Thêm hiệu ứng chuyển đổi mờ dần giữa soạn thư và nút hành động nổi trả lời

Ở bước này, chúng ta sẽ thêm hiệu ứng chuyển đổi giữa các biểu tượng FAB. Vì không muốn nhấn mạnh mối quan hệ không gian hoặc phân cấp, chúng ta sẽ sử dụng hiệu ứng làm mờ để thực hiện một thao tác "hoán đổi" đơn giản giữa các biểu tượng trong FAB.

Trước khi thêm bất kỳ mã bổ sung nào, hãy thử chạy ứng dụng, nhấn vào một email và mở chế độ xem email. Biểu tượng FAB phải thay đổi mà không có hiệu ứng chuyển đổi.

Trước

d8e3afa0447cfc20.gif

Chúng ta sẽ làm việc trong home.dart cho phần còn lại của lớp học lập trình này, vì vậy bạn đừng lo lắng về việc thêm gói nhập cho gói ảnh động vì chúng ta đã làm với home.dart ở bước 2.

Cách chúng ta định cấu hình vài hiệu ứng chuyển đổi tiếp theo sẽ rất giống nhau, vì tất cả đều sử dụng một lớp có thể sử dụng lại là _FadeThroughTransitionSwitcher.

Trong home.dart, hãy thêm đoạn mã sau trong _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,
   );
 }
}

Bây giờ, trong _ReplyFabState, hãy tìm tiện ích fabSwitcher. fabSwitcher trả về một biểu tượng khác tuỳ vào việc biểu tượng đó có ở chế độ xem email hay không. Hãy kết thúc bằng _FadeThroughTransitionSwitcher của chúng ta:

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

Chúng ta cung cấp cho _FadeThroughTransitionSwitcher một fillColor trong suốt để không có nền giữa các phần tử khi chuyển đổi. Chúng ta cũng tạo một UniqueKey rồi gán cho một trong các biểu tượng.

Bây giờ, ở bước này, bạn sẽ có một FAB theo ngữ cảnh được tạo ảnh động hoàn toàn. Khi chuyển sang chế độ xem email, biểu tượng nút hành động nổi cũ sẽ mờ dần và thu nhỏ trong khi biểu tượng mới mờ dần và thu nhỏ.

Sau

c55bacd9a144ec69.gif

10. Thêm hiệu ứng chuyển đổi mờ dần giữa tiêu đề hộp thư biến mất

Trong bước này, chúng ta sẽ thêm hiệu ứng chuyển đổi mờ dần, để làm mờ tiêu đề hộp thư giữa trạng thái hiển thị và ẩn khi ở chế độ xem email. Vì không muốn nhấn mạnh mối quan hệ không gian hoặc phân cấp, chúng ta sẽ sử dụng hiệu ứng làm mờ để thực hiện một thao tác "hoán đổi" đơn giản giữa tiện ích Text bao gồm tiêu đề hộp thư và SizedBox trống.

Trước khi thêm bất kỳ mã bổ sung nào, hãy thử chạy ứng dụng, nhấn vào một email và mở chế độ xem email. Tiêu đề hộp thư sẽ biến mất mà không có hiệu ứng chuyển đổi.

Trước

59eb57a6c71725c0.gif

Phần còn lại của lớp học lập trình này sẽ ngắn gọn vì chúng ta đã thực hiện hầu hết các việc trong _FadeThroughTransitionSwitcher ở bước trước.

Bây giờ, hãy chuyển đến lớp _AnimatedBottomAppBar trong home.dart để thêm hiệu ứng chuyển đổi. Chúng ta sẽ sử dụng lại _FadeThroughTransitionSwitcher ở bước cuối cùng và gói điều kiện onMailView có thể trả về SizedBox trống hoặc tiêu đề hộp thư bị mờ đồng bộ với ngăn dưới cùng:

home.dart

...
const _ReplyLogo(),
const SizedBox(width: 10),
// TODO: Add Fade through transition between disappearing mailbox title (Motion)
_FadeThroughTransitionSwitcher(
 fillColor: Colors.transparent,
 child: onMailView
     ? const SizedBox(width: 48)
     : FadeTransition(
         opacity: fadeOut,
         child: Selector<EmailStore, String>(
           selector: (context, emailStore) =>
               emailStore.currentlySelectedInbox,
           builder: (
             context,
             currentlySelectedInbox,
             child,
           ) {
             return Text(
               currentlySelectedInbox,
               style: Theme.of(context)
                   .textTheme
                   .bodyMedium!
                   .copyWith(
                     color: ReplyColors.white50,
                   ),
             );
           },
         ),
       ),
),

Vậy là xong, chúng ta đã hoàn tất bước này!

Chạy lại ứng dụng. Khi bạn mở email và được đưa đến chế độ xem email, tiêu đề hộp thư trong thanh ứng dụng ở dưới cùng sẽ mờ dần và thu nhỏ. Tuyệt vời!

Sau

3f1a3db01a481124.gif

11. Thêm hiệu ứng chuyển đổi mờ dần giữa các thao tác trên thanh ứng dụng ở dưới cùng

Ở bước này, chúng ta sẽ thêm hiệu ứng làm mờ theo hiệu ứng chuyển đổi, để làm mờ theo các thao tác trên thanh ứng dụng ở dưới cùng dựa trên ngữ cảnh của ứng dụng. Vì không muốn nhấn mạnh mối quan hệ không gian hoặc phân cấp, chúng ta sẽ sử dụng hiệu ứng làm mờ để thực hiện một thao tác "hoán đổi" đơn giản giữa các thao tác trên thanh ứng dụng ở dưới cùng khi ứng dụng ở trên Trang chủ, khi ngăn dưới cùng hiển thị và khi chúng ta đang ở chế độ xem email.

Trước khi thêm bất kỳ mã bổ sung nào, hãy thử chạy ứng dụng, nhấn vào một email và mở chế độ xem email. Bạn cũng có thể thử nhấn vào biểu trưng Reply (Trả lời). Các thao tác trên thanh ứng dụng ở dưới cùng phải thay đổi mà không có hiệu ứng chuyển đổi.

Trước

5f662eac19fce3ed.gif

Tương tự như bước trước, chúng ta sẽ sử dụng lại _FadeThroughTransitionSwitcher. Để có hiệu ứng chuyển đổi mong muốn, hãy chuyển đến phần định nghĩa lớp _BottomAppBarActionItems và gói tiện ích trả về của hàm build() bằng _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
...

Giờ hãy thử xem! Khi bạn mở email và được đưa đến chế độ xem email, các thao tác cũ trên thanh ứng dụng ở dưới cùng sẽ mờ dần và thu nhỏ trong khi các thao tác mới hiện dần và thu nhỏ. Chính xác!

Sau

cff0fa2afa1c5a7f.gif

12. Xin chúc mừng!

Sử dụng ít hơn 100 dòng mã Dart, gói ảnh động đã giúp bạn tạo ra các hiệu ứng chuyển đổi đẹp mắt trong ứng dụng hiện có tuân thủ nguyên tắc Material Design, đồng thời giao diện và hoạt động nhất quán trên tất cả các thiết bị.

d5637de49eb64d8a.gif

Các bước tiếp theo

Để biết thêm thông tin về hệ thống chuyển động Material, hãy nhớ xem hướng dẫntài liệu đầy đủ dành cho nhà phát triển, đồng thời thử thêm một số hiệu ứng chuyển đổi Material vào ứng dụng!

Cảm ơn bạn đã dùng thử tính năng Chuyển động Material. Chúng tôi hy vọng bạn thích lớp học lập trình này!

Tôi đã có thể hoàn thành lớp học lập trình này với khá nhiều thời gian và công sức

Hoàn toàn đồng ý Đồng ý Bình thường Không đồng ý Hoàn toàn không đồng ý

Tôi muốn tiếp tục sử dụng hệ thống chuyển động Material trong tương lai

Hoàn toàn đồng ý Đồng ý Bình thường Không đồng ý Hoàn toàn không đồng ý

Để xem thêm bản minh hoạ cách sử dụng các tiện ích do thư viện Material Flutter cung cấp, cũng như khung Flutter, đừng quên truy cập Flutter Gallery.

46ba920f17198998.pngS

6ae8ae284bf4f9fa.png.