跳到主内容

自动平台适配

了解更多关于 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 上关于文本字段的讨论。您可以在讨论中留下反馈或提问。