1. บทนำ
Material Design คือระบบสำหรับการสร้างผลิตภัณฑ์ดิจิทัลที่โดดเด่นและสวยงาม การรวมสไตล์ การสร้างแบรนด์ การโต้ตอบ และการเคลื่อนไหวภายใต้ชุดหลักการและคอมโพเนนต์ที่สอดคล้องกันจะช่วยให้ทีมผลิตภัณฑ์ตระหนักถึงศักยภาพด้านการออกแบบที่ยิ่งใหญ่ที่สุด
| Material Components (MDC) ช่วยให้นักพัฒนาแอปใช้งาน Material Design ได้ MDC สร้างขึ้นโดยทีมวิศวกรและนักออกแบบ UX ของ Google โดยมีคอมโพเนนต์ UI ที่สวยงามและใช้งานได้หลายสิบรายการ และพร้อมใช้งานสำหรับ Android, iOS, เว็บ และ Flutter.material.io/develop |
ระบบการเคลื่อนไหวของ Material สำหรับ Flutter คืออะไร
ระบบการเคลื่อนไหวของ Material สำหรับ Flutter คือชุดรูปแบบการเปลี่ยนภาพภายในแพ็กเกจภาพเคลื่อนไหวที่ช่วยให้ผู้ใช้เข้าใจและไปยังส่วนต่างๆ ของแอปได้ ตามที่อธิบายไว้ในหลักเกณฑ์การออกแบบ Material
รูปแบบการเปลี่ยนภาพ Material หลัก 4 รูปแบบมีดังนี้
- เปลี่ยนรูปแบบคอนเทนเนอร์: การเปลี่ยนระหว่างองค์ประกอบ UI ที่มีคอนเทนเนอร์ สร้างการเชื่อมต่อที่มองเห็นได้ระหว่างองค์ประกอบ UI 2 รายการที่แตกต่างกันโดยการเปลี่ยนองค์ประกอบหนึ่งเป็นอีกองค์ประกอบหนึ่งอย่างราบรื่น

- แกนร่วม: การเปลี่ยนระหว่างองค์ประกอบ UI ที่มีความสัมพันธ์ในการนำทางหรือเชิงพื้นที่ ใช้การเปลี่ยนรูปแบบร่วมกันบนแกน X, Y หรือ Z เพื่อเสริมความสัมพันธ์ระหว่างองค์ประกอบต่างๆ

- จางผ่าน: การเปลี่ยนระหว่างองค์ประกอบ UI ที่ไม่มีความสัมพันธ์ที่มีอิทธิพลต่อกัน ใช้การจางออกและจางเข้าตามลำดับ โดยมีขนาดขององค์ประกอบที่เข้ามา

- จางลง: ใช้กับองค์ประกอบ UI ที่เข้าหรือออกภายในขอบเขตหน้าจอ

แพ็กเกจภาพเคลื่อนไหวมีวิดเจ็ตการเปลี่ยนสำหรับรูปแบบเหล่านี้ ซึ่งสร้างขึ้นจากทั้งไลบรารีภาพเคลื่อนไหวของ Flutter (flutter/animation.dart) และไลบรารี Material ของ Flutter (flutter/material.dart)
ใน Codelab นี้ คุณจะได้ใช้การเปลี่ยนฉากของ Material ที่สร้างขึ้นบนเฟรมเวิร์ก Flutter และไลบรารี Material ซึ่งหมายความว่าคุณจะได้จัดการกับวิดเจ็ต :)
สิ่งที่คุณจะสร้าง
โค้ดแล็บนี้จะแนะนำวิธีสร้างทรานซิชันบางอย่างลงในแอปอีเมล Flutter ตัวอย่างที่ชื่อ Reply โดยใช้ Dart เพื่อแสดงให้เห็นว่าคุณจะใช้ทรานซิชันจากแพ็กเกจภาพเคลื่อนไหวเพื่อปรับแต่งรูปลักษณ์และความรู้สึกของแอปได้อย่างไร
เราจะให้โค้ดเริ่มต้นสำหรับแอป Reply และคุณจะรวมการเปลี่ยนภาพของ Material ต่อไปนี้ไว้ในแอป ซึ่งจะเห็นได้ใน GIF ของ Codelab ที่เสร็จสมบูรณ์ด้านล่าง
- การเปลี่ยนการเปลี่ยนรูปแบบคอนเทนเนอร์จากรายชื่ออีเมลไปยังหน้ารายละเอียดอีเมล
- การเปลี่ยนจาก FAB ไปยังหน้าเขียนอีเมลด้วยการเปลี่ยนรูปแบบคอนเทนเนอร์
- การเปลี่ยนแกน Z ที่ใช้ร่วมกันจากไอคอนค้นหาไปยังหน้ามุมมองการค้นหา
- การเปลี่ยนภาพจางระหว่างหน้ากล่องจดหมาย
- การเปลี่ยนภาพจางผ่านระหว่าง FAB สำหรับเขียนและตอบ
- การเปลี่ยนภาพจางผ่านระหว่างชื่อกล่องจดหมายที่หายไป
- การเปลี่ยนจางผ่านระหว่างการกระทำในแถบแอปด้านล่าง

สิ่งที่คุณต้องมี
- ความรู้พื้นฐานเกี่ยวกับการพัฒนา Flutter และ Dart
- ตัวแก้ไขโค้ด
- โปรแกรมจำลองหรืออุปกรณ์ Android/iOS
- โค้ดตัวอย่าง (ดูขั้นตอนถัดไป)
คุณจะให้คะแนนระดับประสบการณ์ในการสร้างแอป Flutter เท่าใด
คุณต้องการเรียนรู้อะไรจากโค้ดแล็บนี้
2. ตั้งค่าสภาพแวดล้อมในการพัฒนา Flutter
คุณต้องมีซอฟต์แวร์ 2 อย่างเพื่อทำแล็บนี้ให้เสร็จสมบูรณ์ ได้แก่ Flutter SDK และโปรแกรมแก้ไข
คุณเรียกใช้ Codelab ได้โดยใช้อุปกรณ์ต่อไปนี้
- อุปกรณ์ Android หรือ iOS จริงที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาแอป
- โปรแกรมจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
- Android Emulator (ต้องตั้งค่าใน Android Studio)
- เบราว์เซอร์ (ต้องใช้ Chrome สำหรับการแก้ไขข้อบกพร่อง)
- ในรูปแบบแอปพลิเคชันเดสก์ท็อป Windows, Linux หรือ macOS คุณต้องพัฒนาบนแพลตฟอร์มที่วางแผนจะใช้งาน ดังนั้น หากต้องการพัฒนาแอปเดสก์ท็อป Windows คุณต้องพัฒนาบน Windows เพื่อเข้าถึงห่วงโซ่การสร้างที่เหมาะสม มีข้อกำหนดเฉพาะของระบบปฏิบัติการที่อธิบายไว้โดยละเอียดใน docs.flutter.dev/desktop
3. ดาวน์โหลดแอปเริ่มต้นของ Codelab
ตัวเลือกที่ 1: โคลนแอป Codelab เริ่มต้นจาก GitHub
หากต้องการโคลน codelab นี้จาก GitHub ให้เรียกใช้คำสั่งต่อไปนี้
git clone https://github.com/material-components/material-components-flutter-motion-codelab.git cd material-components-flutter-motion-codelab
ตัวเลือกที่ 2: ดาวน์โหลด ไฟล์ ZIP ของแอป Codelab สำหรับผู้เริ่มต้น
แอปเริ่มต้นอยู่ในไดเรกทอรี material-components-flutter-motion-codelab-starter
ยืนยันการขึ้นต่อกันของโปรเจ็กต์
โปรเจ็กต์นี้ขึ้นอยู่กับแพ็กเกจภาพเคลื่อนไหว ใน pubspec.yaml โปรดสังเกตว่าส่วน dependencies มีข้อมูลต่อไปนี้
animations: ^2.0.0
เปิดโปรเจ็กต์และเรียกใช้แอป
- เปิดโปรเจ็กต์ในโปรแกรมตัดต่อที่ต้องการ
- ทำตามวิธีการเพื่อ "เรียกใช้แอป" ในส่วนเริ่มต้นใช้งาน: ทดลองใช้สำหรับโปรแกรมแก้ไขที่คุณเลือก
สำเร็จ! โค้ดเริ่มต้นสำหรับหน้าแรกของ Reply ควรทำงานบนอุปกรณ์/โปรแกรมจำลอง คุณควรเห็นกล่องจดหมายที่มีรายการอีเมล

ไม่บังคับ: ลดความเร็วภาพเคลื่อนไหวของอุปกรณ์
เนื่องจาก Codelab นี้เกี่ยวข้องกับการเปลี่ยนฉากที่รวดเร็วแต่ราบรื่น การลดความเร็วของภาพเคลื่อนไหวของอุปกรณ์จึงอาจมีประโยชน์ในการสังเกตรายละเอียดที่ละเอียดยิ่งขึ้นของการเปลี่ยนฉากขณะที่คุณกำลังติดตั้งใช้งาน คุณทำได้โดยใช้การตั้งค่าในแอป ซึ่งเข้าถึงได้โดยแตะไอคอนการตั้งค่าเมื่อเปิดลิ้นชักด้านล่าง ไม่ต้องกังวล วิธีการลดความเร็วของภาพเคลื่อนไหวของอุปกรณ์นี้จะไม่ส่งผลต่อภาพเคลื่อนไหวในอุปกรณ์นอกแอป Reply

ไม่บังคับ: โหมดมืด
หากธีมสว่างของ Reply ทำให้คุณปวดตา ไม่ต้องกังวล เรามีการตั้งค่าในแอปที่ช่วยให้คุณเปลี่ยนธีมของแอปเป็นโหมดมืดเพื่อให้เหมาะกับสายตาของคุณมากขึ้น คุณเข้าถึงการตั้งค่านี้ได้โดยแตะไอคอนการตั้งค่าเมื่อเปิดลิ้นชักด้านล่าง

4. ทำความคุ้นเคยกับโค้ดแอปตัวอย่าง
มาดูโค้ดกัน เราได้จัดเตรียมแอปที่ใช้แพ็กเกจภาพเคลื่อนไหวเพื่อเปลี่ยนผ่านระหว่างหน้าจอต่างๆ ในแอปพลิเคชัน
- หน้าแรก: แสดงกล่องจดหมายที่เลือก
- InboxPage: แสดงรายการอีเมล
- MailPreviewCard: แสดงตัวอย่างอีเมล
- MailViewPage: แสดงอีเมลฉบับเดียวแบบเต็ม
- ComposePage: อนุญาตให้เขียนอีเมลใหม่
- SearchPage: แสดงมุมมองการค้นหา
router.dart
ก่อนอื่น ให้เปิด router.dart ในไดเรกทอรี lib เพื่อทำความเข้าใจวิธีตั้งค่าการนำทางรูทของแอป
class ReplyRouterDelegate extends RouterDelegate<ReplyRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<ReplyRoutePath> {
ReplyRouterDelegate({required this.replyState})
: navigatorKey = GlobalObjectKey<NavigatorState>(replyState) {
replyState.addListener(() {
notifyListeners();
});
}
@override
final GlobalKey<NavigatorState> navigatorKey;
RouterProvider replyState;
@override
void dispose() {
replyState.removeListener(notifyListeners);
super.dispose();
}
@override
ReplyRoutePath get currentConfiguration => replyState.routePath!;
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<RouterProvider>.value(value: replyState),
],
child: Selector<RouterProvider, ReplyRoutePath?>(
selector: (context, routerProvider) => routerProvider.routePath,
builder: (context, routePath, child) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Shared Z-Axis transition from search icon to search view page (Motion)
const CustomTransitionPage(
transitionKey: ValueKey('Home'),
screen: HomePage(),
),
if (routePath is ReplySearchPath)
const CustomTransitionPage(
transitionKey: ValueKey('Search'),
screen: SearchPage(),
),
],
);
},
),
);
}
bool _handlePopPage(Route<dynamic> route, dynamic result) {
// _handlePopPage should not be called on the home page because the
// PopNavigatorRouterDelegateMixin will bubble up the pop to the
// SystemNavigator if there is only one route in the navigator.
assert(route.willHandlePopInternally ||
replyState.routePath is ReplySearchPath);
final bool didPop = route.didPop(result);
if (didPop) replyState.routePath = const ReplyHomePath();
return didPop;
}
@override
Future<void> setNewRoutePath(ReplyRoutePath configuration) {
replyState.routePath = configuration;
return SynchronousFuture<void>(null);
}
}
นี่คือตัวนำทางรูทของเรา ซึ่งจะจัดการหน้าจอของแอปที่ใช้พื้นที่ทั้ง Canvas เช่น HomePage และ SearchPage โดยจะคอยฟังสถานะของแอปเพื่อตรวจสอบว่าเราได้ตั้งค่าเส้นทางไปยัง ReplySearchPath หรือไม่ หากมี ระบบจะสร้าง Navigator ใหม่โดยมี SearchPage อยู่ที่ด้านบนของสแต็ก โปรดสังเกตว่าหน้าจอของเราอยู่ใน CustomTransitionPage โดยไม่มีการกำหนดการเปลี่ยน ซึ่งแสดงวิธีหนึ่งในการไปยังส่วนต่างๆ ของหน้าจอโดยไม่มีการเปลี่ยนฉากที่กำหนดเอง
home.dart
เราตั้งค่าเส้นทางไปที่ ReplySearchPath ในสถานะของแอปโดยทำดังนี้ภายใน _BottomAppBarActionItems ใน 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();
},
),
);
ในonPressedพารามิเตอร์ เราจะเข้าถึง RouterProvider และตั้งค่า routePath เป็น ReplySearchPath RouterProvider จะติดตามสถานะของเครื่องมือช่วยนำทางหลัก
mail_view_router.dart
ตอนนี้มาดูวิธีตั้งค่าการนำทางภายในแอปกัน เปิด mail_view_router.dart ในไดเรกทอรี lib คุณจะเห็นเครื่องมือสำรวจที่คล้ายกับด้านบน
class MailViewRouterDelegate extends RouterDelegate<void>
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
MailViewRouterDelegate({required this.drawerController});
final AnimationController drawerController;
@override
Widget build(BuildContext context) {
bool _handlePopPage(Route<dynamic> route, dynamic result) {
return false;
}
return Selector<EmailStore, String>(
selector: (context, emailStore) => emailStore.currentlySelectedInbox,
builder: (context, currentlySelectedInbox, child) {
return Navigator(
key: navigatorKey,
onPopPage: _handlePopPage,
pages: [
// TODO: Add Fade through transition between mailbox pages (Motion)
CustomTransitionPage(
transitionKey: ValueKey(currentlySelectedInbox),
screen: InboxPage(
destination: currentlySelectedInbox,
),
)
],
);
},
);
}
...
}
นี่คือเข็มทิศภายในของเรา โดยจะจัดการหน้าจอด้านในของแอปที่ใช้เฉพาะส่วนเนื้อหาของ Canvas เช่น InboxPage InboxPage จะแสดงรายการอีเมลตามกล่องจดหมายปัจจุบันในสถานะของแอป ระบบจะสร้าง Navigator ใหม่โดยมี InboxPage ที่ถูกต้องอยู่ด้านบนของสแต็กทุกครั้งที่มีการเปลี่ยนแปลงพร็อพเพอร์ตี้ currentlySelectedInbox ของสถานะแอป
home.dart
เราตั้งค่ากล่องจดหมายปัจจุบันในสถานะของแอปโดยทำดังนี้ภายใน _HomePageState ใน home.dart
void _onDestinationSelected(String destination) {
var emailStore = Provider.of<EmailStore>(
context,
listen: false,
);
if (emailStore.onMailView) {
emailStore.currentlySelectedEmailId = -1;
}
if (emailStore.currentlySelectedInbox != destination) {
emailStore.currentlySelectedInbox = destination;
}
setState(() {});
}
ในฟังก์ชัน _onDestinationSelected เราจะเข้าถึง EmailStore และตั้งค่า currentlySelectedInbox เป็นปลายทางที่เลือก EmailStoreจะติดตามสถานะของเนวิเกเตอร์ภายใน
home.dart
สุดท้ายนี้ หากต้องการดูตัวอย่างการกำหนดเส้นทางการนำทาง ให้เปิด home.dart ในไดเรกทอรี lib ค้นหาคลาส _ReplyFabState ภายในพร็อพเพอร์ตี้ onTap ของวิดเจ็ต InkWell ซึ่งควรมีลักษณะดังนี้
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = true;
Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const ComposePage();
},
),
);
},
ซึ่งแสดงวิธีไปยังหน้าเขียนอีเมลโดยไม่มีการเปลี่ยนฉากที่กำหนดเอง ใน Codelab นี้ คุณจะได้เจาะลึกโค้ดของ Reply เพื่อตั้งค่าการเปลี่ยนภาพ Material ที่ทำงานร่วมกับ Actions การนำทางต่างๆ ทั่วทั้งแอป
เมื่อคุ้นเคยกับโค้ดเริ่มต้นแล้ว มาใช้การเปลี่ยนฉากแรกกัน
5. เพิ่มการเปลี่ยน Container Transform จากรายการอีเมลไปยังหน้ารายละเอียดอีเมล
โดยเริ่มจากการเพิ่มทรานซิชันเมื่อคลิกอีเมล สำหรับการเปลี่ยนแปลงการนำทางนี้ รูปแบบการเปลี่ยนรูปแบบคอนเทนเนอร์เหมาะอย่างยิ่ง เนื่องจากออกแบบมาสำหรับการเปลี่ยนระหว่างองค์ประกอบ UI ที่มีคอนเทนเนอร์ รูปแบบนี้สร้างการเชื่อมต่อที่มองเห็นได้ระหว่างองค์ประกอบ UI 2 รายการ
ก่อนเพิ่มโค้ดใดๆ ให้ลองเรียกใช้แอป Reply แล้วคลิกอีเมล โดยควรตัดต่อแบบจัมป์คัตอย่างง่าย ซึ่งหมายความว่าหน้าจอจะถูกแทนที่โดยไม่มีการเปลี่ยนฉาก
ก่อน

เริ่มต้นด้วยการเพิ่มการนำเข้าแพ็กเกจภาพเคลื่อนไหวที่ด้านบนของ mail_card_preview.dart ดังที่แสดงในข้อมูลโค้ดต่อไปนี้
mail_card_preview.dart
import 'package:animations/animations.dart';
ตอนนี้คุณมีแพ็กเกจการนำเข้าสำหรับภาพเคลื่อนไหวแล้ว เราจึงเริ่มเพิ่มการเปลี่ยนฉากที่สวยงามลงในแอปได้ มาเริ่มด้วยการสร้างคลาส StatelessWidget ที่จะเก็บวิดเจ็ต OpenContainer ของเรากัน
ใน mail_card_preview.dart ให้เพิ่มข้อมูลโค้ดต่อไปนี้หลังคำจำกัดความคลาสของ MailPreviewCard
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
class _OpenContainerWrapper extends StatelessWidget {
const _OpenContainerWrapper({
required this.id,
required this.email,
required this.closedChild,
});
final int id;
final Email email;
final Widget closedChild;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return OpenContainer(
openBuilder: (context, closedContainer) {
return MailViewPage(id: id, email: email);
},
openColor: theme.cardColor,
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(0)),
),
closedElevation: 0,
closedColor: theme.cardColor,
closedBuilder: (context, openContainer) {
return InkWell(
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).currentlySelectedEmailId = id;
openContainer();
},
child: closedChild,
);
},
);
}
}
ตอนนี้เรามาใช้ Wrapper ใหม่กัน ภายในคำจำกัดความของคลาส MailPreviewCard เราจะรวมวิดเจ็ต Material จากฟังก์ชัน build() ไว้กับ _OpenContainerWrapper ใหม่
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
id: id,
email: email,
closedChild: Material(
...
_OpenContainerWrapper มีวิดเจ็ต InkWell และพร็อพเพอร์ตี้สีของ OpenContainer จะกำหนดสีของคอนเทนเนอร์ที่ล้อมรอบ ดังนั้นเราจึงนำวิดเจ็ต Material และ Inkwell ออกได้ โค้ดที่ได้จะมีลักษณะดังนี้
mail_card_preview.dart
// TODO: Add Container Transform transition from email list to email detail page (Motion)
return _OpenContainerWrapper(
id: id,
email: email,
closedChild: Dismissible(
key: ObjectKey(email),
dismissThresholds: const {
DismissDirection.startToEnd: 0.8,
DismissDirection.endToStart: 0.4,
},
onDismissed: (direction) {
switch (direction) {
case DismissDirection.endToStart:
if (onStarredInbox) {
onStar();
}
break;
case DismissDirection.startToEnd:
onDelete();
break;
default:
}
},
background: _DismissibleContainer(
icon: 'twotone_delete',
backgroundColor: colorScheme.primary,
iconColor: ReplyColors.blue50,
alignment: Alignment.centerLeft,
padding: const EdgeInsetsDirectional.only(start: 20),
),
confirmDismiss: (direction) async {
if (direction == DismissDirection.endToStart) {
if (onStarredInbox) {
return true;
}
onStar();
return false;
} else {
return true;
}
},
secondaryBackground: _DismissibleContainer(
icon: 'twotone_star',
backgroundColor: currentEmailStarred
? colorScheme.secondary
: theme.scaffoldBackgroundColor,
iconColor: currentEmailStarred
? colorScheme.onSecondary
: colorScheme.onBackground,
alignment: Alignment.centerRight,
padding: const EdgeInsetsDirectional.only(end: 20),
),
child: mailPreview,
),
);
ในขั้นตอนนี้ คุณควรมีการเปลี่ยนรูปแบบคอนเทนเนอร์ที่ใช้งานได้อย่างสมบูรณ์ การคลิกอีเมลจะขยายรายการในลิสต์เป็นหน้าจอรายละเอียดพร้อมกับย่อรายการอีเมล การกดปุ่มย้อนกลับจะยุบหน้าจอรายละเอียดอีเมลกลับไปเป็นรายการในขณะที่ขยายในรายการอีเมล
หลัง

6. เพิ่มการเปลี่ยนฉาก Container Transform จาก FAB ไปยังหน้าเขียนอีเมล
มาต่อกันที่ Container Transform และเพิ่มการเปลี่ยนจากปุ่มการทำงานแบบลอยไปเป็น ComposePage การขยาย FAB เป็นอีเมลใหม่ที่ผู้ใช้จะเขียน ก่อนอื่น ให้เรียกใช้แอปอีกครั้งแล้วคลิก FAB เพื่อดูว่าไม่มีการเปลี่ยนฉากเมื่อเปิดหน้าจอเขียนอีเมล
ก่อน

วิธีที่เรากำหนดค่าการเปลี่ยนผ่านนี้จะคล้ายกับวิธีที่เราทำในขั้นตอนสุดท้ายมาก เนื่องจากเราใช้คลาสวิดเจ็ตเดียวกันคือ OpenContainer
ใน home.dart ให้นำเข้า package:animations/animations.dart ที่ด้านบนของไฟล์ แล้วแก้ไขเมธอด _ReplyFabState build() มาห่อหุ้มวิดเจ็ต Material ที่ส่งคืนด้วยวิดเจ็ต 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,
...
นอกจากพารามิเตอร์ที่ใช้ในการกำหนดค่าวิดเจ็ต OpenContainer ก่อนหน้าแล้ว ตอนนี้ระบบยังตั้งค่า onClosed ด้วย onClosed คือ ClosedCallback ที่เรียกใช้เมื่อเส้นทาง OpenContainer ถูกป๊อปหรือกลับสู่สถานะปิด ค่าส่งคืนของธุรกรรมนั้นจะส่งผ่านไปยังฟังก์ชันนี้เป็นอาร์กิวเมนต์ เราใช้ Callback นี้เพื่อแจ้งให้ผู้ให้บริการแอปของเราทราบว่าเราได้ออกจากเส้นทาง ComposePage แล้ว เพื่อให้ผู้ให้บริการสามารถแจ้งเตือนผู้ฟังทั้งหมดได้
เช่นเดียวกับขั้นตอนสุดท้าย เราจะนำวิดเจ็ต Material ออกจากวิดเจ็ตของเรา เนื่องจากวิดเจ็ต OpenContainer จะจัดการสีของวิดเจ็ตที่ closedBuilder ส่งคืนพร้อมกับ closedColor นอกจากนี้ เราจะนำNavigator.push()การเรียกใช้ภายใน onTap ของวิดเจ็ต InkWell ออก และแทนที่ด้วย openContainer() Callback ที่ได้รับจาก closedBuilder ของวิดเจ็ต OpenContainer เนื่องจากตอนนี้วิดเจ็ต 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 Tooltip(
message: tooltip,
child: InkWell(
customBorder: circleFabBorder,
onTap: () {
Provider.of<EmailStore>(
context,
listen: false,
).onCompose = true;
openContainer();
},
child: SizedBox(
height: _mobileFabDimension,
width: _mobileFabDimension,
child: Center(
child: fabSwitcher,
),
),
),
);
},
);
ตอนนี้เราจะล้างโค้ดเก่าๆ กัน เนื่องจากวิดเจ็ต OpenContainer ของเราจัดการการแจ้งเตือนผู้ให้บริการแอปว่าเราไม่ได้อยู่ใน ComposePage ผ่าน onClosed ClosedCallback แล้ว เราจึงนำการติดตั้งใช้งานก่อนหน้าใน mail_view_router.dart ออกได้
mail_view_router.dart
// TODO: Add Container Transform from FAB to compose email page (Motion)
emailStore.onCompose = false; /// delete this line
return SynchronousFuture<bool>(true);
เพียงเท่านี้ก็เสร็จสิ้นขั้นตอนนี้แล้ว คุณควรมีการเปลี่ยนจาก FAB ไปยังหน้าจอเขียนที่มีลักษณะดังนี้
หลัง

7. เพิ่มการเปลี่ยนฉากแกน Z ที่ใช้ร่วมกันจากไอคอนค้นหาไปยังหน้ามุมมองการค้นหา
ในขั้นตอนนี้ เราจะเพิ่มทรานซิชันจากไอคอนค้นหาไปยังมุมมองการค้นหาแบบเต็มหน้าจอ เนื่องจากไม่มีคอนเทนเนอร์แบบถาวรที่เกี่ยวข้องกับการเปลี่ยนแปลงการนำทางนี้ เราจึงใช้การเปลี่ยนฉากแกน Z ที่ใช้ร่วมกันเพื่อเสริมความสัมพันธ์เชิงพื้นที่ระหว่าง 2 หน้าจอและระบุการเลื่อนขึ้น 1 ระดับในลำดับชั้นของแอป
ก่อนเพิ่มโค้ด ให้ลองเรียกใช้แอปแล้วแตะไอคอนค้นหาที่มุมขวาล่างของหน้าจอ ซึ่งควรจะแสดงหน้าจอมุมมองการค้นหาโดยไม่มีการเปลี่ยน
ก่อน

ก่อนอื่นให้ไปที่router.dartไฟล์ หลังจากReplySearchPathคำจำกัดความของคลาส ให้เพิ่มข้อมูลโค้ดต่อไปนี้
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;
});
}
}
ตอนนี้เรามาใช้ SharedAxisTransitionPageWrapper ใหม่เพื่อให้การเปลี่ยนผ่านเป็นไปตามที่เราต้องการกัน ในReplyRouterDelegateคำจำกัดความของคลาส ภายในพร็อพเพอร์ตี้ pages ให้เราครอบหน้าจอการค้นหาด้วย SharedAxisTransitionPageWrapper แทน 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(),
),
],
);
ตอนนี้ลองเรียกใช้แอปอีกครั้ง

ทุกอย่างเริ่มดูดีขึ้นแล้ว เมื่อคลิกไอคอนค้นหาในแถบแอปด้านล่าง การเปลี่ยนแกนที่แชร์จะปรับขนาดหน้าค้นหาให้แสดง อย่างไรก็ตาม โปรดสังเกตว่าหน้าแรกจะไม่ขยายออก แต่จะคงที่ในขณะที่หน้าค้นหาขยายเข้ามาทับ นอกจากนี้ เมื่อกดปุ่มย้อนกลับ หน้าแรกจะไม่ปรับขนาดให้แสดง แต่จะยังคงอยู่กับที่ในขณะที่หน้าค้นหาปรับขนาดออกจากการแสดง ดังนั้นเราจึงยังไม่หยุดเพียงเท่านี้
มาแก้ไขทั้ง 2 ปัญหาโดยการห่อหุ้ม HomePage ด้วย SharedAxisTransitionWrapper แทน 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(),
),
],
);
เท่านี้ก็เรียบร้อย ตอนนี้ให้ลองเรียกใช้แอปอีกครั้งแล้วแตะไอคอนค้นหา หน้าจอของมุมมองหน้าแรกและมุมมองการค้นหาควรจางลงและปรับขนาดพร้อมกันตามแกน Z ในเชิงลึก เพื่อสร้างเอฟเฟกต์ที่ราบรื่นระหว่าง 2 หน้าจอ
หลัง

8. เพิ่มการเปลี่ยนภาพแบบจางผ่านระหว่างหน้ากล่องจดหมาย
ในขั้นตอนนี้ เราจะเพิ่มทรานซิชันระหว่างกล่องจดหมายต่างๆ เนื่องจากเราไม่ต้องการเน้นความสัมพันธ์เชิงพื้นที่หรือลำดับชั้น เราจึงจะใช้การจางผ่านเพื่อ "สลับ" ระหว่างรายการอีเมลอย่างง่ายๆ
ก่อนที่จะเพิ่มโค้ดเพิ่มเติม ให้ลองเรียกใช้แอป แตะโลโก้ตอบกลับในแถบแอปด้านล่าง และสลับกล่องจดหมาย รายการอีเมลควรเปลี่ยนโดยไม่มีการเปลี่ยนผ่าน
ก่อน

ก่อนอื่นให้ไปที่mail_view_router.dartไฟล์ หลังจากMailViewRouterDelegateคำจำกัดความของคลาส ให้เพิ่มข้อมูลโค้ดต่อไปนี้
mail_view_router.dart
// TODO: Add Fade through transition between mailbox pages (Motion)
class FadeThroughTransitionPageWrapper extends Page {
const FadeThroughTransitionPageWrapper({
required this.mailbox,
required this.transitionKey,
}) : super(key: transitionKey);
final Widget mailbox;
final ValueKey transitionKey;
@override
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
fillColor: Theme.of(context).scaffoldBackgroundColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
pageBuilder: (context, animation, secondaryAnimation) {
return mailbox;
});
}
}
เช่นเดียวกับขั้นตอนสุดท้าย ให้ใช้ FadeThroughTransitionPageWrapper ใหม่เพื่อทำการเปลี่ยนผ่านที่เราต้องการ ในMailViewRouterDelegateคำจำกัดความของคลาส ภายในพร็อพเพอร์ตี้ pages ให้ใช้ FadeThroughTransitionPageWrapper แทนการห่อหุ้มหน้าจอกล่องจดหมายด้วย CustomTransitionPage ดังนี้
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),
),
],
);
เรียกใช้แอปอีกครั้ง เมื่อคุณเปิดลิ้นชักการนำทางด้านล่างและเปลี่ยนกล่องจดหมาย รายการอีเมลปัจจุบันควรจางลงและขยายออก ขณะที่รายการใหม่จางลงและขยายเข้า เยี่ยมไปเลย
หลัง

9. เพิ่มทรานซิชันเฟดผ่านระหว่าง FAB สำหรับเขียนและตอบ
ในขั้นตอนนี้ เราจะเพิ่มการเปลี่ยนระหว่างไอคอน FAB ต่างๆ เนื่องจากเราไม่ต้องการเน้นความสัมพันธ์เชิงพื้นที่หรือลำดับชั้น เราจึงจะใช้การจางผ่านเพื่อ "สลับ" ไอคอนใน FAB อย่างง่าย
ก่อนเพิ่มโค้ดใดๆ ให้ลองเรียกใช้แอป แตะอีเมล และเปิดมุมมองอีเมล ไอคอน FAB ควรเปลี่ยนโดยไม่มีการเปลี่ยนฉาก
ก่อน

เราจะทำงานใน home.dart ในช่วงที่เหลือของโค้ดแล็บ ดังนั้นไม่ต้องกังวลเรื่องการเพิ่มการนำเข้าสำหรับแพ็กเกจภาพเคลื่อนไหว เนื่องจากเราได้ดำเนินการสำหรับ home.dart ไปแล้วในขั้นตอนที่ 2
วิธีที่เรากำหนดค่าการเปลี่ยนฉาก 2-3 รายการถัดไปจะคล้ายกันมาก เนื่องจากทั้งหมดจะใช้คลาสที่นำกลับมาใช้ใหม่ได้ _FadeThroughTransitionSwitcher
ใน home.dart ให้เพิ่มข้อมูลโค้ดต่อไปนี้ภายใต้ _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,
);
}
}
ตอนนี้ใน _ReplyFabState ให้มองหาวิดเจ็ต fabSwitcher fabSwitcher จะแสดงไอคอนที่แตกต่างกันโดยขึ้นอยู่กับว่าอยู่ในมุมมองอีเมลหรือไม่ มาสรุปด้วย_FadeThroughTransitionSwitcherกัน
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,
),
);
...
เราให้ _FadeThroughTransitionSwitcher มีfillColorที่โปร่งใส จึงไม่มีพื้นหลังระหว่างองค์ประกอบเมื่อเปลี่ยนผ่าน นอกจากนี้ เรายังสร้าง UniqueKey และกำหนดให้กับไอคอนรายการใดรายการหนึ่งด้วย
ตอนนี้ในขั้นตอนนี้ คุณควรมี FAB ตามบริบทที่เคลื่อนไหวได้อย่างเต็มรูปแบบ การเข้าสู่มุมมองอีเมลจะทำให้ไอคอน FAB เก่าจางลงและขยายออก ในขณะที่ไอคอนใหม่จะจางลงและขยายเข้า
หลัง

10. เพิ่มการเปลี่ยนภาพแบบจางผ่านระหว่างชื่อกล่องจดหมายที่หายไป
ในขั้นตอนนี้ เราจะเพิ่มการเปลี่ยนฉากแบบจางผ่าน เพื่อให้ชื่อกล่องจดหมายจางผ่านระหว่างสถานะที่มองเห็นได้และมองไม่เห็นเมื่ออยู่ในมุมมองอีเมล เนื่องจากเราไม่ต้องการเน้นความสัมพันธ์เชิงพื้นที่หรือลำดับชั้น เราจึงจะใช้การจางผ่านเพื่อ "สลับ" อย่างง่ายๆ ระหว่างTextวิดเจ็ตที่ครอบคลุมชื่อกล่องจดหมายกับSizedBoxที่ว่างเปล่า
ก่อนเพิ่มโค้ดใดๆ ให้ลองเรียกใช้แอป แตะอีเมล และเปิดมุมมองอีเมล ชื่อกล่องจดหมายควรหายไปโดยไม่มีการเปลี่ยนผ่าน
ก่อน

ส่วนที่เหลือของโค้ดแล็บนี้จะใช้เวลาไม่นานเนื่องจากเราได้ดำเนินการส่วนใหญ่ใน _FadeThroughTransitionSwitcher ในขั้นตอนสุดท้ายแล้ว
ตอนนี้เรามาที่_AnimatedBottomAppBarชั้นเรียนใน home.dart เพื่อเพิ่มการเปลี่ยนฉากกัน เราจะใช้ _FadeThroughTransitionSwitcher จากขั้นตอนสุดท้ายอีกครั้ง และครอบ onMailView แบบมีเงื่อนไข ซึ่งจะแสดงผลเป็น SizedBox ว่างเปล่า หรือชื่อกล่องจดหมายที่ค่อยๆ ปรากฏขึ้นพร้อมกับลิ้นชักด้านล่าง
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,
),
);
},
),
),
),
เรียบร้อย เราดำเนินการขั้นตอนนี้เสร็จแล้ว
เรียกใช้แอปอีกครั้ง เมื่อเปิดอีเมลและระบบนำคุณไปยังมุมมองอีเมล ชื่อกล่องจดหมายในแถบแอปด้านล่างควรจะจางลงและขยายออก ยอดเยี่ยม!
หลัง

11. เพิ่มการเปลี่ยนผ่านแบบจางผ่านระหว่างการดำเนินการในแถบแอปด้านล่าง
ในขั้นตอนนี้ เราจะเพิ่มการเปลี่ยนฉากแบบจางผ่าน เพื่อให้การดำเนินการในแถบแอปด้านล่างจางผ่านตามบริบทของแอปพลิเคชัน เนื่องจากเราไม่ต้องการเน้นความสัมพันธ์เชิงพื้นที่หรือลำดับชั้น เราจึงจะใช้การจางผ่านเพื่อ "สลับ" การทำงานของแถบแอปด้านล่างอย่างง่ายเมื่อแอปอยู่ในหน้าแรก เมื่อลิ้นชักด้านล่างปรากฏขึ้น และเมื่อเราอยู่ในมุมมองอีเมล
ก่อนเพิ่มโค้ดใดๆ ให้ลองเรียกใช้แอป แตะอีเมล และเปิดมุมมองอีเมล คุณยังลองแตะโลโก้ตอบกลับได้ด้วย การดำเนินการในแถบแอปด้านล่างควรเปลี่ยนโดยไม่มีการเปลี่ยนผ่าน
ก่อน

เช่นเดียวกับขั้นตอนสุดท้าย เราจะใช้ _FadeThroughTransitionSwitcher อีกครั้ง หากต้องการเปลี่ยนภาพตามที่ต้องการ ให้ไปที่_BottomAppBarActionItemsคำจำกัดความของคลาสและห่อวิดเจ็ตที่ฟังก์ชัน build() แสดงผลด้วย _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
...
มาลองกันเลย เมื่อเปิดอีเมลและระบบนำคุณไปยังมุมมองอีเมล การดำเนินการในแถบแอปด้านล่างแบบเดิมควรจางลงและขยายออก ขณะที่การดำเนินการใหม่ควรจางลงและขยายเข้า เยี่ยมมาก!
หลัง

12. ยินดีด้วย
แพ็กเกจภาพเคลื่อนไหวช่วยให้คุณสร้างทรานซิชันที่สวยงามในแอปที่มีอยู่ซึ่งเป็นไปตามหลักเกณฑ์การออกแบบ Material รวมถึงมีรูปลักษณ์และลักษณะการทำงานที่สอดคล้องกันในอุปกรณ์ทุกเครื่องได้โดยใช้โค้ด Dart น้อยกว่า 100 บรรทัด

ขั้นตอนถัดไป
ดูข้อมูลเพิ่มเติมเกี่ยวกับระบบการเคลื่อนไหวของ Material ได้ที่หลักเกณฑ์และเอกสารประกอบสำหรับนักพัฒนาซอฟต์แวร์ฉบับเต็ม แล้วลองเพิ่มทรานซิชันของ Material ลงในแอป
ขอขอบคุณที่ลองใช้การเคลื่อนไหวของ Material เราหวังว่าคุณจะชอบ Codelab นี้
ฉันทำ Codelab นี้เสร็จได้โดยใช้เวลาและความพยายามที่สมเหตุสมผล
ฉันต้องการใช้ระบบการเคลื่อนไหวของ Material ต่อไปในอนาคต
ดู Flutter Gallery
| ดูการสาธิตเพิ่มเติมเกี่ยวกับวิธีใช้วิดเจ็ตที่จัดทำโดยไลบรารี Material Flutter รวมถึงเฟรมเวิร์ก Flutter ได้ที่ Flutter Gallery |



