1. 背景与业务痛点
随着刘海屏、水滴屏、打孔屏等异形屏(Display Cutout)的普及,以及全模态全面屏的演进,传统的固定状态栏高度适配方案已无法满足现代 App 的视觉需求。
特别是在横屏高沉浸式场景(如视频播放器、游戏、全景相机等)中,若适配不当,通常会面临以下两个痛点:
- 黑边问题:系统为了防止内容被刘海遮挡,默认会在刘海侧强行填充一条黑色遮罩,导致画面无法真正全屏。
- UI 元素遮挡:在强制全屏后,如果未动态计算安全区域,关键交互组件(如返回键、控制面板、悬浮按钮)会被刘海物理遮挡,引发用户误触或无法点击的故障。
为了规范异形屏适配流程,保证多终端视觉一致性与高可靠性,特制定本技术方案。
2. 核心适配策略
异形屏适配的核心逻辑可解耦为两个步骤:
- Window 级别配置(解禁限制) :修改窗口属性,声明允许应用内容延伸至屏幕短边的物理异形区域。
- View 级别适配(精准避让) :通过底层事件分发机制,动态获取物理遮挡边界,对交互组件(非背景图)施加绝对/相对偏移。
lua
+-------------------------------------------------------------+
| [ 状态栏 / 左刘海区域 ] <--- Window 声明 SHORT_EDGES 延伸到此 |
| +-------------------------------------------------------+ |
| | (返回键) <--- View 监听到 Insets,通过 Padding 避让 | |
| | | |
| | 视频/游戏等背景画面 (全面延伸占满) | |
| +-------------------------------------------------------+ |
+-------------------------------------------------------------+
3. 技术实现方案 (基于 API 28+)
自 Android 9.0 (API 28) 起,Google 推出了官方标准 API。针对横竖屏沉浸式场景,方案实现细则如下:
3.1 属性配置:开启边缘绘制 (Edge-to-Edge)
必须在 Activity 启动时(setContentView 之前),通过设置 layoutInDisplayCutoutMode 告诉系统不再保留黑边。
Java
typescript
private void setupFullscreenAndCutout(Window window) {
// 1. 允许内容延伸到短边刘海区(横屏沉浸式核心)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams lp = window.getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
window.setAttributes(lp);
}
// 2. 隐藏系统状态栏和导航栏
WindowInsetsControllerCompat windowInsetsController =
WindowCompat.getInsetsController(window, window.getDecorView());
windowInsetsController.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
// 3. 核心:允许内容绘制在系统栏区域,解除传统布局限制
WindowCompat.setDecorFitsSystemWindows(window, false);
}
注意: 必须选用
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES。若使用DEFAULT模式,应用在横屏时依然会被系统强制切出黑边。
3.2 动态避让:安全区域边界获取与消费
获取边界时,切忌对全屏背景 View 设置 Padding ,否则全屏沉浸将失效。正确的解法是:将监听器绑定在承载交互 UI 的容器 View 上。
核心实现代码:
scss
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化窗口属性
setupFullscreenAndCutout(getWindow());
setContentView(R.layout.activity_media_player);
// 承载全屏画面的背景 View(如 TextureView 或 ImageView)
View videoRenderView = findViewById(R.id.video_render_view);
// 包裹了返回按钮、进度条等交互组件的容器 View
View uiControlContainer = findViewById(R.id.ui_control_container);
// 注册 WindowInsets 监听
ViewCompat.setOnApplyWindowInsetsListener(uiControlContainer, (v, insets) -> {
DisplayCutoutCompat cutout = insets.getDisplayCutout();
if (cutout != null) {
// 当设备发生 90° 或 270° 旋转时,系统会自动回调此处
// 此时 safeLeft 与 safeRight 会动态互换,无须手动判断旋转角度
int safeLeft = cutout.getSafeInsetLeft();
int safeRight = cutout.getSafeInsetRight();
int safeTop = cutout.getSafeInsetTop();
int safeBottom = cutout.getSafeInsetBottom();
// 动态对交互容器施加 Padding,确保内部组件由于物理刘海被挤压到安全区内
v.setPadding(safeLeft, safeTop, safeRight, safeBottom);
} else {
// 无刘海设备,重置边距
v.setPadding(0, 0, 0, 0);
}
// 返回 CONSUMED 意味着此 Insets 已被完全处理,不再向下游子 View 分发
return WindowInsetsCompat.CONSUMED;
});
// 主动触发一次 Insets 分发,确保 attached 时回调能立刻执行
uiControlContainer.requestApplyInsets();
}
4. 架构专项适配:Jetpack Compose 方案
若项目已向现代声明式 UI 架构(Jetpack Compose)迁移,则不再需要手动注册 Listener 并计算像素值,可直接利用 Compose 的 WindowInsets 扩展。
scss
// 1. 在 ComponentActivity 的 onCreate 中开启全屏幕适配
WindowCompat.setDecorFitsSystemWindows(window, false)
// 2. 在 Composable 中进行生命式布局
@Composable
fun LandscapePlayerScreen() {
Box(modifier = Modifier.fillMaxSize()) {
// 背景渲染层:强制填满整个物理屏幕,包含刘海区
VideoPlayerComponent(modifier = Modifier.fillMaxSize())
// 交互控制层:利用 windowInsetsPadding 自动避开所有异形屏物理遮挡及系统栏
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing) // 核心:自动处理多端异形屏与旋转
) {
ControlTopBar(onBackClick = { /*...*/ })
Spacer(modifier = Modifier.weight(1f))
ControlBottomBar()
}
}
}
5. 历史机型兼容性设计 (Android 8.0 - 8.1)
虽然低版本系统目前市场份额较低,但在特定基线或出海项目中若仍需向下兼容,需要针对头部厂商进行私有配置兜底:
| 厂商 | Manifest 配置 | 动态判断与尺寸获取方式 |
|---|---|---|
| 华为/荣耀 | android.notch_support = true |
通过反射调用 com.huawei.android.util.HwNotchSizeUtil 内的 hasNotchInScreen 与 getNotchSize。 |
| 小米 (MIUI) | notch.config = 1 |
通过反射读取系统属性 ro.miui.notch 判断;通过系统资源 id 间接获取宽度与高度。 |
| OPPO / vivo | 无须特定配置,全屏模式下默认延伸。 | OPPO : 反射 com.oppo.feature.screen.heteromorphism。 vivo : 反射 android.util.FtFeature 的 isFeatureSupport(0x00000020)。 |
注:在当前时间节点,若无强特定老旧机型业务指标,技术侧建议不再为 Android 8.X 编写繁重的反射兼容代码,统一采用 Android 9.0+ 官方标准方案作为基线标准。
6. 方案落地与上线核对清单 (Checklist)
为确保技术方案高质量交付,上线前需在测试阶段核对以下用例:
- 横屏 90° 翻转测试:确认刘海在左侧时,左侧交互组件正常缩进,右侧无异常留白。
- 横屏 270° 翻转测试:确认刘海在右侧时,UI 自动切换,右侧交互组件正常缩进,左侧无异常留白。
- 折叠屏/大屏展开态适配:在内屏及打孔外屏间切换时,检查 Insets 刷新是否会引起界面闪烁或控件跳变。
- 系统手势冲突测试:开启全面屏手势(如边缘右滑返回)后,检查边缘按钮在施加安全 Padding 后是否仍存在点击冲突。