自动平台适配
适配理念
#通常,平台适配性存在两种情况
- 属于操作系统环境行为的事物(如文本编辑和滚动),如果出现不同的行为,那将是“错误”的。
- 在应用程序中通常使用 OEM SDK 实现的事物(例如在 iOS 上使用并行标签或在 Android 上显示
android.app.AlertDialog
)。
本文主要介绍 Flutter 在 Android 和 iOS 上提供的第一种情况的自动适配。
对于第二种情况,Flutter 提供了实现平台约定效果的方法,但在需要应用程序设计选择时不会自动适配。有关讨论,请参阅问题 #8410 和 Material/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 风格转场,通常用于全屏模态页面。



平台特有的转场细节
#在 Android 上,Flutter 使用 ZoomPageTransitionsBuilder
动画。当用户点击一个项目时,UI 会放大到显示该项目的屏幕。当用户点击返回时,UI 会缩小回上一个屏幕。
在 iOS 上使用推入样式转场时,Flutter 内置的 CupertinoNavigationBar
和 CupertinoSliverNavigationBar
导航栏会自动将其每个子组件动画化到下一页或上一页的 CupertinoNavigationBar
或 CupertinoSliverNavigationBar
上的相应子组件。


返回导航
#在 Android 上,操作系统返回按钮默认会发送给 Flutter,并弹出 WidgetsApp
的导航器上的顶部路由。
在 iOS 上,可以使用边缘滑动手势来弹出顶部路由。


滚动
#滚动是平台外观和体验的重要组成部分,Flutter 会自动调整滚动行为以匹配当前平台。
物理模拟
#Android 和 iOS 都具有复杂的滚动物理模拟,这些模拟难以用语言描述。通常,iOS 的可滚动元素具有更大的重量和动态摩擦力,而 Android 具有更大的静态摩擦力。因此,iOS 速度提升更平缓,但停止不那么突然,并且在低速时更滑。



过度滚动行为
#在 Android 上,滚动到可滚动区域边缘之外会显示一个过度滚动发光指示器(基于当前 Material 主题的颜色)。
在 iOS 上,滚动到可滚动区域边缘之外会以增加的阻力过度滚动并弹回。


惯性
#在 iOS 上,向同一方向重复滑动会累积惯性,并随着每次连续滑动而增加速度。Android 上没有类似的行为。

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

排版
#使用 Material 包时,排版会自动默认为适合平台的字体系列。Android 使用 Roboto 字体。iOS 使用 San Francisco 字体。
使用 Cupertino 包时,默认主题使用 San Francisco 字体。
San Francisco 字体的许可仅限于在 iOS、macOS 或 tvOS 上运行的软件。因此,如果在 Android 上运行时,如果平台被调试覆盖为 iOS 或使用默认的 Cupertino 主题,则会使用备用字体。
您可以选择调整 Material 组件的文本样式以匹配 iOS 上的默认文本样式。您可以在UI 组件部分中查看特定组件的示例。


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


Material 库还通过 Icons.adaptive
提供了一组平台自适应图标。
触觉反馈
#Material 和 Cupertino 包在某些情况下会自动触发适合平台的触觉反馈。
例如,通过文本输入框长按选择单词会在 Android 上触发“嗡嗡”振动,而在 iOS 上不会。
在 iOS 上滚动选择器项目会触发“轻微冲击”敲击,而在 Android 上没有反馈。
文本编辑
#Material 和 Cupertino 文本输入字段都支持拼写检查,并适配使用适合平台的拼写检查配置,以及适当的拼写检查菜单和高亮颜色。
Flutter 在编辑文本输入框内容时也会进行以下适配,以匹配当前平台。
键盘手势导航
#在 Android 上,可以在软键盘的 空格 键上进行水平滑动,以在 Material 和 Cupertino 文本输入框中移动光标。
在具有 3D Touch 功能的 iOS 设备上,可以在软键盘上进行强制按压拖动手势,通过浮动光标在 2D 空间中移动光标。这适用于 Material 和 Cupertino 文本输入框。


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


单点手势
#在 Android 上的 Material 中,在文本输入框中单击会将光标放置在点击位置。
折叠的文本选择也会显示一个可拖动的句柄,以便后续移动光标。
在 iOS 上的 Material 或使用 Cupertino 时,在文本输入框中单击会将光标放置在点击单词的最近边缘。
iOS 上折叠的文本选择没有可拖动的句柄。


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


长按拖动手势
#在 Android 上的 Material 中,在长按时拖动会扩展选定的单词。
在 iOS 上的 Material 或使用 Cupertino 时,在长按时拖动会移动光标。


双击手势
#在 Android 和 iOS 上,双击会选择双击处的单词并显示选择工具栏。


UI 组件
#本节包含关于如何适配 Material 组件以在 iOS 上提供自然且引人注目的体验的初步建议。欢迎在问题 #8427 上提供您的反馈。
带 .adaptive() 构造函数的组件
#有几个组件支持 .adaptive()
构造函数。下表列出了这些组件。当应用程序在 iOS 设备上运行时,自适应构造函数会替换相应的 Cupertino 组件。
下表中的组件主要用于输入、选择和显示系统信息。由于这些控件与操作系统紧密集成,用户已经习惯识别和响应它们。因此,我们建议您遵循平台约定。
Material 组件 | Cupertino 组件 | 自适应构造函数 |
---|---|---|
![]() Switch | ![]() CupertinoSwitch | Switch.adaptive() |
![]() Slider | ![]() CupertinoSlider | Slider.adaptive() |
![]() CircularProgressIndicator | ![]() CupertinoActivityIndicator | CircularProgressIndicator.adaptive() |
![]() RefreshProgressIndicator | ![]() CupertinoActivityIndicator | RefreshIndicator.adaptive() |
![]() Checkbox | ![]() CupertinoCheckbox | Checkbox.adaptive() |
![]() Radio | ![]() CupertinoRadio | Radio.adaptive() |
![]() AlertDialog | ![]() CupertinoAlertDialog | AlertDialog.adaptive() |
顶部应用栏和导航栏
#自 Android 12 以来,顶部应用栏的默认 UI 遵循 Material 3 中定义的设计指南。在 iOS 上,Apple 的人机界面指南 (HIG) 中定义了一个名为“导航栏 (Navigation Bars)”的等效组件。


Flutter 应用程序中应用栏的某些属性应进行适配,例如系统图标和页面转场。在使用 Material AppBar
和 SliverAppBar
组件时,这些属性已自动适配。您还可以进一步自定义这些组件的属性,以更好地匹配 iOS 平台样式,如下所示。
// 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) 中定义了一个名为“标签栏 (Tab Bars)”的等效组件。


由于标签栏在您的应用程序中是持久存在的,它们应该与您自己的品牌保持一致。但是,如果您选择在 Android 上使用 Material 的默认样式,您可能需要考虑适配到 iOS 的默认标签栏。
要实现平台特定的底部导航栏,您可以在 Android 上使用 Flutter 的 NavigationBar
组件,在 iOS 上使用 CupertinoTabBar
组件。下面是一个您可以适配以显示平台特定导航栏的代码片段。
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) 定义了一个等效组件。


由于文本输入框需要用户输入,
它们的设计应遵循平台约定。
要在 Flutter 中实现平台特定的 TextField
,您可以适配 Material TextField
的样式。
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 讨论。您可以在讨论中留下反馈或提问。