Skip to main content

自动平台适配

适配理念

#

通常,平台自适应存在两种情况:

  1. 操作系统环境的行为(例如文本编辑和滚动),如果采用不同的行为则会“出错”。
  2. 通常使用 OEM 的 SDK 在应用中实现的功能(例如在 iOS 上使用并行选项卡或在 Android 上显示 android.app.AlertDialog)。

本文主要介绍 Flutter 在 Android 和 iOS 上针对情况 1 提供的自动适配。

对于情况 2,Flutter 提供了产生平台约定适当效果的方法,但在需要应用设计选择时不会自动适应。有关讨论,请参阅问题 #8410Material/Cupertino 自适应小部件问题定义

有关在 Android 和 iOS 上使用不同信息架构结构但共享相同内容代码的应用程序示例,请参阅platform_design 代码示例

页面导航

#

Flutter 提供了在 Android 和 iOS 上看到的导航模式,并且还自动将导航动画适配到当前平台。

导航过渡

#

Android上,默认的Navigator.push()过渡是根据startActivity()建模的,它通常具有一个自下而上的动画变体。

iOS上:

  • 默认的Navigator.push() API 产生 iOS Show/Push 风格的过渡,根据区域设置的 RTL 设置从末端到起始端进行动画处理。新路由后面的页面也以与 iOS 中相同的方向进行视差滑动。
  • 当推送页面路由时,PageRoute.fullscreenDialog 为真,则存在单独的自下而上的过渡样式。这表示 iOS 的 Present/Modal 风格过渡,通常用于全屏模态页面。
Android 自下而上的页面过渡动画
Android 页面过渡
iOS 从末端到起始端的页面推送过渡动画
iOS 推送过渡
iOS 自下而上的页面呈现过渡动画
iOS 呈现过渡

平台特定的过渡细节

#

Android上,Flutter 使用ZoomPageTransitionsBuilder 动画。当用户点击一个项目时,UI 会放大到显示该项目的屏幕。当用户点击返回时,UI 会缩小到之前的屏幕。

iOS上,当使用推送样式过渡时,Flutter 的捆绑CupertinoNavigationBarCupertinoSliverNavigationBar 导航栏会自动将每个子组件动画到下一页或上一页的CupertinoNavigationBarCupertinoSliverNavigationBar上的相应子组件。

Android 页面过渡动画
Android
iOS 页面过渡期间导航栏过渡动画
iOS 导航栏

返回导航

#

Android上,操作系统返回按钮默认情况下会发送到 Flutter 并弹出WidgetsApp 的 Navigator 的顶部路由。

iOS上,可以使用边缘滑动手势弹出顶部路由。

Android 返回按钮触发的页面过渡
Android 返回按钮
iOS 返回滑动手势触发的页面过渡
iOS 返回滑动手势

滚动

#

滚动是平台外观和感觉的重要组成部分,Flutter 会自动调整滚动行为以匹配当前平台。

物理模拟

#

Android 和 iOS 都具有复杂的滚动物理模拟,难以用语言描述。通常,iOS 的可滚动内容重量更大,动态摩擦力更大,但 Android 的静态摩擦力更大。因此,iOS 的速度逐渐提高,但停止时不会突然停止,并且在低速时更流畅。

轻微抛掷,iOS 可滚动内容在较低速度下滑动更长距离,而 Android 则不是
轻微抛掷对比
中等力度抛掷,Android 可滚动内容速度更快,到达更远距离后更突然地停止
中等抛掷对比
大力抛掷,Android 可滚动内容速度更快,到达的距离也明显更远
大力抛掷对比

过度滚动行为

#

Android上,滚动到可滚动区域的边缘之外会显示一个过度滚动辉光指示器(基于当前 Material 主题的颜色)。

iOS上,滚动到可滚动区域的边缘之外会过度滚动,阻力越来越大,然后回弹。

Android 和 iOS 可滚动内容抛掷到边缘之外,并显示平台特定的过度滚动行为
动态过度滚动对比
Android 和 iOS 可滚动内容从静止位置过度滚动,并显示平台特定的过度滚动行为
静态过度滚动对比

动量

#

iOS上,在同一方向上重复抛掷会叠加动量,每次连续抛掷都会增加速度。Android 上没有类似的行为。

iOS 上重复滚动抛掷累积动量
iOS 滚动动量

返回顶部

#

iOS上,点击操作系统状态栏会将主滚动控制器滚动到顶部位置。Android 上没有类似的行为。

点击状态栏将主要可滚动内容滚动回顶部

iOS 状态栏点击返回顶部

排版

#

使用 Material 包时,排版会自动默认为适合平台的字体系列。Android 使用 Roboto 字体。iOS 使用 San Francisco 字体。

使用 Cupertino 包时,默认主题 使用 San Francisco 字体。

San Francisco 字体的许可证将其使用限制在仅在 iOS、macOS 或 tvOS 上运行的软件中。因此,如果平台被调试覆盖到 iOS 或使用默认 Cupertino 主题,则在 Android 上运行时会使用备用字体。

您可能选择调整 Material 小部件的文本样式以匹配 iOS 上的默认文本样式。您可以在UI 组件部分 中看到特定于小部件的示例。

Android 上的 Roboto 字体
Android 上的 Roboto
iOS 上的 San Francisco 字体
iOS 上的 San Francisco

图标

#

使用 Material 包时,某些图标会根据平台自动显示不同的图形。例如,溢出按钮的三个点在 iOS 上是水平的,在 Android 上是垂直的。返回按钮在 iOS 上是一个简单的 Chevron,在 Android 上有一个柄/轴。

适合 Android 的图标
Android 上的图标
适合 iOS 的图标
iOS 上的图标

Material 库还通过Icons.adaptive 提供了一组平台自适应图标。

触觉反馈

#

Material 和 Cupertino 包会在某些情况下自动触发平台合适的触觉反馈。

例如,通过文本字段长按选择文字会在 Android 上触发“嗡嗡”振动,而在 iOS 上不会。

在 iOS 上滚动选择器项目会触发“轻微冲击”敲击声,而在 Android 上则没有反馈。

文本编辑

#

Material 和 Cupertino 文本输入字段都支持拼写检查,并适应使用平台正确的拼写检查配置,以及正确的拼写检查菜单和高亮颜色。

Flutter还在编辑文本字段内容时进行以下调整以匹配当前平台。

键盘手势导航

#

Android上,可以在软键盘的空格键上进行水平滑动以在 Material 和 Cupertino 文本字段中移动光标。

在具有 3D Touch 功能的iOS设备上,可以在软键盘上进行用力按压拖动操作以通过浮动光标在二维空间中移动光标。这适用于 Material 和 Cupertino 文本字段。

通过 Android 上的空格键移动光标
Android 空格键移动光标
通过 iOS 上的 3D Touch 拖动键盘移动光标
iOS 3D Touch 拖动移动光标

文本选择工具栏

#

使用Android 上的 Material时,当在文本字段中进行文本选择时,会显示 Android 风格的选择工具栏。

使用iOS 上的 Material 或使用 Cupertino时,当在文本字段中进行文本选择时,会显示 iOS 风格的选择工具栏。

适合 Android 的文本工具栏
Android 文本选择工具栏
适合 iOS 的文本工具栏
iOS 文本选择工具栏

单击手势

#

使用Android 上的 Material时,在文本字段中单击一下会将光标放在点击的位置。

折叠的文本选择还会显示一个可拖动的句柄,以便随后移动光标。

使用iOS 上的 Material 或使用 Cupertino时,在文本字段中单击一下会将光标放在点击单词的最近边缘。

iOS 上的折叠文本选择没有可拖动的句柄。

在 Android 上将光标移动到点击的位置
Android 点击
在 iOS 上将光标移动到点击单词的最近边缘
iOS 点击

长按手势

#

使用Android 上的 Material时,长按会选择长按下的单词。释放时会显示选择工具栏。

使用iOS 上的 Material 或使用 Cupertino时,长按会将光标放在长按的位置。释放时会显示选择工具栏。

通过 Android 上的长按选择单词
Android 长按
通过 iOS 上的长按选择位置
iOS 长按

长按拖动手势

#

使用Android 上的 Material时,在按住长按的同时拖动会扩展所选单词。

使用iOS 上的 Material 或使用 Cupertino时,在按住长按的同时拖动会移动光标。

通过 Android 上的长按拖动扩展单词选择
Android 长按拖动
通过 iOS 上的长按拖动移动光标
iOS 长按拖动

双击手势

#

在 Android 和 iOS 上,双击都会选择接收双击的单词并显示选择工具栏。

通过 Android 上的双击选择单词
Android 双击
通过 iOS 上的双击选择单词
iOS 双击

UI 组件

#

本节包含关于如何调整 Material 小部件以在 iOS 上提供自然且引人注目的体验的初步建议。欢迎您在问题 #8427 上提供反馈。

带有 .adaptive() 构造函数的小部件

#

一些小部件支持.adaptive() 构造函数。下表列出了这些小部件。当应用在 iOS 设备上运行时,自适应构造函数会替换相应的 Cupertino 组件。

下表中的小部件主要用于输入、选择和显示系统信息。由于这些控件与操作系统紧密集成,因此用户已经接受了对它们的识别和响应。因此,我们建议您遵循平台约定。

Material 小部件Cupertino 小部件自适应构造函数
Material 3 中的开关
Switch
HIG 中的开关
CupertinoSwitch
Switch.adaptive()

|Material 3 中的滑块
Slider|HIG 中的滑块
CupertinoSlider|Slider.adaptive()| |Material 3 中的圆形进度指示器
CircularProgressIndicator|HIG 中的活动指示器
CupertinoActivityIndicator|CircularProgressIndicator.adaptive()| | Material 3 中的复选框
Checkbox| HIG 中的复选框
CupertinoCheckbox|Checkbox.adaptive()| |Material 3 中的单选按钮
Radio|HIG 中的单选按钮
CupertinoRadio|Radio.adaptive()| |Material 3 中的 AlertDialog
AlertDialog|HIG 中的 AlertDialog
CupertinoAlertDialog|AlertDialog.adaptive()|

顶部应用栏和导航栏

#

从 Android 12 开始,顶部应用栏的默认 UI 遵循Material 3 中定义的设计指南。在 iOS 上,Apple 的人类界面指南 (HIG) 中定义了一个名为“导航栏”的等效组件。

Material 3 中的顶部应用栏
Material 3 中的顶部应用栏
人类界面指南中的导航栏
人类界面指南中的导航栏

Flutter 应用中应用栏的某些属性应该进行调整,例如系统图标和页面过渡。使用 Material AppBarSliverAppBar 小部件时,这些属性会自动进行调整。您还可以进一步自定义这些小部件的属性,以更好地匹配 iOS 平台样式,如下所示。

dart
// 将文本主题映射到 iOS 样式
TextTheme cupertinoTextTheme = TextTheme(
    headlineMedium: CupertinoThemeData()
        .textTheme
        .navLargeTitleTextStyle
         // 修复间距的小错误
        .copyWith(letterSpacing: -1.5),
    titleLarge: CupertinoThemeData().textTheme.navTitleTextStyle)
...

// 在 iOS 设备上使用 iOS 文本主题
ThemeData(
      textTheme: Platform.isIOS ? cupertinoTextTheme : null,
      ...
)
...

// 修改 AppBar 属性
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) 中定义了一个名为“标签栏”的等效组件。

Material 3 中的底部导航栏
Material 3 中的底部导航栏
人类界面指南中的标签栏
人类界面指南中的标签栏

由于标签栏在您的应用中是持久存在的,因此它们应该与您的品牌相匹配。但是,如果您选择在 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) 定义了一个等效组件。

Material 3 中的文本字段
Material 3 中的文本字段
人类界面指南中的文本字段
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 讨论。您可以在讨论中留下反馈或提出问题。