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 ซึ่งหมายความว่าคุณต้องจัดการกับวิดเจ็ต :)
สิ่งที่คุณจะสร้าง
Codelab นี้จะแนะนำขั้นตอนการสร้างการเปลี่ยนให้เป็นแอปอีเมล Flutter ตัวอย่างที่เรียกว่า Answer โดยใช้ Dart เพื่อสาธิตวิธีใช้การเปลี่ยนจากแพ็กเกจภาพเคลื่อนไหวเพื่อปรับแต่งรูปลักษณ์ของแอป
โค้ดเริ่มต้นสำหรับแอปตอบกลับจะมีให้ และคุณจะต้องใส่การเปลี่ยนของ Material ต่อไปนี้ลงในแอป ซึ่งสามารถดูได้ใน GIF ของ Codelab ที่สมบูรณ์ด้านล่างนี้
- การเปลี่ยนจากการเปลี่ยนรูปแบบคอนเทนเนอร์จากรายชื่ออีเมลเป็นหน้ารายละเอียดอีเมล
- การเปลี่ยนการแปลงคอนเทนเนอร์จาก FAB เป็นหน้าเขียนอีเมล
- การเปลี่ยนแกน Z ที่แชร์จากไอคอนค้นหาเป็นหน้ามุมมองการค้นหา
- การเปลี่ยนผ่าน Fade Through ระหว่างหน้ากล่องจดหมาย
- การเปลี่ยนผ่าน Fade Through ระหว่าง FAB การเขียนและการตอบ
- การเปลี่ยนผ่าน Fade Through ระหว่างชื่อของกล่องจดหมายที่หายไป
- การเปลี่ยนแบบผ่านลงระหว่างการทำงานของแถบแอปด้านล่าง
สิ่งที่คุณต้องมี
- ความรู้พื้นฐานเกี่ยวกับการพัฒนา Flutter และ Dart
- ตัวแก้ไขโค้ด
- โปรแกรมจำลองหรืออุปกรณ์ Android/iOS
- โค้ดตัวอย่าง (ดูขั้นตอนถัดไป)
โปรดให้คะแนนประสบการณ์ในการสร้างแอป Flutter
คุณต้องการเรียนรู้อะไรจาก Codelab นี้
2. ตั้งค่าสภาพแวดล้อมในการพัฒนาซอฟต์แวร์ Flutter
ห้องทดลองนี้ต้องมีซอฟต์แวร์ 2 ประเภท ได้แก่ Flutter SDK และเครื่องมือแก้ไข
คุณเรียกใช้ Codelab ได้โดยใช้อุปกรณ์ต่อไปนี้
- อุปกรณ์ 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
ยืนยันทรัพยากร Dependency ของโปรเจ็กต์
โปรเจ็กต์ขึ้นอยู่กับแพ็กเกจภาพเคลื่อนไหว ใน pubspec.yaml
โปรดสังเกตว่าส่วน dependencies
จะมีข้อมูลต่อไปนี้
animations: ^2.0.0
เปิดโปรเจ็กต์และเรียกใช้แอป
- เปิดโปรเจ็กต์ในเครื่องมือแก้ไขที่ต้องการ
- ทำตามวิธีการ "เรียกใช้แอป" ในเริ่มต้นใช้งาน: ทดลองใช้กับเครื่องมือแก้ไขที่คุณเลือก
สำเร็จ! รหัสเริ่มต้นสำหรับหน้าแรกของการตอบควรทำงานในอุปกรณ์/โปรแกรมจำลอง คุณควรเห็นกล่องจดหมายที่มีรายการอีเมล
ไม่บังคับ: ทำให้ภาพเคลื่อนไหวของอุปกรณ์ช้าลง
เนื่องจาก Codelab นี้มีการเปลี่ยนที่รวดเร็วแต่ดูดี ภาพเคลื่อนไหวของอุปกรณ์ให้ช้าลง เพื่อสังเกตรายละเอียดที่ละเอียดยิ่งขึ้นของการเปลี่ยนขณะที่คุณใช้งาน ซึ่งทำได้ผ่านการตั้งค่าในแอป ซึ่งเข้าถึงได้ด้วยการแตะไอคอนการตั้งค่าเมื่อลิ้นชักด้านล่างเปิดอยู่ ไม่ต้องกังวล การทำให้ภาพเคลื่อนไหวในอุปกรณ์ช้าลงนี้จะไม่ส่งผลต่อภาพเคลื่อนไหวบนอุปกรณ์ที่อยู่นอกแอป "ตอบกลับ"
ไม่บังคับ: โหมดมืด
ถ้าการตอบกลับเป็นธีมที่สดใสทำให้สายตาของคุณรำคาญ ก็ไม่ต้องมองอีกต่อไป มีการตั้งค่าในแอปที่ช่วยให้คุณเปลี่ยนธีมของแอปเป็นโหมดมืดได้เพื่อให้เหมาะกับดวงตาของคุณมากขึ้น การตั้งค่านี้เข้าถึงได้โดยแตะไอคอนการตั้งค่าเมื่อลิ้นชักด้านล่างเปิดอยู่
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
หรือไม่ หากมี เครื่องมือจะสร้างตัวนำทางของเราอีกครั้งโดยมี 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 พร้อมๆ กัน จะทำให้เกิดผลที่ราบรื่นระหว่างสองหน้าจอ
หลัง
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
สําหรับส่วนที่เหลือของ Codelab ดังนั้นไม่ต้องห่วงว่าจะเพิ่มการนําเข้าสำหรับแพ็กเกจภาพเคลื่อนไหว เนื่องจากเราได้ดําเนินการกับ 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. ยินดีด้วย
แพ็กเกจภาพเคลื่อนไหวนี้ใช้โค้ด Dart น้อยกว่า 100 บรรทัดช่วยสร้างการเปลี่ยนภาพที่สวยงามในแอปที่มีอยู่ซึ่งสอดคล้องกับหลักเกณฑ์ของดีไซน์ Material รวมถึงมีรูปลักษณ์และการทำงานที่สม่ำเสมอในทุกอุปกรณ์
ขั้นตอนถัดไป
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับระบบการเคลื่อนไหวแบบ Material นี้ โปรดอ่านหลักเกณฑ์และเอกสารสำหรับนักพัฒนาซอฟต์แวร์ฉบับเต็ม แล้วลองเพิ่มการเปลี่ยนของ Material ลงในแอปของคุณ
ขอขอบคุณที่ลองใช้การเคลื่อนไหวแบบ Material เราหวังว่าคุณจะชอบ Codelab นี้
ฉันทำ Codelab นี้เสร็จได้ โดยใช้เวลาและลงแรงพอสมควร
ฉันต้องการใช้ระบบการเคลื่อนไหวแบบ Material ต่อไปในอนาคต
ลองดูแกลเลอรี Flutter
โปรดไปที่แกลเลอรี Flutter เพื่อดูการสาธิตเพิ่มเติมเกี่ยวกับวิธีใช้วิดเจ็ตจากไลบรารี Material Flutter รวมถึงเฟรมเวิร์ก Flutter |