1. Giới thiệu
Material Design là một hệ thống để xây dựng các sản phẩm kỹ thuật số nổi bật và đẹp mắt. Bằng cách kết hợp phong cách, thương hiệu, hoạt động 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 sản phẩm có thể nhận ra tiềm năng thiết kế lớn nhất của mình.
| 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 kỹ sư và nhà thiết kế trải nghiệm người dùng tại Google, MDC có hàng chục thành phần giao diện người dùng đẹp mắt và có chức năng, đồng thời có sẵn cho Android, iOS, web và Flutter.material.io/develop |
Hệ thống chuyển động của Material cho Flutter là gì?
Hệ thống chuyển động Material 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à điều hướng một ứ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:
- Biến đổi vùng chứa: chuyển đổi giữa các phần tử giao diện người dùng có vùng chứa; tạo mối liên kết dễ thấy giữa 2 phần tử giao diện người dùng riêng biệt bằng cách biến đổi một phần tử thành phần tử khác một cách liền mạch.

- Trục chung: chuyển đổi giữa các phần tử trên giao diện người dùng có mối quan hệ về hướng hoặc không gian; sử dụng một 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ử.

- Chuyển mờ: chuyển đổi giữa các thành phần 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 mờ dần rồi mờ dần theo trình tự, với tỷ lệ của phần tử đến.

- Làm mờ: dùng cho các thành phần giao diện người dùng đi vào hoặc thoát ra trong phạm vi màn hình.

Gói ảnh động cung cấp các tiện ích chuyển đổi cho những mẫu này, được xây dựng trên cả thư viện ảnh động Flutter (flutter/animation.dart) và thư viện Material của Flutter (flutter/material.dart):
Trong lớp học lập trình này, bạn sẽ sử dụng các hiệu ứng chuyển đổi Material được xây dựng trên nền tảng khung Flutter và thư viện Material, tức là bạn sẽ làm việc với 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 tạo một số hiệu ứng chuyển đổi trong một ứng dụng email mẫu có tên là Reply (Trả lời) bằng Flutter, sử dụng Dart, để minh hoạ cách bạn có thể sử dụng hiệu ứng chuyển đổi từ gói ảnh động để tuỳ chỉnh giao diện của ứng dụng.
Mã khởi đầu cho ứng dụng Reply sẽ được cung cấp 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 các hiệu ứng này trong GIF của lớp học lập trình đã hoàn tất bên dưới:
- Hiệu ứng chuyển đổi Biến đổi vùng chứa từ danh sách email sang trang chi tiết email
- Hiệu ứng chuyển đổi Biến đổi vùng chứa từ FAB sang trang soạn email
- Hiệu ứng chuyển đổi Trục Z chung từ biểu tượng tìm kiếm sang trang chế độ xem tìm kiếm
- Hiệu ứng chuyển đổi Mờ dần giữa các trang hộp thư
- Hiệu ứng chuyển đổi Mờ dần giữa FAB soạn và FAB trả lời
- Hiệu ứng chuyển đổi Mờ dần giữa tiêu đề hộp thư biến mất
- 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ạ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ã
- Trình mô phỏng hoặc thiết bị Android/iOS
- Mã mẫu (xem bước tiếp theo)
Bạn đánh giá thế nào về mức độ kinh nghiệm của mình trong việc tạo ứng dụng Flutter?
Bạn muốn học được điều gì qua lớp học lập trình này?
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 bài thực hành này: Flutter SDK và một trình chỉnh sửa.
Bạn có thể chạy lớp học lập trình này bằng bất kỳ thiết bị nào sau đây:
- Một thiết bị Android hoặc iOS thực được kết nối với máy tính và được đặt ở Chế độ nhà phát triển.
- Trình mô phỏng iOS (bạn cần cài đặt các công cụ Xcode).
- Trình mô phỏng Android (cần thiết lập trong Android Studio).
- Một trình duyệt (bạn cần có Chrome để gỡ lỗi).
- Dưới dạng ứng dụng máy tính cho 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 máy tính cho Windows, bạn phải phát triển trên Windows để truy cập vào chuỗi bản dựng thích hợp. Có những 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 xuống
Cách 1: Sao chép ứng dụng khởi đầu của lớp học lập trình 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 lớp học lập trình dành cho người mới bắt đầ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 này 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
- Mở dự án trong trình chỉnh sửa mà bạn chọn.
- Làm theo hướng dẫn để "Chạy ứng dụng" trong phần Bắt đầu: Lái thử cho trình chỉnh sửa bạn chọn.
Thành công! Mã khởi đầu cho trang chủ của Reply 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 email.

Không bắt buộc: Giảm tốc độ ả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 nhưng mượt mà, nên bạn có thể làm chậm các ảnh động của thiết bị để quan sát một số chi tiết tinh tế hơn của các hiệu ứng chuyển đổi khi triển khai. Bạn có thể thực hiện việc này thông qua một chế độ cài đặt trong ứng dụng. 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 mở. Đừng lo lắng, 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.

Không bắt buộc: Chế độ tối
Nếu chủ đề sáng của Reply khiến bạn khó chịu mắt, thì bạn không cần tìm đâu xa. Ứng dụng có một chế độ cài đặt cho phép bạn thay đổi giao diện ứng dụng sang chế độ tối để phù hợp hơn với mắt của bạn. 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ở.

4. Làm quen với mã ứng dụng mẫu
Hãy xem mã này. 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.
- Trang chủ: hiển thị hộp thư đã chọn
- InboxPage: hiển thị danh sách email
- MailPreviewCard: hiển thị bản xem trước của email
- MailViewPage: hiển thị một email đầy đủ
- ComposePage: cho phép soạn email mới
- SearchPage: hiển thị một khung hiển thị tìm kiếm
router.dart
Trước tiên, để tìm hiểu cách thiết lập chế độ đ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 của chúng ta và trình điều hướng này xử lý các màn hình của ứng dụng chiếm toàn bộ canvas, chẳng hạn như HomePage và SearchPage. Nó lắng nghe trạng thái của ứng dụng để kiểm tra xem chúng ta đã đặt tuyến đường đến ReplySearchPath hay chưa. Nếu có, thì nó sẽ tạo lại trình điều hướng của chúng ta với SearchPage ở đầu ngăn xếp. Lưu ý rằng các màn hình của chúng ta được bao bọc trong một CustomTransitionPage mà không có hiệu ứng chuyển đổi nào được xác định. Điều này cho thấy một cách di chuyển giữa các màn hình mà không cần hiệu ứng chuyển đổi tuỳ chỉnh.
home.dart
Chúng ta đặt tuyến đường thành ReplySearchPath trong trạng thái của ứng dụng bằng cách làm như sau 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 tham số đó thành ReplySearchPath. RouterProvider của chúng tôi theo dõi trạng thái của các trình điều hướng gốc.
mail_view_router.dart
Bây giờ, hãy xem cách thiết lập chế độ đ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à người dẫn đường nội tại của chúng ta. Thành phần này xử lý các màn hình bên trong ứng dụng chỉ sử dụng phần nội dung của canvas, chẳng hạn như InboxPage. InboxPage hiển thị 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 tạo lại với InboxPage chính xác ở đầu ngăn xếp, bất cứ khi nào 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 trong trạng thái của ứng dụng bằng cách làm như sau 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 hàm này thành đích đến đã chọn. EmailStore của chúng tôi theo dõi trạng thái của trình điều hướng bên trong.
home.dart
Cuối cùng, để xem ví dụ về một định tuyến điều hướng đang được sử dụng, hãy mở home.dart trong thư mục lib. Xác định vị trí lớp _ReplyFabState, bên trong thuộc tính onTap của tiện ích InkWell. Lớp này sẽ 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 này minh hoạ cách bạn có thể chuyển đến trang soạn email mà không cần 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 kỹ mã của ứng dụng Reply để thiết lập các hiệu ứng chuyển đổi theo Material Design hoạt động song song với nhiều thao tác điều hướng trong ứng dụng.
Giờ khi bạn đã quen thuộc 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 sang trang chi tiết email
Để bắt đầu, bạn sẽ thêm một hiệu ứng chuyển đổi khi nhấp vào email. Đối với thay đổi điều hướng này, mẫu biến đổi vùng chứa rất phù hợp vì được thiết kế cho quá trình chuyển đổi giữa các thành phần giao diện người dùng có vùng chứa. Mẫu này tạo ra sự kết nối dễ thấy giữa 2 thành phần giao diện người dùng.
Trước khi thêm bất kỳ mã nào, hãy thử chạy ứng dụng Reply và nhấp vào một email. Đoạn chuyển cảnh nên là một đoạn cắt đơn giản, tức là màn hình được thay thế mà không có hiệu ứng chuyển cảnh:
Trước

Bắt đầu bằng cách thêm một lệnh nhập cho gói ảnh động ở đầu mail_card_preview.dart như minh hoạ trong đoạn mã sau:
mail_card_preview.dart
import 'package:animations/animations.dart';
Giờ đây, bạn đã có một quy trình nhập cho 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 sẽ chứa tiện ích OpenContainer của chúng ta.
Trong mail_card_preview.dart, hãy thêm đoạn mã sau sau phần khai báo 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 của chúng ta. Bên trong định nghĩa lớp MailPreviewCard, chúng ta sẽ bao bọc tiện ích Material từ 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à tiện ích đó bao quanh. Do đó, chúng ta có thể xoá các tiện ích Material và Inkwell. Mã kết quả sẽ 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ẽ có một hiệu ứng chuyển đổi vùng chứa hoạt động đầy đủ. Khi bạn nhấp vào một email, mục đó trong danh sách sẽ mở rộng thành một màn hình chi tiết trong khi danh sách email sẽ thu hẹp lại. Khi bạn nhấn nút quay lại, màn hình chi tiết email sẽ thu gọn thành một mục trong danh sách trong khi mở rộng trong danh sách email.
Sau

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

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 đã làm ở bước cuối cùng, vì chúng ta đang sử dụng cùng một lớp tiện ích, 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 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 đường OpenContainer đã bị loại bỏ hoặc đã quay về trạng thái đóng. Giá trị trả về của giao dịch đó được truyền đến hàm này dưới dạng một đối số. Chúng tôi sử dụng Callback này để thông báo cho nhà cung cấp ứng dụng rằng chúng tôi đã rời khỏi tuyến đường ComposePage, để nhà cung cấp có thể thông báo cho tất cả các trình nghe.
Tương tự như bước cuối cùng, 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ì giờ đây tiện ích OpenContainer đang xử lý định tuyến của riêng nó.
Mã kết quả sẽ 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 của ứng dụng rằng chúng ta không còn ở trên ComposePage thông qua onClosed ClosedCallback, nên chúng ta có thể xoá việ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ước này! Bạn sẽ có một hiệu ứng chuyển đổi từ FAB sang màn hình Compose trông như sau:
Sau

7. Thêm hiệu ứng chuyển đổi Trục Z dùng chung từ biểu tượng tìm kiếm sang trang chế độ xem tìm kiếm
Trong bước này, chúng ta sẽ thêm một hiệu ứng chuyển đổi từ biểu tượng tìm kiếm sang khung hiển thị tìm kiếm toàn màn hình. Vì không có vùng chứa cố định nào liên quan đến thay đổi đ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 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 lên một cấp 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ẽ mở màn hình chế độ xem tìm kiếm mà không có hiệu ứng chuyển đổi.
Trước

Để 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;
});
}
}
Giờ đây, hãy sử dụng SharedAxisTransitionPageWrapper mới để đạt được hiệu ứng chuyển cảnh mà chúng ta muốn. Trong định nghĩa lớp ReplyRouterDelegate, bên dưới thuộc tính pages, hãy bao bọc màn hình tìm kiếm bằng một 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.

Mọi thứ đang bắt đầu trở nên tuyệt vời! Khi bạn nhấp vào biểu tượng tìm kiếm trong thanh ứng dụng dưới cùng, hiệu ứng chuyển đổi trục chung sẽ thu nhỏ trang tìm kiếm để hiển thị. Tuy nhiên, hãy lưu ý cách trang chủ không mở rộng mà vẫn giữ nguyên khi trang tìm kiếm mở rộng lên trên trang chủ. Ngoài ra, khi bạn nhấn nút quay lại, trang chủ sẽ không thu nhỏ để hiển thị, mà vẫn giữ nguyên khi trang tìm kiếm thu nhỏ để ẩn đi. Vì vậy, chúng ta vẫn chưa hoàn thành.
Hãy khắc phục cả hai vấn đề này bằng cách cũng bao bọc 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 tìm kiếm sẽ đồng thời mờ dần và thu phóng theo trục Z theo chiều sâu, tạo hiệu ứng liền mạch giữa hai màn hình.
Sau

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 một hiệu ứng chuyển đổi giữa các hộp thư. Vì không muốn nhấn mạnh mối quan hệ về không gian hoặc thứ bậc, nên chúng ta sẽ sử dụng hiệu ứng mờ dần để thực hiện 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ã nào khác, 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

Để 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 cảnh mà chúng ta muốn. Bên trong định nghĩa lớp MailViewRouterDelegate, trong thuộc tính pages, thay vì bao bọc màn hình hộp thư bằng CustomTransitionPage, hãy 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 sẽ mờ dần và phóng to. Tuyệt vời!
Sau

9. Thêm hiệu ứng chuyển cảnh Mờ dần giữa FAB soạn và trả lời
Trong bước này, chúng ta sẽ thêm một 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ệ về không gian hoặc thứ bậc, nên chúng ta sẽ sử dụng hiệu ứng mờ dần để thực hiện 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ã nào khác, 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 sẽ thay đổi mà không có hiệu ứng chuyển đổi.
Trước

Chúng ta sẽ làm việc trong home.dart trong phần còn lại của lớp học lập trình này, vì vậy, đừng lo lắng về việc thêm câu lệnh nhập cho gói ảnh động vì chúng ta đã thực hiện cho home.dart ở bước 2.
Cách chúng ta định cấu hình một 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ể dùng lại là _FadeThroughTransitionSwitcher.
Trong home.dart, hãy thêm đoạn mã sau vào _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 sẽ trả về một biểu tượng khác tuỳ thuộc vào việc biểu tượng đó có ở chế độ xem email hay không. Hãy gói nó 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 sẽ 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 và chỉ định UniqueKey đó cho một trong các biểu tượng.
Giờ đây, ở bước này, bạn sẽ có một FAB theo bối cảnh được tạo hiệu ứng hoàn chỉnh. Khi bạn chuyển sang chế độ xem email, biểu tượng FAB cũ sẽ mờ dần và thu nhỏ, còn biểu tượng mới sẽ mờ dần và phóng to.
Sau

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 cảnh mờ dần để làm mờ tiêu đề hộp thư giữa trạng thái hiển thị và không hiển thị khi ở chế độ xem email. Vì không muốn nhấn mạnh mối quan hệ không gian hoặc thứ bậc, nên chúng ta sẽ sử dụng hiệu ứng mờ dần để thực hiện 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ã nào khác, 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

Phần còn lại của lớp học lập trình này sẽ diễn ra nhanh chóng vì chúng ta đã thực hiện hầu hết công việc trong _FadeThroughTransitionSwitcher ở bước cuối cùng.
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 từ bước cuối cùng và bao bọc điều kiện onMailView. Điều kiện này sẽ trả về một SizedBox trống hoặc tiêu đề hộp thư mờ dần đồ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ở một email và được chuyển đế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

11. Thêm hiệu ứng chuyển cảnh Mờ dần giữa các thao tác trên thanh ứng dụng dưới cùng
Trong bước này, chúng ta sẽ thêm hiệu ứng chuyển cảnh mờ dần để làm mờ 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ệ về không gian hoặc thứ bậc, nên chúng ta sẽ sử dụng hiệu ứng mờ xuyên suốt để thực hiện 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ã nào khác, 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 Trả lời. Các thao tác trên thanh ứng dụng dưới cùng sẽ thay đổi mà không có hiệu ứng chuyển đổi.
Trước

Tương tự như bước cuối cùng, chúng ta sẽ sử dụng lại _FadeThroughTransitionSwitcher. Để đạt được hiệu ứng chuyển đổi mong muốn, hãy chuyển đến định nghĩa lớp _BottomAppBarActionItems và bao bọc tiện ích trả về của hàm build() bằng một _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
...
Bây giờ hãy thử xem sao nhé! Khi bạn mở một email và được chuyển đến chế độ xem email, các thao tác trên thanh ứng dụng cũ ở dưới cùng sẽ mờ dần và thu nhỏ, trong khi các thao tác mới sẽ mờ dần và phóng to. Chính xác!
Sau

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

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 nguyên tắc và toàn bộ tài liệu 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ủa bạn!
Cảm ơn bạn đã dùng thử chuyển động theo ngôn ngữ thiết kế Material. Chúng tôi hy vọng bạn thích lớp học lập trình này!
Tôi đã hoàn thành lớp học lập trình này trong một khoảng thời gian hợp lý và không tốn nhiều công sức
Tôi muốn tiếp tục sử dụng hệ thống chuyển động của Material trong tương lai
Khám phá Flutter Gallery
| Để xem thêm các bản minh hoạ về cách sử dụng các tiện ích do thư viện Material Flutter cung cấp, cũng như khung Flutter, hãy nhớ truy cập vào Flutter Gallery. |



