在分屏模式下,如何确保 UI 元素不会被遮挡或错位?
解读
国内主流 ROM(MIUI、ColorOS、EMUI、OriginOS、Flyme 等)对 AOSP 分屏做了深度定制:
- 比例范围从 16:9 到 1:1,甚至支持“小窗+分屏”三应用同屏;
- 状态栏/导航栏高度、挖孔、水滴、圆角、折叠屏折痕区域各厂差异大;
- 面试时如果只回答“用 dp 适配”会被认为没做过真机验证,必须给出可落地的 WindowInsets、Configuration 变更监听、ConstraintLayout+Guideline 组合拳,并提到国产机折痕避让 API。
面试官真正想听的是:
- 你能不能在生命周期回调里实时拿到最新窗口边界;
- 能不能用 Jetpack WindowManager 1.x 的 WindowMetrics 避开已弃用的 Display.getSize();
- 知不知道国内 ROM 强制“全屏显示”开关会导致 Application 的 DisplayMetrics 被缓存错,需要重写 Resources 走 getSystemService(WindowManager).currentWindowMetrics;
- 有没有在折叠屏机型上验证过当用户从 8:7.1 大屏切到 22.5:18 分屏时,你的底部导航会不会被折痕遮挡。
知识点
- 分屏生命周期:onMultiWindowModeChanged / onConfigurationChanged
- 窗口边界获取:
- SDK 30 以前:Display.getSize()(已废弃,国内面试答这个直接扣分)
- SDK 30+:WindowMetrics#getBounds()、WindowMetrics#getWindowInsets()
- 系统边衬避让:WindowInsets.getInsetsIgnoringVisibility()、WindowInsetsCompat.Type.systemBars() | displayCutout()
- 国产折痕/圆角避让:小米 MiuiFoldableHelper、华为 FoldableScreenManager、OPPO ScreenCutoutManager(均为厂商私有 SDK,需 Maven 私服集成)
- 布局策略:ConstraintLayout + Guideline percent、WindowSizeClass 计算、Compose 的 WindowInsetsPadding、NavigationBarPadding
- 强制全屏开关陷阱:Settings.Secure.getInt("display_fullscreen_apps") 导致 Activity 重启后 Resources 缓存旧高宽,需 recreate() 或 updateResources()
- 测试命令:adb shell am split 0.5 + adb shell wm size 折叠屏分辨率快速模拟
答案
分屏模式下保证 UI 不遮挡要分三步:实时监听、准确取数、动态避让。
-
实时监听
在 Activity 与 Fragment 中同时重写:- onMultiWindowModeChanged(isInMultiWindowMode: Boolean)
- onConfigurationChanged(newConfig: Configuration)
并在 AndroidManifest 对应 Activity 声明 android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden",避免重启。
-
准确取数
使用 Jetpack WindowManager 1.2+:
val metrics = windowManager.currentWindowMetrics
val bounds = metrics.bounds
val insets = metrics.windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout())
计算实际可用矩形:
val usableWidth = bounds.width() - insets.left - insets.right
val usableHeight = bounds.height() - insets.top - insets.bottom
切勿用 DisplayMetrics.widthPixels,它在国产机强制全屏开关开启时会被缓存成错误值。 -
动态避让
布局层:- XML 采用 ConstraintLayout,底部关键按钮用 app:layout_constraintBottom_toTopOf="@id/guideline_nav",guideline 百分比通过代码 setGuidelinePercent(0.9) 动态调整;
- Compose 侧用 Modifier.windowInsetsPadding(WindowInsets.navigationBars + WindowInsets.displayCutout),并监听 LocalConfiguration.current.screenWidthDp 变化;
- 折叠屏机型在 onCreate 中调用厂商 SDK 获取折痕区域,换算成 px 后额外增加一个 Guideline 或 Spacer,确保 BottomSheet 不会被折痕遮挡。
代码示例(Kotlin):
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) {
super.onMultiWindowModeChanged(isInMultiWindowMode)
val windowMetrics = windowManager.currentWindowMetrics
val insets = windowMetrics.windowInsets.getInsets(WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout())
val density = resources.displayMetrics.density
val bottomMarginPx = insets.bottom + getFoldCreaseHeight() // 厂商 SDK
guidelineNav.setGuidelineBegin((windowMetrics.bounds.height() - bottomMarginPx) / density)
}最后,在 UI 测试用例里用 Espresso 的 OrientationChangeAction 和 UiAutomator 的
UiDevice.getInstance().setOrientationLeft()组合分屏命令,断言关键按钮底部距屏幕实际距离 ≥ insets.bottom + foldCreaseHeight,即可覆盖 90% 国产机场景。
拓展思考
- 三应用同屏(小窗+分屏)场景下,你的 Activity 可能只占屏幕 30%,此时如果弹出 Dialog 使用默认 WRAP_CONTENT 会被系统强制截断,需要自定义 Dialog 的 Window 类型为 TYPE_APPLICATION_PANEL 并手动设置 Gravity.BOTTOM|Gravity.CENTER_HORIZONTAL,同时限制最大高度为可用窗口高度的一半。
- 国内银行、支付类 App 为了安全会禁止分屏,做法是在 onCreate 里直接判断 isInMultiWindowMode 并 finish(),但 Android 12L 开始企业管理员可以强制分屏,此时需要弹合规提示而非直接退出,否则会被应用商店审核驳回。
- 折叠屏从展开到半折(Posture HALF_OPENED)时,系统会连续触发两次 configurationChange,第一次比例突变、第二次折痕区域更新,如果两次回调共用同一个 coroutine 做动画,会出现闪屏;正确做法是使用 lifecycleScope + debounce(200ms) 合并事件,等 WindowMetrics 稳定后再刷新布局。