Skip to main content

自動平台調適

進一步了解 Flutter 的平台適應性。

調適理念

#

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

  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 風格轉場, 通常用於全螢幕的模態頁面。
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 動畫。 當使用者點擊某個項目時,UI 會放大進入包含該項目的畫面。 當使用者返回時,UI 會縮小回到前一個畫面。

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

An animation of the page transition on Android

Android

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

iOS 導覽列

返回導覽

#

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

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

A page transition triggered by the Android back button

Android 返回按鈕

A page transition triggered by an iOS back swipe gesture

iOS 返回滑動手勢

滾動行為

#

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

物理模擬

#

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

A soft fling where the iOS scrollable slid longer at lower speed than Android

輕拋比較

A medium force fling where the Android scrollable reaches speed faster and stopped more abruptly after reaching a longer distance

中等拋比較

A strong fling where the Android scrollable reaches speed faster and covered significantly more distance

強拋比較

超出滾動範圍的行為

#

Android 上, 滾動超出可滾動區塊邊緣時會顯示 超出滾動光暈指示器(顏色根據當前 Material 主題)。

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

Android and iOS scrollables being flung past their edge and exhibiting platform specific overscroll behavior

動態超出滾動比較

Android and iOS scrollables being overscrolled from a resting position and exhibiting platform specific overscroll behavior

靜態超出滾動比較

捲軸(Scrollbars)

#

Material 類平台(例如 Android 與 Web)上, 捲軸通常在滾動時可見,並可能根據平台與主題設定持續顯示。

Cupertino 類平台(例如 iOS)上, 捲軸較為精簡,通常只在使用者主動滾動時短暫出現, 停止互動後便會淡出。

此差異反映各平台的視覺慣例, 有助於在各裝置上維持原生的視覺與操作體驗。

動量

#

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

Repeated scroll flings building momentum on iOS

iOS 滾動動量

回到頂部

#

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

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

iOS 狀態列點擊回頂部

字體排印(Typography)

#

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

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

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

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

Roboto font typography scale on Android

Android 上的 Roboto

San Francisco typography scale on iOS

iOS 上的 San Francisco

圖示設計(Iconography)

#

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

Android appropriate icons

Android 上的圖示

iOS appropriate icons

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 文字欄位。

Moving the cursor via the space key on Android

Android 空白鍵游標移動

Moving the cursor via 3D Touch drag on the keyboard on iOS

iOS 3D Touch 拖曳游標移動

文字選取工具列

#

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

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

Android appropriate text toolbar

Android 文字選取工具列

iOS appropriate text toolbar

iOS 文字選取工具列

單點擊手勢

#

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

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

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

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

Moving the cursor to the tapped position on Android

Android 點擊

Moving the cursor to the nearest edge of the tapped word on iOS

iOS 點擊

長按手勢

#

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

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

Selecting a word with long press on Android

Android 長按

Selecting a position with long press on iOS

iOS 長按

長按拖曳手勢

#

Android 上使用 Material 時, 長按後拖曳會擴展所選取的單字範圍。

iOS 上使用 Material 或使用 Cupertino 時, 長按後拖曳會移動游標。

Expanding word selection with a long-press drag on Android

Android 長按拖曳

Moving the cursor with a long-press drag on iOS

iOS 長按拖曳

雙擊手勢

#

在 Android 與 iOS 上, 雙擊會選取接收雙擊的單字, 並顯示選取工具列。

Selecting a word via double tap on Android

Android 雙擊

Selecting a word via double tap on iOS

iOS 雙擊

UI 元件

#

本節提供初步建議,說明如何調適 Material 元件 (Widget), 以在 iOS 上提供自然且吸引人的使用體驗。 歡迎在 issue #8427 提供回饋。

具備 .adaptive() 建構函式的元件

#

多個元件支援 .adaptive() 建構函式。 下表列出這些元件。 當應用程式在 iOS 裝置上執行時, 適應性建構函式會替換為對應的 Cupertino 元件。

下表中的元件主要用於輸入、選取,以及顯示系統資訊。 由於這些控制項與作業系統緊密整合, 使用者已習慣辨識並回應這些控制項。 因此,我們建議遵循各平台的慣例。

Material 元件Cupertino 元件適應性建構函式
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()
Refresh indicator in Material 3
RefreshProgressIndicator
Refresh indicator in HIG
CupertinoActivityIndicator
RefreshIndicator.adaptive()
 Checkbox in Material 3
Checkbox
Checkbox in HIG
CupertinoCheckbox
Checkbox.adaptive()
Radio in Material 3
Radio
Radio in HIG
CupertinoRadio
Radio.adaptive()
AlertDialog in Material 3
AlertDialog
AlertDialog in HIG
CupertinoAlertDialog
AlertDialog.adaptive()

頂部應用程式列與導覽列

#

自 Android 12 起,頂部應用程式列的預設 UI 遵循 Material 3 所定義的設計指引。 在 iOS 上,對應的元件稱為「導覽列(Navigation Bars)」, 其設計定義於 Apple 的人機介面指引(Human Interface Guidelines) (HIG)。

Top App Bar in Material 3

Material 3 中的頂部應用程式列

Navigation Bar in Human Interface Guidelines

人機介面指引中的導覽列

Flutter 應用程式中應用程式列的某些屬性需要調適, 例如系統圖示與頁面轉場。 使用 Material 的 AppBarSliverAppBar 元件時, 這些屬性已會自動調適。 你也可以進一步自訂這些元件的屬性, 以更貼近 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,
        ...
      ),

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

底部導覽列

#

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

Bottom Navigation Bar in Material 3

Material 3 的底部導覽列

Tab Bar in Human Interface Guidelines

人機介面指引中的 Tab Bar

由於分頁列會在應用程式中持續顯示, 因此應該與你的品牌風格相符。 不過,如果你選擇在 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(),
            ));

文字欄位

#

自 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 中的文字欄位

由於文字欄位需要使用者輸入, 其設計應遵循各平台的慣例。

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

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