各位 Android 爱好者们,大家好。
多年来,不少 Android 项目都把屏幕方向当成了 UI 适配的主要依据。
例如下面这样的代码:
if (configuration.orientation == ORIENTATION_LANDSCAPE) {
LandscapeScreen()
} else {
PortraitScreen()
}
但问题是,这套做法越来越不够用了。
到了 2026 年 10 月,Android 设备的屏幕比例已经很丰富了,从 16:9、4:3,到接近 √2:1 的比例都有。
你的应用可能在平板上竖屏运行,也可能在横向显示器里占据一个狭窄的桌面窗口,或是与另一个应用分屏显示,又或是铺满折叠屏的内屏。设备方向已经无法告诉你,UI 实际能用多大的空间。
或者说,横竖屏的判断本身,已经不够用了!
Android 16 开始对以 API 36 为目标版本的应用,在符合条件的大屏上默认忽略方向、窗口大小调整和宽高比限制,推动应用适应不同窗口。
Android 17 则完成了这次过渡:当应用在 Android 17 上运行、且以 API 37 或更高版本为目标时,在最小宽度至少为 600dp 的显示屏上,系统会忽略屏幕方向、窗口可调整大小和宽高比限制。Android 16 提供的临时退出机制也被移除了。在这种环境下,平台可以忽略 screenOrientation、resizeableActivity、minAspectRatio、maxAspectRatio,以及传给 setRequestedOrientation() 的横竖屏方向值。通过 android:appCategory 标记的游戏和小于 sw600dp 的显示屏仍属例外。用户在设备宽高比设置中明确选择应用默认行为时,也属于例外。
这倒不是说横屏和竖屏这两种物理状态被废弃了,而是说,它们不再能作为应用布局的可靠前提。
替代思路其实很简单:
别再围绕设备方向设计布局,而要围绕应用当前拥有的窗口来设计。
到底变了什么
先区分两件事:API 被废弃,和系统忽略某项限制,并不是一回事。
下面的系统行为针对前述 Android 17、目标 API 和大屏条件:
| 过去的假设 | Android 17 在 sw600dp+ 大屏上的实际行为 |
|---|---|
screenOrientation="portrait" 能保证竖屏显示 | 系统可能忽略这项设置 |
resizeableActivity="false" 能阻止窗口大小调整 | 系统仍会将应用视为可调整大小的应用 |
maxAspectRatio 能让内容区域保持手机屏幕的比例 | 这项限制不再生效 |
setRequestedOrientation(...) 能控制大屏的显示方向 | 横屏和竖屏请求都可能被忽略 |
| 横屏意味着“宽度足够放下两个窗格” | 横屏应用窗口的可用宽度仍可能不足以放下两个窗格 |
| 竖屏意味着“单列的手机 UI” | 竖屏平板窗口也可能属于展开尺寸类别 |
对于以 API 36 为目标版本的应用,Android 16 默认采用上述行为,但也提供了一个临时兼容性属性。到了 Android 17,以 API 37 为目标版本的应用就不能再通过这个属性退出了。
所以,迁移的目标不是换一个控制屏幕方向的 API,而是采用自适应 UI 架构。
窗口、尺寸类别和窗格
大多数横竖屏判断,都可以用下面三个概念替代:
- 窗口(Window):系统实际分配给应用的区域,而不是物理显示屏。
- 窗口尺寸类别(Window size class):通过一组稳定的断点,对可用宽度和高度进行分类。
- 窗格(Pane):一个内容区域。在紧凑窗口中可以单独显示,在较大窗口中可以与相关内容并排显示。
为什么要这样区分?同一台平板,几秒钟内就可能让应用从展开尺寸的全屏窗口,变成中等尺寸的分屏窗口,再变成紧凑尺寸的桌面悬浮窗口。Android 官方指南也明确建议,根据应用窗口决定布局,而不是先判断设备属于手机还是平板。
用布局策略替代方向判断
使用 Material 3 Adaptive 1.2.0,Compose 应用可以读取当前的 WindowSizeClass,并在应用窗口变化时作出响应。这个稳定版本还通过 WindowManager 1.5.0 支持了 Large 和 Extra-large 宽度类别。
dependencies {
implementation("androidx.compose.material3.adaptive:adaptive:1.2.0")
}
我们可以定义一个简单的布局策略,避免在 UI 各处散落方向判断:
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.window.core.layout.WindowSizeClass
enum class ContentLayout {
SinglePane,
TwoPane,
}
@Composable
fun currentContentLayout(): ContentLayout {
val windowSizeClass = currentWindowAdaptiveInfo(
supportLargeAndXLargeWidth = true,
).windowSizeClass
return if (
windowSizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND,
)
) {
ContentLayout.TwoPane
} else {
ContentLayout.SinglePane
}
}
这样,功能页面响应的就是可用空间,而不是设备标签:
@Composable
fun MessageRoute(
state: MessageUiState,
onMessageSelected: (String) -> Unit,
) {
when (currentContentLayout()) {
ContentLayout.SinglePane -> {
if (state.selectedMessage == null) {
MessageList(
messages = state.messages,
onMessageSelected = onMessageSelected,
)
} else {
MessageDetail(message = state.selectedMessage)
}
}
ContentLayout.TwoPane -> {
Row(Modifier.fillMaxSize()) {
MessageList(
messages = state.messages,
onMessageSelected = onMessageSelected,
modifier = Modifier.weight(0.4f),
)
VerticalDivider()
MessageDetail(
message = state.selectedMessage,
modifier = Modifier.weight(0.6f),
)
}
}
}
}
这里省略了 MessageUiState 和子组件的定义,重点是布局切换。双窗格模式下还没有选中消息时,MessageDetail 需要能接收 null 并显示占位内容;单窗格模式还需要处理从详情返回列表的操作。
同一个判断,就能用于旋转后的手机、展开的折叠屏、平板、ChromeOS 窗口、桌面窗口和外接显示屏。不需要 isTablet,不需要 isLandscape,也不用维护两套重复的横竖屏界面树。
实际项目中的列表与详情流程,优先考虑 NavigableListDetailPaneScaffold。
相比手写 Row,它对单窗格与多窗格展示、窗格导航和预测性返回的处理更完整。类似地,NavigationSuiteScaffold 可以根据空间变化,把顶层导航从底部导航栏切换为侧边导航栏。
自适应不仅仅是多一个窗格
窗口变大,并不意味着每个组件都可以无限拉伸。
在 360dp 宽度下看起来很合适的登录表单,放到桌面显示器上,可能就成了一组一米宽的输入框。在整体布局适应窗口的同时,局部组件也要适应各自的空间:
@Composable
fun SignInContent(
email: String,
onEmailChanged: (String) -> Unit,
onSubmit: () -> Unit,
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter,
) {
Column(
modifier = Modifier
.widthIn(max = 640.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.imePadding()
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlinedTextField(
value = email,
onValueChange = onEmailChanged,
modifier = Modifier.fillMaxWidth(),
label = { Text("Email") },
)
Button(
onClick = onSubmit,
modifier = Modifier.fillMaxWidth(),
) {
Text("Continue")
}
}
}
}
相比只考虑竖屏的布局,这里有三个细节能让页面更好地适应窗口变化:
widthIn(max = 640.dp)避免表单在视觉上被过度拉宽。verticalScroll(...)让用户在高度较小的横屏或分屏窗口里,仍然能滚动到各个控件。imePadding()为输入法留出空间,避免键盘压缩可用高度后遮挡主要操作。
Android 的适配检查清单专门列出了这些常见问题:组件拉伸、操作按钮超出屏幕、相机预览异常,以及窗口变化时状态丢失。
窗口变化,状态不能丢
有时,应用锁定方向是为了避免 Activity 重建,而不是因为体验确实需要固定方向。这种状态管理方式一直都很脆弱。
把业务状态和页面状态放到 ViewModel 中;对于由 composable 自己管理的少量 UI 状态,使用 rememberSaveable。需要注意,ViewModel 能跨配置变更保留状态,但不能独自应对系统终止进程;需要恢复的少量状态还应使用 SavedStateHandle 等保存机制,业务数据则按需持久化:
@Composable
fun SearchRoute(viewModel: SearchViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var expandedFilter by rememberSaveable { mutableStateOf<String?>(null) }
SearchScreen(
state = uiState,
expandedFilter = expandedFilter,
onFilterExpanded = { expandedFilter = it },
onQueryChanged = viewModel::onQueryChanged,
)
}
用户应该能够在表单中输入内容,然后旋转设备、展开折叠屏、调整窗口大小,再从刚才的位置继续操作。不要把 android:configChanges 当成解决所有状态丢失问题的补丁。如果选择不让系统因相应配置变化而重建 Activity,应用就需要负责正确更新所有受影响的资源和 UI 元素;进程终止等其他原因仍然可能导致重建。处理配置变更
对方向敏感的功能怎么办
有些体验确实在意显示区域的形状。视频播放器可能提供横屏按钮,相机预览需要协调传感器方向与 UI Surface 的关系,绘图应用也可能针对宽画布优化工具的位置。
这些需求没有消失,只是需要在更局部的层面实现。
- 小于
sw600dp的手机,仍然可以响应受支持的方向请求。 - 对于以 API 37 为目标版本、运行在大屏上的应用,要让功能适应系统提供的窗口。
- 视频的控件和内容缩放应适应容器,不要假定自己占据整块物理显示屏。
- 相机预览的变换应根据传感器、预览
Surface、旋转角度和缩放类型计算。不要只凭“竖屏”或“横屏”推断它们之间的关系。 - 对于需要宽阔空间的创作功能,宽度允许时可以提供专门优化的宽屏布局,同时也要保留可用的紧凑布局。
某个功能可以更适合某种窗口形状,但不应该让整个应用都依赖这种形状。
现有项目怎么改🌰
1. 找出隐藏的方向假设
在现有代码中搜索下面这些关键词:
screenOrientation
requestedOrientation
setRequestedOrientation
resizeableActivity
minAspectRatio
maxAspectRatio
ORIENTATION_PORTRAIT
ORIENTATION_LANDSCAPE
LocalConfiguration.current.orientation
逐个判断搜索结果属于哪一类:真正的功能约束、布局决策,还是规避状态丢失的临时方案。
2. 从功能接口中移除屏幕方向
避免这样的 API:
fun DetailsScreen(isLandscape: Boolean)
改用含义明确、也更容易测试的布局策略:
fun DetailsScreen(layout: DetailsLayout)
应用外层可以根据当前窗口推导出 DetailsLayout,功能内部只需要决定每种布局模式下如何展示和交互。
3. 先让组件适应各种尺寸
在构建精致的双窗格布局之前,先保证现有页面在各种不太理想的尺寸下都能正常使用:
- 移除硬编码的屏幕宽高;
- 为阅读区域、表单和对话框设置最大宽度;
- 让高度受限的内容可以滚动;
- 考虑系统栏和输入法(IME)占用的空间;
- 避免根据缓存的显示屏尺寸计算动画位置;
- 检查较大字体缩放比例下的文字显示。
4. 加入自适应应用结构
然后,再引入收益较大的适配:
- 底部导航栏 → 侧边导航栏;
- 先列表、后详情 → 列表与详情并排;
- 只显示主内容 → 主内容加辅助窗格;
- 固定网格列数 → 根据宽度自适应的网格;
- 占满宽度的表单 → 居中且有宽度上限的表单。
5. 测试窗口切换,而不只是设备
不要只测“手机竖屏”和“平板横屏”,还要测试这些切换过程和场景:
- 紧凑 → 展开 → 紧凑;
- 全屏 → 分屏;
- 折叠 → 展开;
- 编辑表单时从竖屏切到横屏;
- 调整桌面窗口大小,使其跨过布局断点;
- 在高度较小的窗口中打开键盘;
- 完成选择或导航操作后,重建应用进程。
可以从 Pixel Tablet 和 Pixel Fold 模拟器开始测试。
Android 兼容性框架也支持启用 UNIVERSAL_RESIZABLE_BY_DEFAULT,用于迁移测试。Compose 自动化测试应注入表达布局含义的模式或窗口尺寸类别,这样就能独立于特定模拟器,验证紧凑和展开布局的行为。
对了,希望这篇文章可以帮到你——适配小米“中”折屏,非得买一台吗。
新的 UI 架构是什么样
未来的 Android 界面,不再是过去那种固定的“屏幕”,而是一个可以调整大小、按需显示一个或多个窗格的窗口。用户操作的过程中,窗口形状也可能发生变化。
这会让职责划分更清楚:
- 平台管理窗口;
- 应用外层判断可用空间;
- 功能提供含义明确的布局模式;
- 组件在各个模式下灵活适应空间;
- 状态在每次切换后都能保留。
横屏和竖屏依然存在,只不过它们是更复杂运行环境中的两个输入条件,不宜再作为决定 UI 布局的唯一依据。
如果应用以 Android 17 为目标版本,与其只问“怎么让这个页面一直保持竖屏”,不如先考虑:在用户选择的窗口里,这个功能怎样呈现才最合适?