16、显示与窗口兼容性:Display Cutout适配,多窗口模式与自由窗口的兼容性处理
各位同学,今天我们来聊聊显示与窗口这块的兼容性。说实话,这块内容在Android系统升级中,是让我踩坑最多的领域之一。你想想看,手机厂商为了做出差异化,挖孔屏、刘海屏、药丸屏,什么花样都有。而多窗口和自由窗口,更是让应用开发者头疼不已。
我个人习惯把这块内容分成两大块来讲:屏幕物理形态的适配和窗口逻辑形态的适配。前者是Display Cutout,后者是多窗口与自由窗口。咱们一个一个来。
16.1 Display Cutout适配:别让你的UI被“吃掉”
Display Cutout,说白了就是屏幕上的挖孔区域。刘海、水滴、挖孔,都属于这个范畴。Android从9.0开始正式支持,但真正让开发者头疼的是,各家厂商的实现细节千差万别。
核心原则:永远不要假设Cutout区域的位置和大小。系统API会告诉你一切。
16.1.1 获取Cutout信息
在Android 9+上,我们可以通过WindowInsets来获取Cutout信息。我在项目中遇到过,有些同学直接硬编码刘海高度,结果换了台设备就崩了。正确的做法是这样的:
// 在Activity或View中获取Cutout信息
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.decorView.post {
val insets = window.decorView.rootWindowInsets
val cutout = insets?.displayCutout
cutout?.let {
// safeInsetTop 就是刘海区域的高度
val topInset = it.safeInsetTop
val bottomInset = it.safeInsetBottom
val leftInset = it.safeInsetLeft
val rightInset = it.safeInsetRight
// 获取Cutout的精确路径(多个挖孔的情况)
val boundingRects = it.boundingRects
Log.d("Cutout", "顶部安全区域: $topInset")
Log.d("Cutout", "挖孔数量: ${boundingRects.size}")
}
}
}
我的小技巧:建议在Application的onCreate里就获取一次设备Cutout信息,缓存起来。因为很多页面都需要用到,每次都去查WindowInsets反而容易出错。
16.1.2 布局模式的选择
Android提供了三种布局模式,通过android:windowLayoutInDisplayCutoutMode来控制:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 默认模式,竖屏时刘海区域不显示内容 | 普通应用,视频播放等 |
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 允许内容延伸到短边Cutout区域 | 全屏游戏,沉浸式体验 |
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 从不允许延伸到Cutout区域 | 状态栏有重要信息的应用 |
嗯,这里要注意。我曾经在适配某款挖孔屏手机时,用了SHORT_EDGES模式,结果挖孔正好在游戏按钮下面,用户根本点不到。后来我加了个判断:如果Cutout区域和交互区域重叠,就自动调整布局。
16.1.3 避坑指南:厂商定制化问题
我曾经踩过的坑:某厂商在Android 10上把刘海高度写死了,导致系统API返回的值和实际刘海区域不匹配。最后我不得不加了个厂商白名单,针对特定机型做特殊处理。
我的建议是:
- 始终使用系统API获取Cutout信息,不要相信任何常量值
- 在关键UI元素周围保留至少16dp的边距
- 测试时覆盖主流挖孔形态:左上角、居中、药丸形
- 对于全屏应用,考虑提供“安全区域”的配置选项
16.2 多窗口模式与自由窗口的兼容性处理
多窗口模式从Android 7.0开始引入,到现在已经非常成熟了。但自由窗口(Freeform)模式,说实话,用的人不多,但一旦用上,坑特别多。
16.2.1 多窗口模式的生命周期变化
进入多窗口模式时,Activity会经历一次重建。你想想看,如果你的应用在onSaveInstanceState里保存了窗口状态,但恢复时窗口尺寸变了,那画面会怎样?
我建议这样处理:
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) {
super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig)
if (isInMultiWindowMode) {
// 进入多窗口模式,调整布局
adjustLayoutForMultiWindow(newConfig)
} else {
// 退出多窗口模式,恢复布局
restoreLayout()
}
}
private fun adjustLayoutForMultiWindow(config: Configuration?) {
config?.let {
val screenWidthDp = it.screenWidthDp
val screenHeightDp = it.screenHeightDp
// 如果窗口宽度小于320dp,切换到紧凑布局
if (screenWidthDp < 320) {
switchToCompactLayout()
}
}
}
核心要点:多窗口模式下,你的Activity可能只有原来一半甚至更小的空间。布局必须能自适应,否则用户会看到被截断的UI。
16.2.2 自由窗口的特殊处理
自由窗口模式,说白了就是让应用像桌面窗口一样可以随意拖动和调整大小。Android原生支持有限,但很多厂商(尤其是平板和折叠屏)都做了深度定制。
我记得有一次,某厂商的折叠屏手机在自由窗口模式下,窗口最小尺寸被设成了200dp,结果我的应用里有个弹窗,宽度正好是220dp,弹出来就超出窗口了。嗯,从那以后,我养成了一个习惯:
- 设置
android:minWidth和android:minHeight,告诉系统你的最小窗口尺寸 - 所有弹窗和Dialog都使用
WindowManager.LayoutParams的WRAP_CONTENT模式 - 避免使用固定尺寸的布局,多用ConstraintLayout
16.2.3 配置变更的处理策略
多窗口模式下,配置变更(尤其是屏幕尺寸变化)非常频繁。我个人习惯使用android:configChanges来自己处理,而不是让系统重建Activity:
<activity
android:name=".MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:resizeableActivity="true">
然后在代码里:
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// 重新计算布局
recalculateLayout(newConfig)
// 更新Cutout安全区域
updateSafeArea()
}
我的经验:如果你选择自己处理配置变更,一定要确保所有UI元素都能响应尺寸变化。我见过有些应用只处理了onCreate里的布局,结果窗口一变化,UI就乱套了。
16.3 知识体系总览
为了让大家更直观地理解本章的知识结构,我画了一张图:
16.4 总结与最佳实践
好了,说了这么多,我给大家总结几条铁律:
- 不要相信硬件参数——不同厂商、不同机型的Cutout位置和大小都不一样,用API说话
- 窗口尺寸是动态的——多窗口模式下,你的Activity随时可能被缩小或放大,布局必须自适应
- 测试要全面——至少覆盖:竖屏刘海、横屏刘海、分屏模式、自由窗口模式
- 善用系统回调——onMultiWindowModeChanged和onConfigurationChanged是你的好朋友
最后提醒一句:如果你在适配过程中发现某个厂商的设备行为异常,不要犹豫,直接加厂商判断。虽然这不太优雅,但总比用户看到UI被切掉要好。
显示与窗口兼容性这块,说白了就是一场和硬件厂商的博弈。你永远不知道下一台设备会有什么样的屏幕形态。但只要掌握了系统API的正确用法,再加上充分的测试,大部分问题都能解决。
公众号:蓝海资料掘金营,微信deep3321