Adaptive Layout — 一种界面布局方法,应用程序针对不同类型的设备和屏幕方向使用不同的布局。与 responsive design 不同,adaptive layout 在断点处切换预先准备好的布局。本文分析了 iOS 中的 Size Classes 和 UITraitCollection,Android 中的 sw600dp/layout-w600dp 限定符,并提供了实际示例。
要点
Adaptive Layout — 一种构建界面的策略,开发人员为每种设备类型创建单独的布局,并根据屏幕特征切换不同的布局。与 responsive design 不同,responsive design 中元素平滑流动,而 adaptive layout 使用离散切换:手机 — 一种布局,平板 — 另一种,桌面 — 第三种。
苹果在 iOS 8(2014 年)中随通用应用(universal binaries,可在 iPhone 和 iPad 上运行)一起引入了 Size Classes。谷歌在 Android 3.2(2011 年)中引入了 smallestWidth(sw
Apple HIG 和 Google Material Design 的现代建议一致认为,对于支持多种屏幕尺寸的应用程序,adaptive layout 是必需的。Google Play 将缺少对平板的适配标记为缺陷。App Store 没有正式要求,但 iPad 用户期望原生体验,而不是拉伸的 iPhone 应用。
Size Classes — iOS 的一项特性,将每个轴上的可用空间定义为 Compact 或 Regular。UITraitCollection — iOS 用于通过视图层次结构传递环境特征的系统:尺寸、屏幕比例、force touch capability、色域(light/dark)、accessibility settings。
| 设备 / 方向 | Horizontal | Vertical |
|---|---|---|
| iPhone 竖屏(所有型号) | Compact | Regular |
| iPhone Plus/Pro Max 横屏 | Regular | Compact |
| iPhone SE / mini 横屏 | Compact | Compact |
| iPad 竖屏 | Regular | Regular |
| iPad 横屏 | Regular | Regular |
| iPad Split View(1/3 屏幕) | Compact | Regular |
UITraitCollection 由系统创建,并从 UIApplication 通过 UIWindow 传递给每个 UIView。当方向或窗口大小(Split View)发生变化时,系统会生成新的 UITraitCollection 并调用 traitCollectionDidChange。在 SwiftUI 中,更改通过 Environment Values 跟踪并自动重绘视图。
在 UIKit 中通过 UITraitCollection 进行自适应的实现方式是订阅 trait collection 更改并重建布局。下面的示例展示了一个控制器,它在 iPhone 的垂直堆栈和 iPad 的分割布局之间切换。
// 使用 Size Classes 的自适应控制器
import UIKit
final class AdaptiveViewController: UIViewController {
private let compactStack = UIStackView()
private let regularStack = UIStackView()
private let sidebar = UIView()
private let content = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
updateLayout(for: traitCollection)
}
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.horizontalSizeClass
!= previousTraitCollection?.horizontalSizeClass {
updateLayout(for: traitCollection)
}
}
private func updateLayout(for traits: UITraitCollection) {
if traits.horizontalSizeClass == .regular {
showRegularLayout() // iPad:侧边栏 + 内容
} else {
showCompactLayout() // iPhone:标签栏 + push
}
}
private func showRegularLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
regularStack.addArrangedSubview(sidebar)
regularStack.addArrangedSubview(content)
regularStack.frame = view.bounds
regularStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(regularStack)
}
private func showCompactLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
compactStack.addArrangedSubview(content)
compactStack.frame = view.bounds
compactStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(compactStack)
}
private func setupViews() {
sidebar.backgroundColor = .systemGray6
content.backgroundColor = .systemBackground
compactStack.axis = .vertical
regularStack.axis = .horizontal
}
}traitCollectionDidChange 方法在 traits 发生任何更改时调用,但我们只检查 horizontalSizeClass — 更改布局的最重要指标。在 iOS 17+ 中,建议使用 UIViewController.horizontalSizeClass 作为计算属性来进行反应式更新,而无需订阅更改。
Android 提供两个主要的宽度限定符:sw
| 限定符 | 条件 | 设备示例 |
|---|---|---|
| layout-sw600dp | 最小宽度 ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | 当前宽度 ≥ 600 dp | Split View 中的 iPad (600 dp), 横屏手机 |
| layout-land | 横屏方向 | 任何水平旋转的设备 |
| layout-sw720dp | 最小宽度 ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Android 中的资源层次结构:res/layout/activity_main.xml(手机),res/layout-sw600dp/activity_main.xml(7 英寸平板),res/layout-w600dp-land/activity_main.xml(横屏手机)。Android 选择最适合当前设备配置的最具体的限定符。如果没有合适的文件,则使用 res/layout/ 中的基本资源。
现代 Android 开发使用 Jetpack WindowManager 来确定窗口大小,而不是直接读取资源。这对于可折叠设备尤其重要,因为屏幕尺寸可能会动态变化(Galaxy Fold 从 6.2 英寸展开到 7.6 英寸)。
// 通过 Jetpack WindowManager 进行自适应
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AdaptiveScreen() {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
FlowRow(
verticalAlignment = Arrangement.Top,
horizontalArrangement = when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.EXPANDED -> Arrangement.SpaceEvenly
else -> Arrangement.Start
}
) {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
CompactContent()
}
WindowWidthSizeClass.MEDIUM -> {
MediumContent()
}
WindowWidthSizeClass.EXPANDED -> {
ExpandedContent()
}
}
}
}
@Composable
private fun CompactContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text("手机", style = MaterialTheme.typography.headlineSmall)
ListContent()
}
}
@Composable
private fun MediumContent() {
Row(modifier = Modifier.padding(24.dp)) {
NavigationRail { ... }
Column { ListContent() }
}
}
@Composable
private fun ExpandedContent() {
Row(modifier = Modifier.padding(32.dp)) {
PermanentNavigationDrawer { ... }
Column {
ListContent()
}
DetailPane()
}
}来自 androidx.window 库的 currentWindowAdaptiveInfo() 组件使界面适应任何设备:手机、平板、可折叠、ChromeOS。宽度类:COMPACT(0–600 dp),MEDIUM(600–840 dp),EXPANDED(>840 dp)。高度类:COMPACT(0–480 dp),MEDIUM(480–900 dp),EXPANDED(>900 dp)。
SwiftUI 提供了用于自适应布局的内置工具,无需直接使用 Size Classes。AnyLayout、ViewThatFits 和 GeometryReader 允许构建自动适应可用空间的界面。
// 通过 AnyLayout 和 ViewThatFits 进行 SwiftUI 自适应
import SwiftUI
struct AdaptiveGrid: View {
@Environment(\.horizontalSizeClass) private var hSizeClass
var body: some View {
let layout = hSizeClass == .regular
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Label("收藏", systemImage: "star")
Label("最近", systemImage: "clock")
Label("设置", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — 自动选择布局
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // 优先级 1:水平
CardView()
CardView()
}
VStack { // 如果放不下 — 垂直
CardView()
CardView()
}
}
}
}ViewThatFits — 一个强大的 SwiftUI 工具,可自动选择适合可用空间且不会被裁剪的第一个子视图。这消除了为简单的自适应切换而显式检查 Size Classes 的需要。AnyLayout 允许改变布局类型(HStack/VStack),而无需在视图主体中使用 if-else。
Jetpack Compose 使用 WindowSizeClass(来自 Material 3 adaptive 库)和 BoxWithConstraints 实现自适应布局。与 XML 限定符不同,Compose 在运行时确定窗口大小,并在配置更改时响应式地重新组合 UI。
// 通过 BoxWithConstraints 进行 Jetpack Compose 自适应
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// 平板:两列(list-detail)
Row(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.weight(1f)) {
items(items) { item ->
ListItem(text = item)
}
}
var selected by remember { mutableStateOf(items.first()) }
DetailPanel(item = selected)
.weight(2f)
}
} else {
// 手机:带导航的单列
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
ListItem(text = item, onClick = { navigateToDetail(item) })
}
}
}
}
}
@Composable
fun AdaptiveListMaterial3() {
val windowClass = currentWindowAdaptiveInfo().windowSizeClass
AdaptiveLayout(
layout = windowClass.windowWidthSizeClass
) {
when (windowClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
else -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
}
}
}Material 3 Adaptive Layouts 提供现成的组件:ListDetailScaffold、SupportingPaneScaffold、NavigationSuiteScaffold。这些组件会自动适应 WindowWidthSizeClass — 在 stacked(COMPACT)和 side-by-side(MEDIUM/EXPANDED)之间切换。开发人员只需选择模式并传递内容面板即可。
常见问题
sw600dp(smallestWidth)— 屏幕最小宽度,与方向无关。始终在 7 英寸以上的平板上触发。w600dp(available width)— 考虑当前方向的可用宽度。在横屏模式下的手机上触发,这时宽度可能超过 600 dp。
UITraitCollection — 一个 iOS 对象,包含环境特征:size classes(horizontal/vertical)、display scale、force touch capability、user interface idiom(iPhone/iPad)、color gamut、accessibility settings。它从 UIApplication 通过视图层次结构传递,并在方向或窗口大小改变时更新。
使用 Jetpack WindowManager(Android)和 UIScreen nativeBounds + traitCollection(iOS)。可折叠设备在展开时会更改 Size Class。建议采用基于规范布局(list-detail、supporting pane)的架构,并反应式订阅窗口大小变化,而无需重启 Activity。
不需要。使用具有 Size Classes 约束变体的单个 storyboard 或通过 UIKit 进行编程布局。SwiftUI 完全不需要 storyboard。为 iPad 创建单独的 storyboard 会导致重复并增加维护难度 — 最好通过 Size Classes 调整单个布局。
Canonical layouts — Google 提供的现成自适应布局模式:list-detail(列表+详情)、supporting pane(内容+操作面板)、feed(卡片)。每种模式都支持三种状态(compact/medium/expanded),并在 Material 3 Adaptive 库中通过 ListDetailScaffold 和 SupportingPaneScaffold 实现。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。