跳到主內容

自動平臺適配

瞭解更多關於 Flutter 平臺自適應性的資訊。

適配理念

#

通常,平臺自適應性存在兩種情況

  1. 屬於作業系統環境行為的事物(例如文字編輯和滾動),如果採用不同的行為則會被視為“錯誤”。
  2. 傳統上使用 OEM SDK 在應用中實現的事物(例如在 iOS 上使用平行標籤頁,或在 Android 上顯示 android.app.AlertDialog)。

本文主要涵蓋 Flutter 在 Android 和 iOS 上針對第 1 種情況提供的自動適配功能。

對於第 2 種情況,Flutter 捆綁了實現相應平臺約定效果的方法,但在需要應用設計選擇時不會自動適配。相關討論,請參閱 issue #8410 以及 Material/Cupertino 自適應元件問題定義

關於在 Android 和 iOS 上使用不同資訊架構結構但共享相同內容程式碼的應用示例,請參閱 platform_design 程式碼示例

#

Flutter 提供了 Android 和 iOS 上常見的導航模式,並會自動根據當前平臺調整導航動畫。

#

Android 上,預設的 Navigator.push() 轉場效仿了 startActivity(),它通常具有一種自下而上的動畫變體。

iOS

  • 預設的 Navigator.push() API 會產生一種 iOS 顯示/推送樣式轉場,它根據區域設定的 RTL 設定從結束到開始方向進行動畫處理。新路由背後的頁面也會像在 iOS 中一樣向相同方向視差滑動。
  • 當推送一個 PageRoute.fullscreenDialog 為 true 的頁面路由時,存在一種單獨的自下而上的轉場樣式。這代表了 iOS 的呈現/模態樣式轉場,通常用於全屏模態頁面。
An animation of the bottom-up page transition on Android

Android 頁面轉場

An animation of the end-start style push page transition on iOS

iOS 推送轉場

An animation of the bottom-up style present page transition on iOS

iOS 呈現轉場

平臺特定的轉場詳情

#

Android 上,Flutter 使用 ZoomPageTransitionsBuilder 動畫。當用戶點選專案時,UI 會縮放到包含該專案的螢幕。當用戶點選返回時,UI 會縮回上一個螢幕。

iOS 上使用推送樣式轉場時,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

靜態過度滾動對比

捲軸

#

基於 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 點選狀態列返回頂部

排版

#

使用 Material 包時,排版會自動預設使用適合該平臺的字體系列。Android 使用 Roboto 字型。iOS 使用 San Francisco 字型。

使用 Cupertino 包時,預設主題使用 San Francisco 字型。

San Francisco 字型的許可協議限制其僅在 iOS、macOS 或 tvOS 上執行的軟體中使用。因此,如果平臺被除錯覆蓋為 iOS 或使用預設的 Cupertino 主題,在 Android 上執行時將使用備用字型。

您可以選擇調整 Material 元件的文字樣式,以匹配 iOS 上的預設文字樣式。您可以在 UI 元件部分中檢視針對具體元件的示例。

Roboto font typography scale on Android

Android 上的 Roboto

San Francisco typography scale on iOS

iOS 上的 San Francisco

圖示設計

#

使用 Material 包時,某些圖示會自動根據平臺顯示不同的圖形。例如,溢位選單按鈕的三個點在 iOS 上是水平排列的,而在 Android 上則是垂直排列的。返回按鈕在 iOS 上是一個簡單的 V 形符號(chevron),而在 Android 上則帶有一個杆/軸。

Android appropriate icons

Android 上的圖示

iOS appropriate icons

iOS 上的圖示

Material 庫還透過 Icons.adaptive 提供了一組平臺自適應圖示。

觸覺反饋

#

Material 和 Cupertino 包在特定場景下會自動觸發適合平臺的觸覺反饋。

例如,透過長按文字欄位選擇單詞會觸發 Android 上的“蜂鳴”振動,而 iOS 上則不會。

在 iOS 上滾動選擇器專案會觸發“輕微撞擊”反饋,而 Android 上沒有反饋。

文字編輯

#

Material 和 Cupertino 文字輸入欄位均支援拼寫檢查,並會適配使用適合該平臺的拼寫檢查配置,以及適當的拼寫檢查選單和高亮顏色。

Flutter 在編輯文字欄位內容時也會進行以下調整,以匹配當前平臺。

鍵盤手勢導航

#

Android 上,可以在軟鍵盤的 空格 鍵上進行水平滑動,以在 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 元件以在 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 上,Apple 的人機介面指南 (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 上,Apple 的人機介面指南 (HIG) 中定義了一個名為“標籤欄”的等效元件。

Bottom Navigation Bar in Material 3

Material 3 中的底部導航欄

Tab Bar in Human Interface Guidelines

人機介面指南中的標籤欄

由於標籤欄在整個應用中都是持久存在的,它們應該符合您自己的品牌風格。但是,如果您選擇在 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 的 人機介面指南 (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 關於文字欄位的討論。您可以在討論中留下反饋或提出問題。