利用適用於 Flutter 的質感動態效果建構精美轉場效果

1. 簡介

Material Design 是打造搶眼數位產品的系統。產品團隊只要遵循一致的原則和元件組合,將樣式、品牌宣傳、互動和動畫一貫化,就能實現最大的設計潛力。

logo_components_color_2x_web_96dp.png

Material Design 元件 (MDC) 可協助開發人員實作質感設計。MDC 是由 Google 工程師和使用者體驗設計師團隊打造,提供數十種精美且功能豐富的 UI 元件,適用於 Android、iOS、網頁和 Flutter.material.io/develop

Flutter 的 Material 動態系統為何?

Flutter 的 Material Design 動作系統是動畫套件中的一組轉場模式,可協助使用者瞭解及瀏覽應用程式,如 Material Design 指南所述。

主要的 Material 轉換模式如下:

  • 容器轉換:在包含容器的 UI 元素之間轉換。將一個元素順暢轉換為另一個元素,以在兩個不同的 UI 元素之間建立明顯的連結。

11807bdf36c66657.gif

  • 共用軸:用於轉換有空間或導覽關係的 UI 元素。使用 x、y 或 z 軸的共用轉換來強化元素之間的關係。

71218f390abae07e.gif

  • 淡入/淡出:轉換彼此關係不穩定的 UI 元素。會採用循序漸進的淡出和淡入,同時為傳入的元素縮放比例。

(385ba37b8da68969.gif)

  • 淡出:適用於在螢幕邊界內進入或離開的 UI 元素。

cfc40fd6e27753b6.gif

動畫套件以 Flutter 動畫程式庫 (flutter/animation.dart) 和 Flutter Material Library (flutter/material.dart) 為基礎,為這些模式提供轉換小工具:

在本程式碼研究室中,您將使用以 Flutter 架構和 Material 程式庫為基礎建構的 Material 轉換作業,代表您將處理小工具。:)

建構項目

本程式碼研究室將引導您使用 Dart,將一些轉場效果建構為名為 Reply 的 Flutter 電子郵件應用程式範例,示範如何使用動畫套件的轉換效果,自訂應用程式的外觀和風格。

系統會提供 Reply 應用程式的範例程式碼,而您要在應用程式中整合以下 Material 轉換,如以下程式碼研究室的 GIF 所示:

  • Container 轉換:從電子郵件清單轉換至電子郵件詳細資料頁面
  • 容器轉換會從懸浮動作按鈕 (FAB) 轉換為撰寫電子郵件頁面
  • 共用 Z 軸從搜尋圖示轉換為搜尋檢視頁面
  • 淡出在信箱頁面間轉換
  • 在撰寫和回覆懸浮動作按鈕 (FAB) 之間進行淡入轉換
  • 在「遺失」信箱標題間轉換淡出轉換
  • 在底部應用程式列動作之間進行淡入/淡出轉換

b26fe84fed12d17d.gif

軟硬體需求

  • 對 Flutter 開發和 Dart 有基本瞭解
  • 程式碼編輯器
  • Android/iOS 模擬器或裝置
  • 程式碼範例 (請參閱下一步)

針對建立 Flutter 應用程式的經驗,您會給予什麼評價?

新手 中級 還算容易

您希望從本程式碼研究室學到什麼?

我對這個主題不太熟悉,且希望概略瞭解相關資訊。 我對這個主題瞭若指掌,但希望複習一下。 我想在自己的專案中參考範例程式碼。 我想查看特定事項的說明。

2. 設定 Flutter 開發環境

您需要使用兩項軟體:Flutter SDK編輯器

您可以使用下列任一裝置執行程式碼研究室:

  • 將實體 AndroidiOS 裝置接上電腦,並設為開發人員模式。
  • iOS 模擬器 (需要安裝 Xcode 工具)。
  • Android Emulator (需要在 Android Studio 中設定)。
  • 瀏覽器 (必須使用 Chrome 進行偵錯)。
  • 下載 WindowsLinuxmacOS 桌面應用程式。您必須在要部署的平台上進行開發。因此,如果您想要開發 Windows 電腦版應用程式,就必須在 Windows 上進行開發,以便存取適當的建構鏈結。如要進一步瞭解作業系統的特定需求,請參閱 docs.flutter.dev/desktop

3. 下載程式碼研究室的範例應用程式

方法 1:從 GitHub 複製入門程式碼研究室應用程式

如要從 GitHub 複製這個程式碼研究室,請執行下列指令:

git clone https://github.com/material-components/material-components-flutter-motion-codelab.git
cd material-components-flutter-motion-codelab

方法 2: 下載 程式碼研究室應用程式的 ZIP 檔案

範例應用程式位於 material-components-flutter-motion-codelab-starter 目錄中。

驗證專案依附元件

專案依附動畫套件。請留意 pubspec.yaml 中的 dependencies 區段,如下所示:

animations: ^2.0.0

開啟專案並執行應用程式

  1. 在您選擇的編輯器中開啟專案。
  2. 按照操作說明「執行應用程式」。

大功告成!Reply 首頁的範例程式碼應在裝置/模擬器上執行。您應該會看到內含電子郵件清單的收件匣。

回覆首頁

選用:放慢裝置動畫的速度

由於本程式碼研究室牽涉到快速、精美的轉場效果,因此建議您在實作時放慢裝置的動畫,以便觀察更豐富的轉換細節。使用者可以透過應用程式內的設定完成操作,方法是在底部導覽匣開啟時輕觸設定圖示。請放心,這個方法減緩裝置動畫的速度,不會影響 Reply 應用程式以外裝置上的動畫。

d23a7bfacffac509.gif

選用:深色模式

如果回覆的鮮明主題讓人眼花撩亂,放輕鬆。應用程式內提供一項設定,可讓你將應用程式主題變更為深色模式,打造更符合你需求的螢幕。在底部導覽匣開啟時輕觸設定圖示,即可存取這項設定。

87618d8418eee19e.gif

4. 熟悉範例應用程式的程式碼

我們來看看程式碼。我們提供了應用程式,能使用動畫套件在應用程式中切換不同畫面。

  • HomePage:顯示所選的信箱
  • InboxPage:顯示電子郵件清單
  • MailPreviewCard:顯示電子郵件預覽畫面
  • MailViewPage:顯示單封完整的電子郵件
  • ComposePage:可組合新電子郵件
  • SearchPage:顯示搜尋檢視畫面

router.dart

首先,如要瞭解應用程式根導覽的設定方式,請在 lib 目錄中開啟 router.dart

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);
 }
}

這是我們的根導覽器,它會處理使用整個畫布的應用程式畫面,例如 HomePageSearchPage。系統會監聽應用程式的狀態,檢查是否已將路徑設為 ReplySearchPath。如果有,系統會使用堆疊頂端的 SearchPage 重新建構導覽器。請注意,我們的畫面會包裝在 CustomTransitionPage 中,且未定義轉場效果。這是一種在畫面之間瀏覽的方式,無需任何自訂轉場效果。

home.dart

我們在 home.dart_BottomAppBarActionItems 內執行以下操作,以便在應用程式的狀態中將路徑設為 ReplySearchPath

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

現在,來看看如何設定應用程式內部導覽,請在 lib 目錄中開啟 mail_view_router.dart。您會看到類似上述畫面的導覽器:

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,
             ),
           )
         ],
       );
     },
   );
 }
...
}

這是內部導覽器它會處理應用程式內部螢幕,只取用畫布的主體,例如 InboxPageInboxPage 會根據目前應用程式狀態的信箱顯示電子郵件清單。每當應用程式狀態的 currentlySelectedInbox 屬性發生變更時,堆疊頂端都會使用正確的 InboxPage 重新建構導覽器。

home.dart

我們透過 home.dart_HomePageState 內部執行下列動作,在應用程式的狀態中設定目前信箱:

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

最後,如要查看使用導覽路徑的範例,請在 lib 目錄中開啟 home.dart。在 InkWell 小工具的 onTap 屬性中找到 _ReplyFabState 類別,如下所示:

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();
     },
   ),
 );
},

這裡顯示如何在不自訂轉場效果的情況下,前往電子郵件撰寫頁面。在本程式碼研究室中,您將深入探索 Reply 的程式碼,設定 Material 轉換功能,與應用程式的各種導覽動作搭配使用。

現在您已熟悉範例程式碼,接下來要實作第一個轉換作業。

5. 新增從電子郵件清單到電子郵件詳細資料頁面的 Container Transform 轉換作業

首先,您將在點選電子郵件時新增轉場效果。針對這項導覽變更,容器轉換模式是非常實用的,因為其設計是為了在內含容器的 UI 元素之間轉換。這個模式可在兩個 UI 元素之間建立可見的連線。

新增任何程式碼之前,請先嘗試執行 Reply 應用程式,然後點選電子郵件。它應會執行簡單的跳轉,也就是以不轉換方式取代螢幕畫面:

變更前

48b00600f73c7778.gif

首先,請在 mail_card_preview.dart 頂端新增動畫套件的匯入項目,如以下程式碼片段所示:

mail_card_preview.dart

import 'package:animations/animations.dart';

現在您已匯入動畫套件,就可以開始在應用程式中加入精美的轉換。首先,建立一個用來存放 OpenContainer 小工具的 StatelessWidget 類別。

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,
       );
     },
   );
 }
}

現在,讓我們加入使用新的包裝函式。在 MailPreviewCard 類別定義中,我們會使用新的 _OpenContainerWrapper 納入 build() 函式中的 Material 小工具:

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) 轉換為撰寫電子郵件頁面的 Container Transform 轉換作業

讓我們繼續使用容器轉換,並將「懸浮動作」按鈕的轉場效果新增至 ComposePage,將 FAB 展開為要由使用者撰寫的新電子郵件。首先,請重新執行應用程式,然後點選懸浮動作按鈕 (FAB),確認啟動電子郵件撰寫畫面時不會發生轉換。

變更前

(4aa2befdc5170c60.gif)

由於我們使用相同的小工具類別 OpenContainer,因此設定這個轉換作業的方式會與在上一個步驟中非常類似。

home.dart 中,讓我們匯入檔案頂端的 package:animations/animations.dart,並修改 _ReplyFabState build() 方法。現在,讓我們使用 OpenContainer 小工具納入傳回的 Material 小工具:

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 現在也已設定完成。onClosedClosedCallback,系統會在 OpenContainer 路線彈出或返回關閉狀態時呼叫。交易的傳回值會做為引數傳遞至此函式。我們會使用這個 Callback 通知應用程式供應商,我們已經離開 ComposePage 路徑,以便通知所有事件監聽器。

與最後一個步驟類似,我們會從小工具中移除 Material 小工具,因為 OpenContainer 小工具會處理 closedBuilder 透過 closedColor 傳回的小工具顏色。我們也會移除 InkWell 小工具 onTap 內的 Navigator.push() 呼叫,並將其替換為 OpenContainer 小工具 closedBuilder 提供的 openContainer() Callback,因為現在 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 小工具現在能夠透過 onClosed ClosedCallback 通知應用程式供應器,表示我們已不再使用 ComposePage,因此我們可以移除 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) 轉換為 Compose 畫面,如下所示:

變更後

5c7ad1b4b40f9f0c.gif

7. 新增分享的 Z 軸從搜尋圖示到搜尋檢視頁面

在這個步驟中,我們要新增轉場效果:從搜尋圖示轉換至全螢幕搜尋檢視畫面。由於這次導覽變更並未牽涉到永久的容器,因此我們可以使用共用的 Z 軸轉場來強化兩個畫面之間的空間關係,並指定應用程式階層中向上的一層。

如要新增其他程式碼,請先執行應用程式,然後輕觸畫面右下角的搜尋圖示。這應該會出現搜尋檢視畫面,而不顯示任何轉場效果。

變更前

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)

一切都開始看起來很棒!當你點選底部應用程式列中的搜尋圖示時,共用軸轉場會將搜尋頁面縮放為檢視畫面中。但請注意,首頁不會向外擴充,而是在搜尋網頁縮放時保持靜止狀態。此外,按下返回按鈕時,首頁不會縮放至檢視畫面中,而會在搜尋網頁縮小檢視範圍時保持靜止狀態。我們還沒結束

我們也可以將 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. 在信箱頁面之間新增淡入/淡出轉場效果

在這個步驟中,我們會在不同信箱之間新增轉換作業。由於我們不想強調空間或階層關係,因此我們會使用淡入效果來執行簡單的「切換」作業電子郵件清單。

新增任何程式碼之前,請先嘗試執行應用程式、輕觸底部應用程式列中的「Reply」標誌,然後切換信箱。電子郵件清單應在不經過轉換的情況下變更。

變更前

(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,因此不必擔心新增動畫套件的匯入作業,因為步驟 2 中已經完成了 home.dart

接下來數次轉換作業的設定方式會非常類似,因為這些轉換都會使用可重複使用的類別 _FadeThroughTransitionSwitcher

home.dart_ReplyFabState 底下,新增下列程式碼片段:

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
class _FadeThroughTransitionSwitcher extends StatelessWidget {
 const _FadeThroughTransitionSwitcher({
   required this.fillColor,
   required this.child,
 });

 final Widget child;
 final Color fillColor;

 @override
 Widget build(BuildContext context) {
   return PageTransitionSwitcher(
     transitionBuilder: (child, animation, secondaryAnimation) {
       return FadeThroughTransition(
         fillColor: fillColor,
         child: child,
         animation: animation,
         secondaryAnimation: secondaryAnimation,
       );
     },
     child: child,
   );
 }
}

現在,請在 _ReplyFabState 中尋找 fabSwitcher 小工具。fabSwitcher 會根據是否在電子郵件檢視畫面中傳回不同的圖示。讓我們用 _FadeThroughTransitionSwitcher 包裝內容:

home.dart

// TODO: Add Fade through transition between compose and reply FAB (Motion)
static final fabKey = UniqueKey();
static const double _mobileFabDimension = 56;

@override
Widget build(BuildContext context) {
 final theme = Theme.of(context);
 final circleFabBorder = const CircleBorder();

 return Selector<EmailStore, bool>(
   selector: (context, emailStore) => emailStore.onMailView,
   builder: (context, onMailView, child) {
     // TODO: Add Fade through transition between compose and reply FAB (Motion)
     final fabSwitcher = _FadeThroughTransitionSwitcher(
       fillColor: Colors.transparent,
       child: onMailView
           ? Icon(
               Icons.reply_all,
               key: fabKey,
               color: Colors.black,
             )
           : const Icon(
               Icons.create,
               color: Colors.black,
             ),
     );
...

我們會為 _FadeThroughTransitionSwitcher 提供透明的 fillColor,因此轉換時元素之間沒有背景。我們也會建立 UniqueKey,並將其指派給其中一個圖示。

現在,您應該具有完整動畫的懸浮動作按鈕 (FAB)。進入電子郵件檢視畫面後,舊的懸浮動作按鈕 (FAB) 圖示在淡出和縮小時淡出和縮小。

變更後

c55bacd9a144ec69.gif

10. 在信箱標題消失之間新增淡出轉場效果

在這個步驟中,我們會加入淡入/淡出轉場效果,讓信箱標題在電子郵件檢視畫面的可見狀態和隱形狀態之間淡出。由於我們不想強調空間或階層關係,因此我們會使用淡入效果來執行簡單的「切換」作業包含信箱標題的 Text 小工具和空白的 SizedBox

新增任何程式碼之前,請先嘗試執行應用程式,然後輕觸電子郵件並開啟電子郵件檢視畫面。信箱標題應在未經轉換的情況下消失。

變更前

$59eb57a6c71725c0.gif

由於我們已在上一個步驟中完成大部分的 _FadeThroughTransitionSwitcher 工作,因此本程式碼研究室的其餘部分將很快完成。

現在,我們要前往 home.dart 中的 _AnimatedBottomAppBar 類別,新增轉場效果。我們會重複使用最後一個步驟中的 _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 類別定義,然後使用 _FadeThroughTransitionSwitcher 納入 build() 函式的傳回小工具:

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. 恭喜!

使用不到 100 行的 Dart 程式碼,動畫套件可協助您在符合 Material Design 指南的現有應用程式中,建立美觀的轉場效果,而且在所有裝置上都能呈現一致的外觀和行為。

d5637de49eb64d8a.gif

後續步驟

如要進一步瞭解 Material 動態效果系統,請務必查看指南和完整的開發人員說明文件,嘗試在應用程式中新增一些 Material 轉場效果!

感謝你試用 Material Design。希望您喜歡本程式碼研究室!

我可以在合理的時間內,完成本程式碼研究室

非常同意 同意 普通 不同意 非常不同意

我想在未來繼續使用 Material Design 動態系統

非常同意 同意 普通 不同意 非常不同意

如需更多有關 Material Flutter 程式庫所提供小工具的示範,以及 Flutter 架構,請務必造訪 Flutter Gallery

46ba920f17198998.png

6ae8ae284bf4f9fa.png