Adaptive Layout:关键概念、尺寸类别和限定符

作者: IT Sectr 发布日期: 2026-02-26 阅读时间: 10 分钟

Adaptive Layout — 一种界面布局方法,应用程序针对不同类型的设备和屏幕方向使用不同的布局。与 responsive design 不同,adaptive layout 在断点处切换预先准备好的布局。本文分析了 iOS 中的 Size Classes 和 UITraitCollection,Android 中的 sw600dp/layout-w600dp 限定符,并提供了实际示例。

要点

  • Adaptive Layout — 根据屏幕尺寸、方向和设备类型离散切换布局
  • Size Classes 在 iOS 中将空间按水平和垂直方向分类为 Compact 或 Regular
  • UITraitCollection — iOS 环境特征的容器(尺寸、比例、force touch、色域)
  • sw600dp — 针对平板电脑的 Android 最小宽度限定符(7 英寸及以上)
  • layout-w600dp — 可用宽度限定符,在当前方向有足够空间时触发

什么是 Adaptive Layout?

Adaptive Layout — 一种构建界面的策略,开发人员为每种设备类型创建单独的布局,并根据屏幕特征切换不同的布局。与 responsive design 不同,responsive design 中元素平滑流动,而 adaptive layout 使用离散切换:手机 — 一种布局,平板 — 另一种,桌面 — 第三种。

苹果在 iOS 8(2014 年)中随通用应用(universal binaries,可在 iPhone 和 iPad 上运行)一起引入了 Size Classes。谷歌在 Android 3.2(2011 年)中引入了 smallestWidth(swdp)限定符。这两种机制都解决了同一个问题:为开发人员提供一种工具,用于为不同的屏幕创建不同的 UI,而无需重复代码。

Apple HIG 和 Google Material Design 的现代建议一致认为,对于支持多种屏幕尺寸的应用程序,adaptive layout 是必需的。Google Play 将缺少对平板的适配标记为缺陷。App Store 没有正式要求,但 iPad 用户期望原生体验,而不是拉伸的 iPhone 应用。

iOS Size Classes 和 UITraitCollection

Size Classes — iOS 的一项特性,将每个轴上的可用空间定义为 Compact 或 Regular。UITraitCollection — iOS 用于通过视图层次结构传递环境特征的系统:尺寸、屏幕比例、force touch capability、色域(light/dark)、accessibility settings。

设备 / 方向HorizontalVertical
iPhone 竖屏(所有型号)CompactRegular
iPhone Plus/Pro Max 横屏RegularCompact
iPhone SE / mini 横屏CompactCompact
iPad 竖屏RegularRegular
iPad 横屏RegularRegular
iPad Split View(1/3 屏幕)CompactRegular

UITraitCollection 由系统创建,并从 UIApplication 通过 UIWindow 传递给每个 UIView。当方向或窗口大小(Split View)发生变化时,系统会生成新的 UITraitCollection 并调用 traitCollectionDidChange。在 SwiftUI 中,更改通过 Environment Values 跟踪并自动重绘视图。

Swift 中的自适应布局示例

在 UIKit 中通过 UITraitCollection 进行自适应的实现方式是订阅 trait collection 更改并重建布局。下面的示例展示了一个控制器,它在 iPhone 的垂直堆栈和 iPad 的分割布局之间切换。

swift
// 使用 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 限定符:sw600dp 和 w600dp

Android 提供两个主要的宽度限定符:swdp(smallestWidth)和 wdp(available width)。区别至关重要:sw — 屏幕最小宽度,与方向无关;w — 考虑当前方向的可用宽度。sw 保证布局会为物理上具有指定最小宽度的设备加载。w 响应旋转 — 在横屏模式下可以触发 w600dp,即使在竖屏模式下宽度为 360 dp。

限定符条件设备示例
layout-sw600dp最小宽度 ≥ 600 dpiPad (768 dp), Pixel C (900 dp)
layout-w600dp当前宽度 ≥ 600 dpSplit View 中的 iPad (600 dp), 横屏手机
layout-land横屏方向任何水平旋转的设备
layout-sw720dp最小宽度 ≥ 720 dpiPad 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/ 中的基本资源。

Kotlin 中的自适应布局示例

现代 Android 开发使用 Jetpack WindowManager 来确定窗口大小,而不是直接读取资源。这对于可折叠设备尤其重要,因为屏幕尺寸可能会动态变化(Galaxy Fold 从 6.2 英寸展开到 7.6 英寸)。

kotlin
// 通过 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 中的 Adaptive Layout

SwiftUI 提供了用于自适应布局的内置工具,无需直接使用 Size Classes。AnyLayout、ViewThatFits 和 GeometryReader 允许构建自动适应可用空间的界面。

swift
// 通过 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 中的 Adaptive Layout

Jetpack Compose 使用 WindowSizeClass(来自 Material 3 adaptive 库)和 BoxWithConstraints 实现自适应布局。与 XML 限定符不同,Compose 在运行时确定窗口大小,并在配置更改时响应式地重新组合 UI。

kotlin
// 通过 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 提供现成的组件:ListDetailScaffoldSupportingPaneScaffoldNavigationSuiteScaffold。这些组件会自动适应 WindowWidthSizeClass — 在 stacked(COMPACT)和 side-by-side(MEDIUM/EXPANDED)之间切换。开发人员只需选择模式并传递内容面板即可。

常见问题

sw600dp 和 w600dp 有什么区别?

sw600dp(smallestWidth)— 屏幕最小宽度,与方向无关。始终在 7 英寸以上的平板上触发。w600dp(available width)— 考虑当前方向的可用宽度。在横屏模式下的手机上触发,这时宽度可能超过 600 dp。

iOS 中的 UITraitCollection 是什么?

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。

是否需要为 iPad 创建单独的 storyboard?

不需要。使用具有 Size Classes 约束变体的单个 storyboard 或通过 UIKit 进行编程布局。SwiftUI 完全不需要 storyboard。为 iPad 创建单独的 storyboard 会导致重复并增加维护难度 — 最好通过 Size Classes 调整单个布局。

Material 3 中的 canonical layouts 是什么?

Canonical layouts — Google 提供的现成自适应布局模式:list-detail(列表+详情)、supporting pane(内容+操作面板)、feed(卡片)。每种模式都支持三种状态(compact/medium/expanded),并在 Material 3 Adaptive 库中通过 ListDetailScaffold 和 SupportingPaneScaffold 实现。

总结

  • Adaptive Layout — 在断点处为不同设备离散切换布局
  • Size Classes iOS(Compact/Regular)确定 iPhone、iPad 和 Split View 的布局
  • UITraitCollection — 通过视图层次结构传递环境特征的机制
  • sw600dp — 针对 7 英寸以上平板的 Android 最小宽度限定符
  • w600dp — 横屏旋转时触发的可用宽度限定符
  • Jetpack WindowManager 提供 WindowSizeClass(WindowWidthSizeClass:COMPACT/MEDIUM/EXPANDED)
  • Material 3 Adaptive — 具有自动适应功能的现成组件(ListDetailScaffold)

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读