自動適配不同平台操作體驗

適配哲學

平台適配通常有兩種情形:

  1. 作業系統所特有的操作體驗(例如文字編輯和捲動)。如果操作體驗與作業系統不一致,則通常會被認為是『錯誤的』。

  2. 使用 OEM 提供的 SDK 實現的功能體驗(例如 iOS 常使用的頁籤, Android 使用 android.app.AlertDialog 顯示一個提示視窗)。

本文囊括了 Flutter 為解決情形 1 而提供的覆蓋 Android 和 iOS 的自動適配。

對於情形 2,Flutter 提供了一些工具可以生成符合平台習慣的體驗,但是不會根據平台自動適配,需要根據 App 設計來手工選擇。更多有關的討論,請訪問 issue #8410 和這個文件 定義 Material/Cupertino widget 適配問題

如果一個應用需要在 Android 和 iOS 不同架構上使用相同的程式碼,請參閱 platform_design 這份程式碼範例

Flutter 分別為 Android 和 iOS 提供了各自平台的導向模式,並根據當前平台自動適配導向轉場動畫。

Android 平台,預設提供的 Navigator.push() 轉場動畫模仿了 startActivity() 的動畫,即一種自下而上的動畫效果。

iOS 平台:

  • iOS 的 Navigator.push() API 提供了 iOS 上的 Show 轉場動畫(也被稱為 Push 轉場動畫),即根據語言的方向設定,執行一種從後到前的捲動動畫效果。在顯示新頁面的時候,原來的頁面也會沿著相同的方向進行視差捲動。

  • 當顯示一個頁面,且 PageRoute.fullscreenDialog 是 true 的時候, iOS 提供了另外一種自下而上的動畫效果。這個動畫通常被用在展示全屏模態頁,也被稱為 iOS 上的 Present 轉場動畫或 Modal 轉場動畫。

An animation of the bottom-up page transition on Android
Android 轉場動畫
An animation of the end-start style push page transition on iOS
iOS Push 轉場動畫
An animation of the bottom-up style present page transition on iOS
iOS Present 轉場動畫

不同平台的轉場動畫細節

Android 平台上,Flutter 使用 ZoomPageTransitionsBuilder 轉場動畫。當用戶進行了路由跳轉,介面會縮放至下一個頁面。當用戶回傳上一頁時,介面會縮放回上一個頁面。

當在 iOS 平台上使用 Push 轉場特效的時候, Flutter 內建的 CupertinoNavigationBarCupertinoSliverNavigationBar 會自動的給當前頁下一頁的子元件使用正確的動畫效果(CupertinoNavigationBar 或者 CupertinoSliverNavigationBar)。

Android
An animation of the nav bar transitions during a page transition on iOS
iOS Nav Bar

回傳導向

Android 平台,通常作業系統的回傳按鈕觸發的事件會發給 Flutter,並彈出 WidgetsApp 路由的最頂端。

iOS 平台,從螢幕邊緣的輕掃手勢會彈出路由的最頂端。

A page transition triggered by the Android back button
Android back button
A page transition triggered by an iOS back swipe gesture
iOS back swipe gesture

捲動

捲動是不同平台提供獨有體驗非常重要的一環, Flutter 會根據當前的平台自動適配捲動體驗。

物理模擬

Android 和 iOS 平台都提供了非常複雜的捲動物理模擬,因而很難用語言來描述。通常來說, iOS 的捲動通常提供更多的分量和動態的阻力;而 Android 則更多的使用靜態的阻力。所以,iOS 隨著捲動慢慢的達到高速,且不會突然的停止,而且在慢速的時候顯得更順滑。

A soft fling where the iOS scrollable slid longer at lower speed than Android
Soft fling comparison
A medium force fling where the Android scrollable reached speed faster and stopped more abruptly after reaching a longer distance
Medium fling comparison
A strong fling where the Android scrollable reach speed faster and reached significantly more distance
Strong fling comparison

捲動邊界行為

Android 平台,捲動達到邊界的時候,會顯示 捲動灰色指示 (具體顏色根據 Material 主題而有所不同)。

iOS 平台,捲動達到邊界的時候,會顯示一個 捲動邊界 的彈簧效果。

Android and iOS scrollables being flung past their edge and exhibiting platform specific overscroll behavior
Dynamic overscroll comparison
Android and iOS scrollables being overscrolled from a resting position and exhibiting platform specific overscroll behavior
Static overscroll comparison

動量

iOS 平台,不停的按相同方向捲動會產生動量疊加,從而連續捲動速度會越來越快。在 Android 平台上沒有對應的行為。

Repeated scroll flings building momentum on iOS
iOS scroll momentum

回傳頂部

iOS 平台,點選作業系統的狀態欄,主要的捲軸控制器會捲動到頂部。 Android 沒有對應的行為(部分國產系統自己實現了該屬性)。

Tapping the status bar scrolls the primary scrollable back to the top
iOS status bar tap to top

排版

當使用 Material package 的時候,排版會根據平台自動使用對應的字型。 Android 平台會使用 Roboto 字型,而 iOS 則會使用系統自帶的 San Francisco 字型。

當使用 Cupertino 套件的時候,預設主題 會使用 San Francisco 字型。

San Francisco 字型的授權限制了它只能被用在執行於 iOS、macOS 和 tvOS 平台上的軟體。因此當執行在 Android 平台的時候,即使強制覆蓋系統平台為 iOS 或者使用 Cupertino 預設主題,都會使用對應的替代字型。

你可以選擇將 Material widgets 的文字樣式適配到 iOS 的預設文字樣式。你可以在 UI 元件部分 看到特定元件的例子。

Roboto font on Android
Roboto on Android
San Francisco font on iOS
San Francisco on iOS

圖示

當使用 Material 套件的時候,根據平台不同,圖示的具體樣式會有差別。舉例來說,更多按鈕的圖示,Android 上是豎直的三個點而 iOS 是橫著的三個點;退回按鈕,iOS 是一個簡單的 V 型標記,而 Android 平台,V 型標記有個短橫線。

Android appropriate icons
Icons on Android
iOS appropriate icons
Icons on iOS

Material 也透過 Icons.adaptive 提供了一系列根據平台自適應的圖示。

觸控反饋

Material 和 Cupertino 套件在特定場景下都會自動觸發符合平台特點的觸控反饋。

例如,在文字輸入框控制元件裡面長按選中單詞會在 Android 裝置上會觸發震動,而 iOS 不會。

在 iOS 捲動選擇器專案清單,會觸發一個很輕的敲擊音效,而 Android 則不會。

文字編輯

Flutter 會根據當前平台來適配正確的文字編輯體驗。

鍵盤手勢導向

Android 平台,在虛擬鍵盤空格鍵上可以透過左右輕掃來移動游標, Material 和 Cupertino 的文字輸入框控制元件都支援該屬性。

iOS 裝置提供了 3D Touch 相容,透過在虛擬鍵盤上使用長按並拖拽手勢可以任意方向移動游標。 Material 和 Cupertino 都對這個功能提供了支援。

Moving the cursor via the space key on Android
Android space key cursor move
Moving the cursor via 3D Touch drag on the keyboard on iOS
iOS 3D Touch drag cursor move

文字選中工具欄

Android 平台上使用 Material,在文字輸入框裡面選中文字會顯示一個 Android 風格的文字選中工具欄。

iOS 平台上使用 Material 或者在兩個平台上都使用 Cupertino,在文字輸入框裡面選中文字會展示一個 iOS 風格的文字選中工具欄。

Android appropriate text toolbar
Android text selection toolbar
iOS appropriate text toolbar
iOS text selection toolbar

點選手勢

Android 平台使用 Material,在文字控制元件中點擊會移動游標到點擊處。

同時,游標會有一個可移動的把手,隨後可以透過這個把手移動游標。

iOS 平台使用 Material 或者在兩個平台都使用 Cupertino,在文字空間中點擊,會把游標移動到點選處最近的單詞末尾。

在 iOS 平台上,游標是沒有把手的。

Moving the cursor to the tapped position on Android
Android tap
Moving the cursor to the nearest edge of the tapped word on iOS
iOS tap

長按手勢

Android 平台使用 Material,在單詞上長按會選中單詞,並在釋放長按的時候顯示文字選中工具欄。

iOS 平台使用 Material 或者在兩個平台都使用 Cupertino,長按會把游標放置到長按的位置,並在釋放長按的時候顯示文字選中工具欄。

Selecting a word via long press on Android
Android long press
Selecting a position via long press on iOS
iOS long press

長按並拖放手勢

Android 平台上使用 Material,長按並拖拽會選中更多單詞。

iOS 平台使用 Material 或者在兩個平台都使用 Cupertino,長按並拖拽會移動游標。

Expanding word selection via long press drag on Android
Android long press drag
Moving the cursor via long press drag on iOS
iOS long press drag

雙擊手勢

Android 和 iOS 平台上,雙擊選中一個單詞都會收到雙擊手勢事件,並顯示文字選中工具欄。

Selecting a word via double tap on Android
Android double tap
Selecting a word via double tap on iOS
iOS double tap

UI 元件

本節包含有關如何調整 Material widget 以在 iOS 上提供自然且亮眼的體驗的初步提議。歡迎你對問題 issue #8427 提出反饋。

帶有 .adaptive() 構造的 widget

部分 widget 帶有 .adaptive() 構造。下方的表列出了這些 widget。當應用程式在 iOS 裝置上執行時,它們會自動以 Cupertino 的元件構造。

這些 widget 主要用於輸入、選擇和顯示系統訊息。由於它們與系統高度相關,使用者可能已經習慣於與它們產生對應的記憶和反應。因此我們建議你在應用程式中採用各自平台的轉化內容。

Material Widget Cupertino Widget Adaptive Constructor
Switch in Material 3
Switch
Switch in HIG
CupertinoSwitch
Switch.adaptive()
Slider in Material 3
Slider
Slider in HIG
CupertinoSlider
Slider.adaptive()
Circular progress indicator in Material 3
CircularProgressIndicator
Activity indicator in HIG
CupertinoActivityIndicator
CircularProgressIndicator.adaptive()
 Checkbox in Material 3
Checkbox
Checkbox in HIG
CupertinoCheckbox
Checkbox.adaptive()
Radio in Material 3
Radio
Radio in HIG
CupertinoRadio
Radio.adaptive()

頂部應用欄和導向欄

自 Android 12 起,頂部應用欄的介面遵循 Material 3 中定義的設計指南。在 iOS 上,Apple 的人機界面指南 (HIG) 中定義了一個名為「導向欄」的等效元件。

 Top App Bar in Material 3
Top App Bar in Material 3
Navigation Bar in Human Interface Guidelines
Navigation Bar in Human Interface Guidelines

Flutter 應用程式中應用欄的某些屬性,例如系統圖標和頁面轉場,都應該進行調整。使用 Material AppBarSliverAppBar widget 時,這些屬性都已經自動調整。你還可以進一步自定義它們的屬性,以更好地對應 iOS 的平台風格,如下所示。

// 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,
        ...
      ),

但是,由於頂欄是與頁面中的其他內容一起顯示的,因此我們只建議調整樣式,它只要能與應用程式的其餘部分保持一致即可。你可以在 有關應用欄適配的 GitHub 討論中 檢視其他程式碼範例和進一步說明。

Bottom navigation bars

自 Android 12 起,底部導向欄的介面遵循 Material 3 中定義的設計指南。在 iOS 上,Apple 的人機界面指南 (HIG) 中定義了一個名為「標籤頁欄」的等效元件。

Bottom Navigation Bar in Material 3
Bottom Navigation Bar in Material 3
Tab Bar in Human Interface Guidelines
Tab Bar in Human Interface Guidelines

由於標籤欄在你的應用程式中是持續存在的,因此它們應該與你自己的品牌對應。如果你選擇在 Android 上使用 Material 的預設樣式,你應該要在考慮在 iOS 平台上使用 iOS 的標籤頁欄。

如果你要實現特定於平台的底部導向欄,可以在 Android 上使用 Flutter 的 NavigationBar 小部件,在 iOS 上使用 CupertinoTabBar 小部件。下面是用於顯示特定於平台的導向欄的程式碼片段。

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

文字輸入

自 Android 12 起,文字輸入元件的介面遵循 Material 3 中定義的設計指南。在 iOS 上,Apple 的人機界面指南 (HIG) 中定義了一個名為「文字欄」的等效元件。

Text Field in Material 3
Text Field in Material 3
Text Field in Human Interface Guidelines
Text Field in HIG

由於使用者需要用文字欄來輸入,因此它們應該遵循平台習慣來展示。

你可以調整 Material 材質的 TextField 的樣式來實現對應平台的 TextField

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

若你想要瞭解有關調整文字欄位的更多訊息,請檢視 有關文字欄位的 GitHub 討論。你可以在討論中留下反饋或提出問題。

提醒對話框

自 Android 12 起,提醒對話框的介面遵循 Material 3 中定義的設計指南。在 iOS 上,Apple 的人機界面指南 (HIG) 中定義了一個名為「提醒」的等效元件。

Basic Dialog in Material 3
Basic Dialog in M3
Alert in Human Interface Guidelines
Alert in HIG

由於提醒與系統高度相關,通常應當遵循平台習慣來實現它們。對話框的實現對於使用對話框請求使用者輸入安全、隱私或破壞性操作(例如永久刪除檔案)而言至關重要。而與品牌相關的提醒對話框可以以不打斷使用者流程的方式,高亮特定的內容。

如果你要實現特定於平台的提示對話框,可以在 Android 上使用 Flutter 的 AlertDialog 小部件,在 iOS 上使用 CupertinoAlertDialog 小部件。下面是用於顯示特定於平台的提醒對話框的程式碼片段。

void _showAdaptiveDialog(
  context, {
  required Text title,
  required Text content,
  required List<Widget> actions,
}) {
  Platform.isIOS || Platform.isMacOS
      ? showCupertinoDialog<String>(
          context: context,
          builder: (BuildContext context) => CupertinoAlertDialog(
            title: title,
            content: content,
            actions: actions,
          ),
        )
      : showDialog(
          context: context,
          builder: (BuildContext context) => AlertDialog(
            title: title,
            content: content,
            actions: actions,
          ),
        );
}

若你想要瞭解有關提醒對話框適配問題的更多訊息,請檢視 有關對話框適配的 GitHub 討論。你可以在討論中留下反饋或提出問題。