跳到主內容

自動平臺適配

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

適配理念

#

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

  1. 那些是作業系統環境的行為(例如文字編輯和滾動),如果發生不同的行為,將會是“錯誤”的。
  2. 那些是使用 OEM 的 SDK 在應用程式中常規實現的內容(例如在 iOS 上使用並行選項卡,或在 Android 上顯示 android.app.AlertDialog)。

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

對於情況 2,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 推送過渡

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

軟 fling 比較

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

中 fling 比較

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

強 fling 比較

超滾動行為

#

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 上,同一方向上的重複 fling 會堆疊動量並隨著每次後續 fling 增加速度。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 上是一個簡單的向左箭頭,在 Android 上有一個莖/軸。

Android appropriate icons

Android 上的圖示

iOS appropriate icons

iOS 上的圖示

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

觸覺反饋

#

Material 和 Cupertino 包會自動觸發在某些情況下平臺適當的觸覺反饋。

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

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

文字編輯

#

Material 和 Cupertino 文字輸入欄位都支援拼寫檢查,並適應使用平臺的正確拼寫檢查配置以及正確的拼寫檢查選單和高亮顏色。

Flutter 還會在編輯文字欄位的內容時進行以下適配以匹配當前平臺。

鍵盤手勢導航

#

Android 上,可以在軟鍵盤的 space 鍵上進行水平滑動以在 Material 和 Cupertino 文字欄位中移動游標。

在具有 3D Touch 功能的 iOS 裝置上,可以在軟鍵盤上進行強制按壓拖動勢來透過浮動游標以 2D 方式移動游標。這適用於 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 拖動游標移動

文字選擇工具欄

#

使用 Material on Android 時,在文字欄位中進行文字選擇時會顯示 Android 樣式的選擇工具欄。

使用 Material on iOS 或使用 Cupertino 時,在文字欄位中進行文字選擇時會顯示 iOS 樣式的選擇工具欄。

Android appropriate text toolbar

Android 文字選擇工具欄

iOS appropriate text toolbar

iOS 文字選擇工具欄

單擊手勢

#

使用 Material on Android 時,在文字欄位中單擊會將游標放置在單擊的位置。

摺疊的文字選擇還會顯示一個可拖動的控制代碼以隨後移動游標。

使用 Material on iOS 或使用 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 點選

長按手勢

#

使用 Material on Android 時,長按會選擇長按下的單詞。釋放時會顯示選擇工具欄。

使用 Material on iOS 或使用 Cupertino 時,長按會將游標放置在長按的位置。釋放時會顯示選擇工具欄。

Selecting a word with long press on Android

Android 長按

Selecting a position with long press on iOS

iOS 長按

長按拖動手勢

#

使用 Material on Android 時,在按住長按的情況下拖動會擴充套件所選單詞。

使用 Material on iOS 或使用 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 上關於文字欄位的討論。您可以在討論中留下反饋或提問。