調適理念

#

一般來說,平台適應性有兩種情境:

  1. 屬於作業系統環境行為的事項 (例如文字編輯與滾動), 若出現不同行為會被視為「錯誤」。
  2. 傳統上在應用程式中,會依照 OEM 的 SDK 實作的事項(例如在 iOS 使用平行分頁標籤, 或在 Android 顯示 android.app.AlertDialog)。

本文主要說明 Flutter 在 Android 與 iOS 上針對第一種情境所提供的自動調適。

針對第二種情境,Flutter 提供產生符合平台慣例效果的方式, 但當需要應用程式設計決策時,則不會自動調適。 相關討論請參見 issue #8410Material/Cupertino 適應元件問題定義

若需參考同一份內容程式碼, 但在 Android 與 iOS 採用不同資訊架構結構的應用程式範例, 請參見 platform_design 程式碼範例

頁面導覽

#

Flutter 提供 Android 與 iOS 常見的導覽模式, 並會自動依據目前平台調整導覽動畫。

導覽轉場動畫

#

Android 上,預設的 Navigator.push() 轉場動畫 仿照 startActivity(), 通常只有一種自底向上的動畫變體。

iOS 上:

  • 預設的 Navigator.push() API 產生 iOS 的 Show/Push 風格轉場, 會依據語系的 RTL 設定,從結束端到起始端進行動畫。 新路由背後的頁面也會如 iOS 一樣進行視差滑動。
  • 當推送一個 PageRoute.fullscreenDialog 為 true 的頁面路由時, 會有一種自底向上的獨立轉場風格。 這對應 iOS 的 Present/Modal 風格轉場, 通常用於全螢幕的模態頁面。
Android 上自底向上頁面轉場動畫
Android 頁面轉場
iOS 上結束端到起始端風格的 push 頁面轉場動畫
iOS push 轉場
iOS 上自底向上風格的 present 頁面轉場動畫
iOS present 轉場

平台專屬轉場細節

#

Android 上,Flutter 採用 ZoomPageTransitionsBuilder 動畫。 當使用者點擊某個項目時,UI 會放大進入包含該項目的畫面。 當使用者返回時,UI 會縮小回到前一個畫面。

iOS 上,當使用 push 風格轉場時, Flutter 內建的 CupertinoNavigationBarCupertinoSliverNavigationBar 導覽列 會自動將每個子元件動畫過渡到下一頁或前一頁對應的 CupertinoNavigationBarCupertinoSliverNavigationBar

Android 上頁面轉場動畫
Android
iOS 上頁面轉場時導覽列的動畫過渡
iOS 導覽列

返回導覽

#

Android 上, 作業系統的返回按鈕預設會傳送給 Flutter, 並彈出 WidgetsApp 的 Navigator 最上層路由。

iOS 上, 可以使用螢幕邊緣滑動手勢來彈出最上層路由。

由 Android 返回按鈕觸發的頁面轉場
Android 返回按鈕
由 iOS 返回滑動手勢觸發的頁面轉場
iOS 返回滑動手勢

滾動行為

#

滾動是平台視覺與操作體驗的重要部分, Flutter 會自動調整滾動行為以符合當前平台。

物理模擬

#

Android 與 iOS 都有複雜的滾動物理模擬, 難以用文字完整描述。 一般來說,iOS 的可滾動區塊具有較大的重量與動態摩擦力, 而 Android 則有較高的靜態摩擦力。 因此 iOS 在加速時速度提升較慢,但停止時較不突然, 且在低速時較為滑順。

iOS 可滾動區塊以較低速度滑動距離較 Android 長
輕拋比較
Android 可滾動區塊加速較快,滑動距離較長後突然停止
中等拋比較
Android 可滾動區塊加速較快,且滑動距離明顯更長
強拋比較

超出滾動範圍的行為

#

Android 上, 滾動超出可滾動區塊邊緣時會顯示 overscroll glow indicator (based on the color of the current Material theme)。

iOS 上,滾動超出可滾動區塊邊緣時 會超出滾動範圍,阻力逐漸增加並彈回原位。

Android 與 iOS 可滾動區塊被拋出邊界時展現平台特有的超出滾動行為
動態超出滾動比較
Android 與 iOS 可滾動區塊從靜止狀態被超出滾動時展現平台特有的超出滾動行為
靜態超出滾動比較

動量

#

iOS 上, 連續向同一方向拋動會疊加動量, 每次拋動都會累積更高速度。 Android 上則沒有這種行為。

iOS 上連續拋動產生動量
iOS 滾動動量

回到頂部

#

iOS 上, 點擊作業系統狀態列會將主要 滾動控制器滾動回頂部。 Android 上則沒有這種行為。

點擊狀態列將主要可滾動區塊滾回頂部
iOS 狀態列點擊回頂部

字體排印(Typography)

#

當使用 Material 函式庫時, 字體會自動預設為符合平台的字型。 Android 使用 Roboto 字型。 iOS 使用 San Francisco 字型。

當使用 Cupertino 函式庫時,預設主題 會使用 San Francisco 字型。

San Francisco 字型授權僅限於 執行於 iOS、macOS 或 tvOS 的軟體。 因此,若在 Android 上執行時平台被偽裝為 iOS, 或使用預設 Cupertino 主題時,會使用備用字型。

你也可以選擇讓 Material 元件的文字樣式 更貼近 iOS 的預設文字樣式。 你可以在 UI 元件區段 看到針對元件的具體範例。

Android 上 Roboto 字型排印比例
Android 上的 Roboto
iOS 上 San Francisco 字型排印比例
iOS 上的 San Francisco

圖示設計(Iconography)

#

當使用 Material 函式庫時, 部分圖示會依據平台自動顯示不同圖形。 例如,溢出選單按鈕的三個點在 iOS 上是水平排列, 在 Android 上則是垂直排列。 返回按鈕在 iOS 上是簡單的山形符號(chevron), 在 Android 上則有桿身(stem/shaft)。

適用於 Android 的圖示
Android 上的圖示
適用於 iOS 的圖示
iOS 上的圖示

Material 函式庫也透過 Icons.adaptive 提供一組平台適應性圖示。

觸覺回饋(Haptic feedback)

#

Material 與 Cupertino 函式庫會在 特定情境下自動觸發符合平台的觸覺回饋。

例如, 在文字欄位長按選字時, Android 會有「嗡嗡」震動,iOS 則沒有。

在 iOS 上捲動選擇器(picker)項目時會有 「輕敲」的觸覺回饋,Android 則無回饋。

文字編輯

#

Material 與 Cupertino 的文字輸入欄位 皆支援拼字檢查,並會依平台調整 拼字檢查設定、拼字選單與高亮顏色。

Flutter 也會在編輯文字欄位內容時, 根據當前平台進行下列調適。

鍵盤手勢導覽

#

Android 上, 可在軟體鍵盤的 space 鍵上左右滑動 以移動 Material 與 Cupertino 文字欄位的游標。

在具備 3D Touch 功能的 iOS 裝置上, 可在軟體鍵盤上用重壓拖曳手勢, 以浮動游標在二維空間移動游標。 此功能同時適用於 Material 與 Cupertino 文字欄位。

在 Android 上透過空白鍵移動游標
Android 空白鍵游標移動
在 iOS 上透過 3D Touch 拖曳鍵盤移動游標
iOS 3D Touch 拖曳游標移動

文字選取工具列

#

Android 上使用 Material 時, 於文字欄位選取文字時會顯示 Android 風格的選取工具列。

iOS 上使用 Material 或使用 Cupertino 時, 於文字欄位選取文字時會顯示 iOS 風格的選取工具列。

適用於 Android 的文字工具列
Android 文字選取工具列
適用於 iOS 的文字工具列
iOS 文字選取工具列

單點擊手勢

#

Android 上使用 Material 時, 於文字欄位單點會將游標移至點擊位置。

當文字選取為收合狀態時,也會顯示可拖曳的 控制柄以移動游標。

iOS 上使用 Material 或使用 Cupertino 時, 於文字欄位單點會將游標移至所點單字的最近邊緣。

iOS 上收合狀態的文字選取不會有可拖曳的控制柄。

在 Android 上將游標移至點擊位置
Android 點擊
在 iOS 上將游標移至所點單字最近邊緣
iOS 點擊

長按手勢

#

Android 上使用 Material 時, 長按會選取長按下的單字。 放開時會顯示選取工具列。

iOS 上使用 Material 或使用 Cupertino 時, 長按會將游標移至長按位置。 放開時會顯示選取工具列。

在 Android 上長按選取單字
Android 長按
iOS 
<div class=
dart
// Map the text theme to iOS styles
TextTheme cupertinoTextTheme = TextTheme(
    headlineMedium: CupertinoThemeData()
        .textTheme
        .navLargeTitleTextStyle
         // fixes a small bug with spacing
        .copyWith(letterSpacing: -1.5),
    titleLarge: CupertinoThemeData().textTheme.navTitleTextStyle)
...

// Use iOS text theme on iOS devices
ThemeData(
      textTheme: Platform.isIOS ? cupertinoTextTheme : null,
      ...
)
...

// Modify AppBar properties
AppBar(
        surfaceTintColor: Platform.isIOS ? Colors.transparent : null,
        shadowColor: Platform.isIOS ? CupertinoColors.darkBackgroundGray : null,
        scrolledUnderElevation: Platform.isIOS ? .1 : null,
        toolbarHeight: Platform.isIOS ? 44 : null,
        ...
      ),

不過,由於應用程式列(app bars)會與頁面中的其他內容一起顯示,建議僅在與應用程式整體風格一致的情況下調整其樣式。你可以在 GitHub 討論區關於 app bar 調整的討論 中,查看更多程式碼範例與進一步說明。

底部導覽列(Bottom navigation bars)

#

自 Android 12 起,底部導覽列的預設 UI 已遵循 Material 3 所定義的設計指引。在 iOS 上,對應的元件稱為「Tab Bars(分頁列)」,其設計可參考 Apple 的人機介面指引(Human Interface Guidelines) (HIG)。

<div class=" style=" ">

iOS

dart
// Map the text theme to iOS styles
TextTheme cupertinoTextTheme = TextTheme(
    headlineMedium: CupertinoThemeData()
        .textTheme
        .navLargeTitleTextStyle
         // fixes a small bug with spacing
        .copyWith(letterSpacing: -1.5),
    titleLarge: CupertinoThemeData().textTheme.navTitleTextStyle)
...

// Use iOS text theme on iOS devices
ThemeData(
      textTheme: Platform.isIOS ? cupertinoTextTheme : null,
      ...
)
...

// Modify AppBar properties
AppBar(
        surfaceTintColor: Platform.isIOS ? Colors.transparent : null,
        shadowColor: Platform.isIOS ? CupertinoColors.darkBackgroundGray : null,
        scrolledUnderElevation: Platform.isIOS ? .1 : null,
        toolbarHeight: Platform.isIOS ? 44 : null,
        ...
      ),

不過,由於應用程式列(app bars)會與頁面中的其他內容一起顯示,建議僅在與應用程式整體風格一致的情況下調整其樣式。你可以在 GitHub 討論區關於 app bar 調整的討論 中,查看更多程式碼範例與進一步說明。

底部導覽列(Bottom navigation bars)

#

自 Android 12 起,底部導覽列的預設 UI 已遵循 Material 3 所定義的設計指引。在 iOS 上,對應的元件稱為「Tab Bars(分頁列)」,其設計可參考 Apple 的人機介面指引(Human Interface Guidelines) (HIG)。

Tab Bar in Human Interface Guidelines
Human Interface Guidelines 中的 Tab Bar

由於分頁列(tab bars)會在你的應用程式中持續顯示,因此應該與你的品牌風格相符。不過,如果你選擇在 Android 上使用 Material 的預設樣式,也可以考慮在 iOS 上採用預設的分頁列樣式。

若要實作平台特定的底部導覽列,你可以在 Android 上使用 Flutter 的 NavigationBar 元件,在 iOS 上則使用 CupertinoTabBar 元件。以下是一段你可以參考並調整的程式碼片段,用於顯示平台特定的導覽列。

dart
final Map<String, Icon> _navigationItems = {
    'Menu': Platform.isIOS ? Icon(CupertinoIcons.house_fill) : Icon(Icons.home),
    'Order': Icon(Icons.adaptive.share),
  };

...

Scaffold(
  body: _currentWidget,
  bottomNavigationBar: Platform.isIOS
          ? CupertinoTabBar(
              currentIndex: _currentIndex,
              onTap: (index) {
                setState(() => _currentIndex = index);
                _loadScreen();
              },
              items: _navigationItems.entries
                  .map<BottomNavigationBarItem>(
                      (entry) => BottomNavigationBarItem(
                            icon: entry.value,
                            label: entry.key,
                          ))
                  .toList(),
            )
          : NavigationBar(
              selectedIndex: _currentIndex,
              onDestinationSelected: (index) {
                setState(() => _currentIndex = index);
                _loadScreen();
              },
              destinations: _navigationItems.entries
                  .map<Widget>((entry) => NavigationDestination(
                        icon: entry.value,
                        label: entry.key,
                      ))
                  .toList(),
            ));

文字欄位 (text fields)

#

自 Android 12 起,文字欄位遵循 Material 3 (M3) 設計指引。 在 iOS 上,Apple 的 人機介面指引(Human Interface Guidelines) (HIG) 則定義了對應的元件。

Text Field in Material 3
Material 3 中的文字欄位
Text Field in Human Interface Guidelines
HIG 中的文字欄位

由於文字欄位 (text fields) 需要用戶輸入, 其設計應遵循各平台的慣例。

若要在 Flutter 中實作平台專屬的 TextField, 可以調整 Material TextField 的樣式來達成。

dart
Widget _createAdaptiveTextField() {
  final _border = OutlineInputBorder(
    borderSide: BorderSide(color: CupertinoColors.lightBackgroundGray),
  );

  final iOSDecoration = InputDecoration(
    border: _border,
    enabledBorder: _border,
    focusedBorder: _border,
    filled: true,
    fillColor: CupertinoColors.white,
    hoverColor: CupertinoColors.white,
    contentPadding: EdgeInsets.fromLTRB(10, 0, 0, 0),
  );

  return Platform.isIOS
      ? SizedBox(
          height: 36.0,
          child: TextField(
            decoration: iOSDecoration,
          ),
        )
      : TextField();
}

想進一步了解如何調整文字欄位(text field),請參閱 GitHub 上關於文字欄位的討論。 你可以在該討論中留下回饋或提出問題。