การสร้างการเปลี่ยนฉากอย่างสวยงามด้วยการเคลื่อนไหวของวัสดุเพื่อ Flutter

1. บทนำ

Material Design คือระบบสำหรับการสร้างผลิตภัณฑ์ดิจิทัลที่โดดเด่นและสวยงาม โดยการรวมสไตล์ การสร้างแบรนด์ การโต้ตอบ และการเคลื่อนไหวเข้าด้วยกันภายใต้ชุดหลักการและองค์ประกอบที่สอดคล้องกัน ทำให้ทีมผลิตภัณฑ์ตระหนักถึงศักยภาพด้านการออกแบบที่ดีที่สุดของตนเอง

logo_components_color_2x_web_96dp.png

Material Components (MDC) ช่วยให้นักพัฒนานำดีไซน์ Material มาใช้ MDC สร้างโดยทีมวิศวกรและนักออกแบบ UX ที่ Google โดยมีคอมโพเนนต์ UI ที่สวยงามและใช้งานได้หลายสิบอย่างและพร้อมใช้งานสำหรับ Android, iOS, เว็บ และ Flutter.material.io/develop

ระบบการเคลื่อนไหวของ Material สำหรับ Flutter คืออะไร

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

รูปแบบการเปลี่ยน Material 4 รูปแบบหลักๆ มีดังนี้

  • การเปลี่ยนรูปแบบคอนเทนเนอร์: การเปลี่ยนระหว่างองค์ประกอบ UI ที่มีคอนเทนเนอร์ สร้างการเชื่อมต่อที่มองเห็นได้ระหว่างองค์ประกอบ UI 2 องค์ประกอบที่แตกต่างกันด้วยการเปลี่ยนองค์ประกอบหนึ่งเป็นอีกองค์ประกอบหนึ่งอย่างราบรื่น

11807bdf36c66657.gif

  • แกนที่ใช้ร่วมกัน: การเปลี่ยนระหว่างองค์ประกอบ UI ที่มีความสัมพันธ์เชิงพื้นที่หรือการนำทาง ใช้การเปลี่ยนรูปแบบร่วมกันบนแกน x, y หรือ z เพื่อเสริมสร้างความสัมพันธ์ระหว่างองค์ประกอบ

71218f390abae07e.gif

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

385ba37b8da68969.gif

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

cfc40fd6e27753b6.gif

แพ็กเกจภาพเคลื่อนไหวมีวิดเจ็ตการเปลี่ยนสำหรับรูปแบบเหล่านี้ ซึ่งสร้างขึ้นที่ด้านบนของทั้งไลบรารีภาพเคลื่อนไหว 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 ระหว่างชื่อของกล่องจดหมายที่หายไป
  • การเปลี่ยนแบบผ่านลงระหว่างการทำงานของแถบแอปด้านล่าง

b26fe84fed12d17d.gif

สิ่งที่คุณต้องมี

  • ความรู้พื้นฐานเกี่ยวกับการพัฒนา 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

เปิดโปรเจ็กต์และเรียกใช้แอป

  1. เปิดโปรเจ็กต์ในเครื่องมือแก้ไขที่ต้องการ
  2. ทำตามวิธีการ "เรียกใช้แอป" ในเริ่มต้นใช้งาน: ทดลองใช้กับเครื่องมือแก้ไขที่คุณเลือก

สำเร็จ! รหัสเริ่มต้นสำหรับหน้าแรกของการตอบควรทำงานในอุปกรณ์/โปรแกรมจำลอง คุณควรเห็นกล่องจดหมายที่มีรายการอีเมล

หน้าแรกของการตอบ

ไม่บังคับ: ทำให้ภาพเคลื่อนไหวของอุปกรณ์ช้าลง

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

d23a7bfacffac509.gif

ไม่บังคับ: โหมดมืด

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

87618d8418eee19e.gif

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 รายการ

ก่อนที่จะเพิ่มโค้ดใดๆ ให้ลองเรียกใช้แอปตอบกลับแล้วคลิกอีเมล เครื่องมือดังกล่าวควรตัดภาพอย่างรวดเร็ว ซึ่งหมายความว่าโดยไม่ต้องเปลี่ยนหน้าจอเลย วิธีการมีดังนี้

ก่อน

48b00600f73c7778.gif

เริ่มต้นโดยเพิ่มการนําเข้าสำหรับแพ็กเกจภาพเคลื่อนไหวที่ด้านบนของ 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,
 ),
);

ในขั้นตอนนี้ คุณควรเปลี่ยนรูปแบบคอนเทนเนอร์ที่ทำงานได้อย่างสมบูรณ์ การคลิกอีเมลจะเป็นการขยายรายการในหน้าจอรายละเอียดในขณะที่ย่อรายการอีเมล การกด "กลับ" จะยุบหน้าจอรายละเอียดอีเมลกลับไปเป็นรายการย่อยในขณะที่ปรับขนาดรายการอีเมลให้ใหญ่ขึ้น

หลัง

663e8594319bdee3.gif

6. เพิ่มการเปลี่ยนคอนเทนเนอร์จาก FAB เพื่อเขียนหน้าอีเมล

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

ก่อน

4aa2befdc5170c60.gif

วิธีกำหนดค่าการเปลี่ยนนี้จะคล้ายกับวิธีในขั้นตอนสุดท้ายเป็นอย่างมาก เนื่องจากเราใช้คลาสวิดเจ็ตเดียวกัน นั่นคือ 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 ไปเป็นหน้าจอการเขียนที่มีลักษณะดังต่อไปนี้:

หลัง

5c7ad1b4b40f9f0c.gif

7. เพิ่มการเปลี่ยนแกน Z ที่แชร์จากไอคอนค้นหาลงในหน้ามุมมองการค้นหา

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

ก่อนที่จะเพิ่มโค้ดเพิ่มเติม ให้ลองเรียกใช้แอปแล้วแตะไอคอนค้นหาที่มุมขวาล่างของหน้าจอ ซึ่งจะเป็นการเปิดหน้าจอมุมมองการค้นหาโดยไม่มีการเปลี่ยนแปลง

ก่อน

df7683a8ad7b920e.gif

เริ่มต้นด้วยการไปที่ไฟล์ 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(),
     ),
 ],
);

ตอนนี้ให้ลองเรียกใช้แอปอีกครั้ง

81b3ea098926931.gif

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

เราจะแก้ไขปัญหาทั้ง 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 พร้อมๆ กัน จะทำให้เกิดผลที่ราบรื่นระหว่างสองหน้าจอ

หลัง

462d890086a3d18a.gif

8. เพิ่มการเปลี่ยนแบบเฟดผ่านระหว่างหน้ากล่องจดหมาย

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

ก่อนใส่รหัสเพิ่มเติม ให้ลองเรียกใช้แอป แตะโลโก้ "ตอบกลับ" ในแถบแอปด้านล่าง แล้วสลับกล่องจดหมาย รายการอีเมลควรเปลี่ยนแปลงโดยไม่มีการเปลี่ยน

ก่อน

89033988ce26b92e.gif

เริ่มต้นด้วยการไปที่ไฟล์ 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),
   ),
 ],
);

เรียกใช้แอปอีกครั้ง เมื่อเปิดลิ้นชักการนำทางด้านล่างและเปลี่ยนกล่องจดหมาย รายการอีเมลปัจจุบันจะค่อยๆ ปรากฏขึ้นและค่อยๆ ปรากฏขึ้น ในขณะที่รายการใหม่จะค่อยๆ ปรากฏขึ้นและค่อยๆ ปรากฏขึ้น เยี่ยมไปเลย

หลัง

8186940082b630d.gif

9. เพิ่มการค่อยๆ เปลี่ยนผ่านระหว่างการเขียนและการตอบ FAB

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

ก่อนเพิ่มโค้ดใดๆ เพิ่มเติม ให้ลองเรียกใช้แอป แตะอีเมลและเปิดมุมมองอีเมล ไอคอน FAB ควรเปลี่ยนโดยไม่ต้องเปลี่ยน

ก่อน

d8e3afa0447cfc20.gif

เราจะทํางานใน 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 เดิมจางลงและปรับขนาดออก ขณะที่ไอคอนใหม่ค่อยๆ จางลงและปรับขนาดเข้ามา

หลัง

c55bacd9a144ec69.gif

10. เพิ่มการเปลี่ยนแบบเฟดผ่านระหว่างชื่อของกล่องจดหมายที่หายไป

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

ก่อนเพิ่มโค้ดใดๆ เพิ่มเติม ให้ลองเรียกใช้แอป แตะอีเมลและเปิดมุมมองอีเมล ชื่อกล่องจดหมายควรหายไปโดยไม่มีการเปลี่ยนผ่าน

ก่อน

59eb57a6c71725c0.gif

ส่วนที่เหลือของ 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,
                   ),
             );
           },
         ),
       ),
),

เท่านี้ก็เรียบร้อย

เรียกใช้แอปอีกครั้ง เมื่อเปิดอีเมลและระบบนำคุณไปที่มุมมองอีเมล ชื่อกล่องจดหมายในแถบแอปด้านล่างควรค่อยๆ ปรากฏขึ้นและปรับขนาดออก ยอดเยี่ยม!

หลัง

3f1a3db01a481124.gif

11. เพิ่มการเปลี่ยนแบบเฟดผ่านระหว่างการทำงานของแถบแอปด้านล่าง

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

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

ก่อน

5f662eac19fce3ed.gif

เราจะใช้ _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
...

มาลองกันเลย! เมื่อเปิดอีเมลและระบบนำการดำเนินการดังกล่าวไปที่มุมมองอีเมล การดำเนินการบนแถบแอปด้านล่างแบบเก่าจะค่อยๆ จางลงและใหญ่ขึ้น ในขณะที่การดำเนินการใหม่ค่อยๆ ปรากฏขึ้นและค่อยๆ ปรากฏขึ้น เยี่ยมมาก!

หลัง

cff0fa2afa1c5a7f.gif

12. ยินดีด้วย

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

d5637de49eb64d8a.gif

ขั้นตอนถัดไป

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับระบบการเคลื่อนไหวแบบ Material นี้ โปรดอ่านหลักเกณฑ์และเอกสารสำหรับนักพัฒนาซอฟต์แวร์ฉบับเต็ม แล้วลองเพิ่มการเปลี่ยนของ Material ลงในแอปของคุณ

ขอขอบคุณที่ลองใช้การเคลื่อนไหวแบบ Material เราหวังว่าคุณจะชอบ Codelab นี้

ฉันทำ Codelab นี้เสร็จได้ โดยใช้เวลาและลงแรงพอสมควร

เห็นด้วยอย่างยิ่ง เห็นด้วย เฉยๆ ไม่เห็นด้วย ไม่เห็นด้วยอย่างยิ่ง

ฉันต้องการใช้ระบบการเคลื่อนไหวแบบ Material ต่อไปในอนาคต

เห็นด้วยอย่างยิ่ง เห็นด้วย เฉยๆ ไม่เห็นด้วย ไม่เห็นด้วยอย่างยิ่ง

โปรดไปที่แกลเลอรี Flutter เพื่อดูการสาธิตเพิ่มเติมเกี่ยวกับวิธีใช้วิดเจ็ตจากไลบรารี Material Flutter รวมถึงเฟรมเวิร์ก Flutter

46ba920f17198998.png

6ae8ae284bf4f9fa.png