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

1. 簡介

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

logo_components_color_2x_web_96dp.png

Material 元件 (MDC) 可協助開發人員實作 Material Design。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 材質程式庫 (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

選用:深色模式

如果 Reply 的亮色主題讓你眼睛不舒服,應用程式內提供一項設定,可讓你將應用程式主題變更為深色模式,更加符合眼睛需求。開啟底部抽屜後,輕觸設定圖示即可存取這項設定。

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

我們在應用程式狀態中將路線設為 ReplySearchPath,方法是在 home.dart_BottomAppBarActionItems 中執行下列操作:

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. 從電子郵件清單新增至電子郵件詳細資料頁面的容器轉換轉場效果

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

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

變更前

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

接下來,我們來使用新的包裝函式。在 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 小工具會透過 closedColor 處理 closedBuilder 傳回的小工具顏色。我們也會移除 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 到構圖畫面的轉場效果,如下所示:

變更後

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)

情況開始好轉!當你點選底部應用程式列中的搜尋圖示時,共用軸轉場會將搜尋頁面縮放為檢視畫面中。不過,請注意首頁並未縮放,而是在搜尋頁面縮放時保持靜止。此外,按下返回按鈕時,首頁不會縮放至檢視畫面中,而會在搜尋網頁縮小檢視範圍時保持靜止狀態。我們還沒結束

讓我們同時修正這兩個問題,並使用 SharedAxisTransitionWrapper 而非 CustomTransitionPage 包裝 HomePage

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