1. บทนำ
Material Design เป็นระบบสำหรับสร้างผลิตภัณฑ์ดิจิทัลที่โดดเด่นและสวยงาม โดยการรวมสไตล์ การสร้างแบรนด์ การโต้ตอบ และการเคลื่อนไหวเข้าด้วยกันภายใต้ชุดหลักการและองค์ประกอบที่สอดคล้องกัน ทำให้ทีมผลิตภัณฑ์ตระหนักถึงศักยภาพด้านการออกแบบที่ดีที่สุดของตนเอง
Material Components (MDC) ช่วยให้นักพัฒนานำดีไซน์ Material มาใช้ 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
) และไลบรารีวัสดุ Flutter (flutter/material.dart
)
ใน Codelab นี้ คุณจะได้ใช้ทรานซิชันของ Material ที่สร้างจากเฟรมเวิร์ก Flutter และไลบรารี Material ซึ่งหมายความว่าคุณต้องจัดการกับวิดเจ็ต :)
สิ่งที่คุณจะสร้าง
โค้ดแล็บนี้จะแนะนำการสร้างทรานซิชันบางอย่างในแอปอีเมล Flutter ตัวอย่างชื่อ Reply โดยใช้ Dart เพื่อสาธิตวิธีใช้ทรานซิชันจากแพ็กเกจภาพเคลื่อนไหวเพื่อปรับแต่งรูปลักษณ์ของแอป
โค้ดเริ่มต้นสำหรับแอปตอบกลับจะมีให้ และคุณจะต้องใส่การเปลี่ยนของ Material ต่อไปนี้ลงในแอป ซึ่งสามารถดูได้ใน GIF ของ Codelab ที่สมบูรณ์ด้านล่างนี้
- การเปลี่ยนจากการเปลี่ยนรูปแบบคอนเทนเนอร์จากรายชื่ออีเมลเป็นหน้ารายละเอียดอีเมล
- การเปลี่ยนการแปลงคอนเทนเนอร์จาก FAB เป็นหน้าเขียนอีเมล
- แกน Z ที่ใช้ร่วมกันเปลี่ยนจากไอคอนการค้นหาเป็นหน้ามุมมองการค้นหา
- การเปลี่ยนผ่าน Fade Through ระหว่างหน้ากล่องจดหมาย
- การเปลี่ยนจางลงระหว่าง FAB เขียนและตอบ
- การเปลี่ยนจางลงระหว่างชื่อกล่องจดหมายที่หายไป
- การเปลี่ยนเลือนระหว่างการดําเนินการของแถบแอปด้านล่าง
สิ่งที่ต้องมี
- ความรู้พื้นฐานเกี่ยวกับการพัฒนา Flutter และ Dart
- ตัวแก้ไขโค้ด
- โปรแกรมจำลองหรืออุปกรณ์ Android/iOS
- โค้ดตัวอย่าง (ดูขั้นตอนถัดไป)
คุณจะให้คะแนนระดับประสบการณ์ในการสร้างแอป Flutter เท่าใด
คุณต้องการเรียนรู้อะไรจาก Codelab นี้
2. ตั้งค่าสภาพแวดล้อมการพัฒนา Flutter
ห้องทดลองนี้ต้องมีซอฟต์แวร์ 2 ประเภท ได้แก่ Flutter SDK และเครื่องมือแก้ไข
คุณเรียกใช้โค้ดแล็บได้โดยใช้อุปกรณ์ต่อไปนี้
- อุปกรณ์ Android หรือ iOS จริงที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาแอป
- เครื่องมือจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
- โปรแกรมจำลอง Android (ต้องมีการตั้งค่าใน 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
เปิดโปรเจ็กต์และเรียกใช้แอป
- เปิดโปรเจ็กต์ในเครื่องมือแก้ไขที่ต้องการ
- ทำตามวิธีการ "เรียกใช้แอป" ในเริ่มต้นใช้งาน: ลองใช้สำหรับโปรแกรมตัดต่อที่คุณเลือก
สำเร็จ! รหัสเริ่มต้นสำหรับหน้าแรกของการตอบควรทำงานในอุปกรณ์/โปรแกรมจำลอง คุณควรเห็นกล่องจดหมายที่มีรายการอีเมล
ไม่บังคับ: ชะลอภาพเคลื่อนไหวของอุปกรณ์
เนื่องจากโค้ดแล็บนี้เกี่ยวข้องกับการเปลี่ยนภาพที่รวดเร็วแต่ดูดี จึงอาจมีประโยชน์ในการทำให้ภาพเคลื่อนไหวของอุปกรณ์ช้าลงเพื่อสังเกตรายละเอียดเล็กๆ น้อยๆ ของการเปลี่ยนภาพขณะที่คุณใช้งาน ซึ่งทำได้ผ่านการตั้งค่าในแอป ซึ่งเข้าถึงได้ด้วยการแตะไอคอนการตั้งค่าเมื่อลิ้นชักด้านล่างเปิดอยู่ ไม่ต้องกังวล การทำให้ภาพเคลื่อนไหวในอุปกรณ์ช้าลงนี้จะไม่ส่งผลต่อภาพเคลื่อนไหวบนอุปกรณ์ที่อยู่นอกแอป "ตอบกลับ"
ไม่บังคับ: โหมดมืด
ถ้าการตอบกลับเป็นธีมที่สดใสทำให้สายตาของคุณรำคาญ ก็ไม่ต้องมองอีกต่อไป การตั้งค่าในแอปช่วยให้คุณเปลี่ยนธีมของแอปเป็นโหมดมืดเพื่อให้สบายตายิ่งขึ้นได้ การตั้งค่านี้เข้าถึงได้โดยแตะไอคอนการตั้งค่าเมื่อลิ้นชักด้านล่างเปิดอยู่
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);
}
}
นี่เป็นโปรแกรมนำทางระดับรูท ซึ่งจัดการหน้าจอของแอปที่ใช้ทั้งผืนผ้าใบ เช่น HomePage
และ SearchPage
แอปจะฟังสถานะของแอปเพื่อตรวจสอบว่าเราได้กำหนดเส้นทางไปยัง ReplySearchPath
หรือไม่ หากมี เครื่องมือจะสร้างตัวนำทางของเราอีกครั้งโดยมี 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
จะแสดงรายการอีเมลโดยขึ้นอยู่กับกล่องจดหมายปัจจุบันในสถานะของแอป ตัวนำทางจะสร้างขึ้นใหม่โดยมี 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 นี้ คุณจะเจาะลึกไปที่โค้ดของการตอบเพื่อตั้งค่าการเปลี่ยนของ Material ที่ทำงานควบคู่กับการนำทางต่างๆ ในแอป
เมื่อคุณคุ้นเคยกับรหัสเริ่มต้นแล้ว เรามาเริ่มการเปลี่ยนผ่านแรกกัน
5. เพิ่มการเปลี่ยนคอนเทนเนอร์จากรายชื่ออีเมลไปยังหน้ารายละเอียดอีเมล
ในการเริ่มต้น คุณจะต้องเพิ่มทรานซิชันเมื่อคลิกอีเมล สำหรับการเปลี่ยนแปลงการนำทางนี้ รูปแบบการเปลี่ยนรูปแบบคอนเทนเนอร์นี้เหมาะสมอย่างยิ่ง เนื่องจากออกแบบมาสำหรับการเปลี่ยนระหว่างองค์ประกอบ UI ที่มีคอนเทนเนอร์ รูปแบบนี้สร้างการเชื่อมต่อที่มองเห็นได้ระหว่างองค์ประกอบ UI 2 รายการ
ก่อนเพิ่มโค้ดใดๆ ให้ลองเรียกใช้แอปตอบกลับและคลิกอีเมล โดยควรใช้การตัดต่อแบบกระโดด ซึ่งหมายความว่าระบบจะแทนที่หน้าจอโดยไม่มีทรานซิชัน
ก่อน
เริ่มต้นโดยเพิ่มการนําเข้าสำหรับแพ็กเกจภาพเคลื่อนไหวที่ด้านบนของ 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. เพิ่มการเปลี่ยนรูปแบบคอนเทนเนอร์จาก FAB ไปยังหน้าเขียนอีเมล
มาเปลี่ยนรูปแบบคอนเทนเนอร์ต่อและเพิ่มการเปลี่ยนจากปุ่มการทำงานแบบลอยเป็น 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 ที่แชร์เพื่อเน้นย้ำความสัมพันธ์เชิงพื้นที่ระหว่างสองหน้าจอและบ่งบอกถึงการเลื่อนขึ้น 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
แทนที่จะรวมหน้าจอกล่องจดหมายด้วย CustomTransitionPage
ให้ใช้ 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),
),
],
);
เรียกใช้แอปอีกครั้ง เมื่อคุณเปิดลิ้นชักการนำทางด้านล่างและเปลี่ยนกล่องจดหมาย รายการอีเมลปัจจุบันควรค่อยๆ เลือนหายไปและย่อขนาดลง ขณะที่รายการใหม่ค่อยๆ ปรากฏขึ้นและขยายขนาดขึ้น ดีมาก
หลัง
9. เพิ่มการค่อยๆ เปลี่ยนผ่านระหว่างการเขียนและการตอบ FAB
ในขั้นตอนนี้ เราจะเพิ่มทรานซิชันระหว่างไอคอน FAB ต่างๆ เนื่องจากเราไม่ต้องการเน้นความสัมพันธ์เชิงพื้นที่หรือลำดับชั้น เราจะใช้การค่อยๆ ผ่านเพื่อ "สลับ" ไอคอนต่างๆ ใน FAB ง่ายๆ
ก่อนเพิ่มโค้ดใดๆ เพิ่มเติม ให้ลองเรียกใช้แอป แตะอีเมลและเปิดมุมมองอีเมล ไอคอน FAB ควรเปลี่ยนโดยไม่มีทรานซิชัน
ก่อน
เราจะทํางานใน home.dart
ตลอดช่วงที่เหลือของโค้ดแล็บนี้ ดังนั้นไม่ต้องกังวลเกี่ยวกับการเพิ่มการนําเข้าสําหรับแพ็กเกจภาพเคลื่อนไหว เนื่องจากเราได้ทําสําหรับ home.dart
ไปแล้วในขั้นตอนที่ 2
วิธีการกําหนดค่าการเปลี่ยนภาพ 2 รายการถัดไปจะคล้ายกันมาก เนื่องจากทั้งหมดจะใช้คลาส _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,
);
}
}
มองหาวิดเจ็ต fabSwitcher
ใน _ReplyFabState
ของเรา 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,
),
);
...
เราจะให้ fillColor
แบบโปร่งใสกับ _FadeThroughTransitionSwitcher
เพื่อไม่ให้มีพื้นหลังระหว่างองค์ประกอบเมื่อเปลี่ยน นอกจากนี้ เรายังสร้าง UniqueKey
และกำหนดให้กับไอคอนใดไอคอนหนึ่งด้วย
ในขั้นตอนนี้ ในขั้นตอนนี้ คุณควรจะมี FAB ตามบริบทที่เคลื่อนไหวทั้งหมด การเปลี่ยนเป็นมุมมองอีเมลจะทำให้ไอคอน FAB เก่าค่อยๆ จางหายไปและปรับขนาดออก ขณะที่ไอคอนใหม่ค่อยๆ จางขึ้นมาและปรับขนาดเข้า
หลัง
10. เพิ่มการเปลี่ยนแบบเฟดผ่านระหว่างชื่อของกล่องจดหมายที่หายไป
ในขั้นตอนนี้ เราจะเพิ่มการค่อยๆ เปลี่ยนผ่าน เพื่อทำให้ชื่อกล่องจดหมายจางลงระหว่างสถานะมองเห็นได้และมองไม่เห็นเมื่ออยู่ในมุมมองอีเมล เนื่องจากเราไม่ต้องการเน้นความสัมพันธ์เชิงพื้นที่หรือลำดับชั้น เราจึงใช้การค่อยๆ ผ่านเพื่อ "สลับ" แบบง่ายๆ ระหว่างวิดเจ็ต Text
ที่รวมชื่อกล่องจดหมายและ SizedBox
ที่ว่างเปล่า
ก่อนเพิ่มโค้ดเพิ่มเติม ให้ลองเรียกใช้แอป แตะอีเมล แล้วเปิดมุมมองอีเมล ชื่อกล่องจดหมายควรหายไปโดยไม่มีการเปลี่ยนผ่าน
ก่อน
Codelab ที่เหลือจะดำเนินไปอย่างรวดเร็ว เนื่องจากเราได้ดำเนินการส่วนใหญ่ใน _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 Design โดยใช้โค้ด Dart ไม่ถึง 100 บรรทัด ทั้งยังช่วยให้แอปมีรูปลักษณ์และลักษณะการทำงานที่สอดคล้องกันในทุกอุปกรณ์
ขั้นตอนถัดไป
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับระบบการเคลื่อนไหวแบบ Material นี้ โปรดอ่านหลักเกณฑ์และเอกสารสำหรับนักพัฒนาซอฟต์แวร์ฉบับเต็ม แล้วลองเพิ่มการเปลี่ยนของ Material ลงในแอปของคุณ
ขอขอบคุณที่ลองใช้ Material Motion เราหวังว่าคุณจะชอบ Codelab นี้
ฉันทำ Codelab นี้เสร็จภายในระยะเวลาและความพยายามที่เหมาะสม
ฉันต้องการใช้ระบบการเคลื่อนไหวแบบ Material ต่อไปในอนาคต
ดู Flutter Gallery
โปรดไปที่แกลเลอรี Flutter เพื่อดูการสาธิตเพิ่มเติมเกี่ยวกับวิธีใช้วิดเจ็ตจากไลบรารี Material Flutter รวมถึงเฟรมเวิร์ก Flutter |