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:minWidthandroid:minHeight,告诉系统你的最小窗口尺寸
  • 所有弹窗和Dialog都使用WindowManager.LayoutParamsWRAP_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 知识体系总览

为了让大家更直观地理解本章的知识结构,我画了一张图:

显示与窗口兼容性 Display Cutout 适配 获取Cutout信息 布局模式选择 厂商定制化 安全区域策略 多窗口与自由窗口 生命周期变化 自由窗口处理 配置变更策略 自适应布局 核心原则:使用系统API,避免硬编码,充分测试 实践建议 1. 始终使用WindowInsets获取Cutout信息 2. 多窗口模式下使用最小宽度320dp作为紧凑布局阈值

16.4 总结与最佳实践

好了,说了这么多,我给大家总结几条铁律:

  1. 不要相信硬件参数——不同厂商、不同机型的Cutout位置和大小都不一样,用API说话
  2. 窗口尺寸是动态的——多窗口模式下,你的Activity随时可能被缩小或放大,布局必须自适应
  3. 测试要全面——至少覆盖:竖屏刘海、横屏刘海、分屏模式、自由窗口模式
  4. 善用系统回调——onMultiWindowModeChanged和onConfigurationChanged是你的好朋友

最后提醒一句:如果你在适配过程中发现某个厂商的设备行为异常,不要犹豫,直接加厂商判断。虽然这不太优雅,但总比用户看到UI被切掉要好。

显示与窗口兼容性这块,说白了就是一场和硬件厂商的博弈。你永远不知道下一台设备会有什么样的屏幕形态。但只要掌握了系统API的正确用法,再加上充分的测试,大部分问题都能解决。


公众号:蓝海资料掘金营,微信deep3321