HarmonyOS 6.0 沉浸式状态栏与安全区域适配实战
从 setWindowLayoutFullScreen 到 expandSafeArea,一篇搞懂鸿蒙沉浸式开发的所有姿势
沉浸式到底是个什么概念
打开一个新创建的 HarmonyOS 应用,你会看到这样的画面——顶部状态栏区域有一块系统占位的灰色/黑色区域,底部导航条区域也有一块,中间才是你的应用内容。整个界面被切成三段,视觉上非常割裂。
所谓沉浸式,就是让这三段"融为一体"。状态栏、导航条这些系统 UI 元素不再突兀地横在那里,而是和应用界面形成视觉上的延续——要么把应用背景色延伸上去,要么干脆把系统栏隐藏掉。
HarmonyOS 里,状态栏和底部导航条所在的位置叫避让区(AvoidArea),避让区之外的区域叫安全区(SafeArea)。沉浸式开发要解决的就是两件事:
- 视觉融合:避让区与应用界面色调统一
- UI 避让:交互元素不被避让区遮挡
HarmonyOS NEXT(API 12+)给了我们两条路:窗口全屏布局方案和组件安全区方案。两条路的思路完全不同,适用场景也不同,下面逐个拆解。
方案一:窗口全屏布局——setWindowLayoutFullScreen
核心思路
调用 window.setWindowLayoutFullScreen(true) 后,整个窗口的布局区域从"安全区"扩展到了"全屏"。也就是说,你的组件可以从屏幕最顶端 (y=0) 开始布局,一直铺到屏幕最底部。
这时候状态栏和导航条还在显示,但它们叠在了你的内容之上。所以你要做的就变成了:手动计算避让区高度,给内容加上 padding,把可交互元素从避让区里挪出来。
在 EntryAbility 中初始化
这套方案通常在 EntryAbility.ets 的 onWindowStageCreate 中完成初始化:
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'testTag', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/TechBlogDemo', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content.');
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
try {
const mainWindow: window.Window = windowStage.getMainWindowSync();
AppStorage.setOrCreate('mainWindow', mainWindow);
} catch (e) {
hilog.error(DOMAIN, 'testTag', 'Failed to get main window.');
}
});
}
几个关键点:
- 必须先 loadContent 再 getMainWindowSync:在页面加载前调用会报 1300002 “window state is abnormal”,因为窗口尚未完成创建
getWindowAvoidArea返回的尺寸单位是 px(物理像素),布局用的是 vp,所以页面里要做单位转换avoidAreaChange监听是折叠屏适配的核心——折叠/展开时避让区尺寸可能变化- 用
getMainWindowSync()而非getMainWindow():异步获取可能在页面渲染时还未完成,导致窗口状态异常
页面中使用避让区高度
拿到 statusBarHeight 和 navBarHeight 之后,在页面中通过 @StorageProp 读取,再配合 px2vp 转换后设置 padding:
@Entry
@Component
struct Index {
@StorageProp('statusBarHeight') statusBarHeight: number = 0;
@StorageProp('navBarHeight') navBarHeight: number = 0;
build() {
Column() {
// 应用内容
Text('Hello World')
}
.width('100%')
.height('100%')
.padding({
top: this.getUIContext().px2vp(this.statusBarHeight),
bottom: this.getUIContext().px2vp(this.navBarHeight),
})
}
}
px2vp 是 UIContext 上挂载的方法,通过 this.getUIContext().px2vp() 调用。它把物理像素值转换成 vp 虚拟像素,这样在不同屏幕密度的设备上表现一致。getWindowAvoidArea 返回的 topRect.height 是 px 单位,如果直接拿来当 padding 用而不转换,在 480dpi 的设备上(1vp ≈ 3px),你会看到 padding 比预期大 3 倍。
getWindowAvoidArea 的避让区类型
getWindowAvoidArea 接收一个 AvoidAreaType 枚举参数,API 12 中常用的有:
| 类型 | 值 | 说明 |
|---|---|---|
| TYPE_SYSTEM | 0 | 状态栏区域(悬浮窗下是三点控制栏) |
| TYPE_CUTOUT | 1 | 刘海屏/挖孔屏区域 |
| TYPE_NAVIGATION_INDICATOR | 4 | 底部导航条区域 |
| TYPE_KEYBOARD | 3 | 软键盘区域 |
返回的 AvoidArea 对象结构包含 topRect、bottomRect、leftRect、rightRect 四个 Rect,每个 Rect 有 left、top、width、height 四个属性(单位均为 px)。
对状态栏,关注 topRect.height;对底部导航条,关注 bottomRect.height。
方案一适用场景
全屏布局方案的好处是整个应用(所有页面)统一生效,一次设置,全局生效。但它需要你手动处理每个页面的 padding/margin 避让。
适合的场景:
- 视频播放器(播放时隐藏系统栏,控制栏叠在视频上)
- 游戏界面(完全全屏)
- 相机取景器(预览画面铺满全屏)
- 需要在避让区内布局交互元素的场景
方案二:组件安全区——expandSafeArea
核心思路
这是 HarmonyOS 的默认模式。不调用 setWindowLayoutFullScreen(true) 的情况下,组件的布局自动限制在安全区内,可交互元素不会被状态栏/导航条遮挡。
但问题来了——默认模式下,顶部和底部会有一块系统窗口背景色露出来,和你的应用界面颜色不统一,视觉还是割裂的。
expandSafeArea 修饰符就是来解决这个问题的。它让组件的绘制区域(背景色、背景图)延伸到安全区外,但布局位置不变。换句话说,文字、按钮等交互元素仍然呆在安全区内,但背景色可以"铺"到状态栏和导航条下面。
基本用法
// 顶部标题栏延伸到状态栏
Row() {
Text('我的应用')
.fontSize(18)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height(56)
.backgroundColor('#1a73e8')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
// 底部导航栏延伸到导航条
Row() {
// 底部tab按钮
}
.width('100%')
.height(50)
.backgroundColor('#ffffff')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
参数说明:
- 第一个参数
SafeAreaType[]:要扩展的安全区类型,SYSTEM对应状态栏和导航条 - 第二个参数
SafeAreaEdge[]:要扩展的边缘方向,TOP向状态栏延伸,BOTTOM向导航条延伸
同时延伸顶部和底部也是可以的:
Column() {
// 内容
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
expandSafeArea 的工作原理
这里有个容易踩的坑:expandSafeArea 只扩展绘制区域,不改变布局区域。
打个比方,你有一个 height(56) 的 Row,设置了 expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])。那么这个 Row 的布局位置不变(文字还在 y=56 的位置开始渲染),但它的背景色会向上延伸到状态栏区域。视觉上,状态栏下面的背景色和 Row 一致,形成沉浸感。
但如果你的 Row 上面还有其他组件,那些组件的布局还是从安全区顶部开始的,不会被"推上去"。这正是 expandSafeArea 的设计意图——只做视觉延伸,不做布局侵入。
需要注意 clip 属性的配合。如果父容器设置了 .clip(true)(Tabs 组件默认 clip 为 true),超出父容器边界的绘制会被裁剪掉,expandSafeArea 就不生效了。这种情况下需要设置 .clip(false)。
方案二适用场景
组件安全区方案更适合大多数普通应用,原因很简单:
- 不需要手动计算避让区高度
- 不需要在每个页面设置 padding
- 交互元素自动避让,不会误触
- 只需在需要沉浸感的组件上加一个 expandSafeArea
适合的场景:
- 普通应用的首页、列表页、详情页
- 顶部标题栏需要延伸到状态栏
- 底部 Tab 栏需要延伸到导航条
- 滚动容器背景需要铺满全屏
两种方案对比
| 维度 | 窗口全屏布局 | 组件安全区 |
|---|---|---|
| 核心API | setWindowLayoutFullScreen(true) |
expandSafeArea |
| 布局范围 | 全屏(从屏幕顶端到底端) | 安全区内(默认避让系统栏) |
| 避让处理 | 手动计算 padding/margin | 自动避让 |
| 视觉延伸 | 内容直接铺到避让区 | 仅背景/绘制延伸 |
| 生效范围 | 整个窗口(全局) | 单个组件 |
| 单位转换 | 需要 px2vp | 不需要 |
| 适配成本 | 每个页面都要处理 | 选择性添加修饰符 |
| 典型场景 | 视频、游戏、相机 | 普通应用 |
不夸张地说,90% 的应用用组件安全区方案就够了。全屏布局方案更适合那种"就是要铺满全屏"的场景。
setSpecificSystemBarEnabled——隐藏系统栏
有些场景下,你需要的不是"视觉融合",而是"直接消失"——比如视频播放器全屏播放时,状态栏和导航条都不应该出现。
setSpecificSystemBarEnabled 可以单独控制状态栏和导航条的显隐:
const mainWindow = AppStorage.get<window.Window>('mainWindow');
// 隐藏状态栏
mainWindow?.setSpecificSystemBarEnabled('status', false);
// 隐藏导航条(注意参数名是 navigationIndicator)
win.setSpecificSystemBarEnabled('navigationIndicator', false);
// 恢复显示
win.setSpecificSystemBarEnabled('status', true);
win.setSpecificSystemBarEnabled('navigationIndicator', true);
参数 'status' 对应顶部状态栏,'navigationIndicator' 对应底部导航条。第二个参数 true 显示,false 隐藏。
隐藏系统栏通常和 setWindowLayoutFullScreen(true) 配合使用。开了全屏布局 + 隐藏系统栏,你的应用就完全占据整个屏幕了。
注意:隐藏系统栏后用户可能不好退出应用或切到其他应用,所以通常的做法是——点击屏幕时显示系统栏,几秒无操作后再自动隐藏。
折叠屏适配——avoidAreaChange 监听
折叠屏是沉浸式适配中的一个"特殊变量"。同一个应用,在折叠态和展开态下,状态栏高度可能不同,导航条也可能出现或消失。
这就是为什么 avoidAreaChange 监听是必须的。单纯在 onWindowStageCreate 里获取一次避让区高度是不够的——设备折叠/展开后,那些值就过时了。
注册监听
mainWindow.on('avoidAreaChange', (data) => {
console.info(`避让区变化: type=${data.type}, area=${JSON.stringify(data.area)}`);
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
AppStorage.setOrCreate('statusBarHeight', data.area.topRect.height);
} else if (data.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
AppStorage.setOrCreate('navBarHeight', data.area.bottomRect.height);
}
});
回调中的 data 包含:
data.type:变化的避让区类型(AvoidAreaType枚举)data.area:变化后的避让区信息(AvoidArea对象)
配合屏幕方向检测
折叠屏展开时,屏幕方向也可能变化。可以在 avoidAreaChange 回调中同步获取屏幕方向:
import { display } from '@kit.ArkUI';
mainWindow.on('avoidAreaChange', async (data) => {
// 获取当前屏幕方向
let defaultDisplay = display.getDefaultDisplaySync();
console.info(`屏幕方向: ${defaultDisplay.orientation}`);
// 更新避让区数据
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
AppStorage.setOrCreate('statusBarHeight', data.area.topRect.height);
}
});
典型的折叠屏适配策略
- 初始化时获取一次避让区高度,存入
AppStorage - 注册
avoidAreaChange监听,动态更新AppStorage中的值 - 页面中通过
@StorageProp响应式绑定,数据变化时 UI 自动刷新 - 所有涉及避让区高度的布局值,统一通过
px2vp转换
这套策略在 Demo 中也有体现——ImmersiveDemo 中的 @StorageProp('statusBarHeight') 和 @StorageProp('navBarHeight') 就是绑定到 AppStorage 的响应式属性,避让区变化时数值自动更新。
px2vp 单位转换——一个坑点
getWindowAvoidArea 返回的高度值是 px(物理像素),而 ArkUI 布局系统默认使用 vp(虚拟像素) 作为单位。这两者之间的换算关系取决于屏幕密度:
vp = px / (DPI / 160)
在 480dpi 的设备上,1vp = 3px。如果你拿一个 96px 的状态栏高度直接设成 padding({ top: 96 }),实际效果相当于给了 96vp 的 padding,在 480dpi 设备上相当于 288px,远超状态栏的实际高度。
正确的做法:
// 方式一:通过 UIContext 转换
.padding({
top: this.getUIContext().px2vp(this.statusBarHeight),
bottom: this.getUIContext().px2vp(this.navBarHeight),
})
// 方式二:在 AppStorage 写入时就转换(不推荐,因为密度可能随折叠屏变化)
AppStorage.setOrCreate('statusBarHeightVp', px2vp(systemArea.topRect.height));
推荐方式一,因为 UIContext 的 px2vp 会使用当前 UI 实例所在屏幕的虚拟像素比,折叠屏切换屏幕后换算比例可能变化,实时转换更准确。
UIContext 上挂载的单位转换方法一共有六个:
| 方法 | 说明 |
|---|---|
px2vp(value) |
px → vp |
vp2px(value) |
vp → px |
px2fp(value) |
px → fp(字体像素) |
fp2px(value) |
fp → px |
px2lpx(value) |
px → lpx(逻辑像素,折叠屏适配用) |
lpx2px(value) |
lpx → px |
日常开发中最常用的是 px2vp 和 vp2px。lpx 系列主要用在折叠屏的响应式布局中,它基于屏幕逻辑宽度做比例缩放,和 vp 解决的不是同一个问题——vp 解决的是"不同密度的屏幕上物理尺寸一致",lpx 解决的是"不同尺寸的屏幕上布局比例一致"。
完整 Demo 代码走读
接下来逐段解析 ImmersiveDemo.ets 的完整代码,看它是如何把上面这些 API 串联起来的。
状态定义
@Entry
@Component
struct ImmersiveDemo {
@StorageProp('statusBarHeight') statusBarHeight: number = 0;
@StorageProp('navBarHeight') navBarHeight: number = 0;
@State isFullScreen: boolean = false;
@State isStatusBarHidden: boolean = false;
@State isNavBarHidden: boolean = false;
@State activeTab: number = 0;
@State statusText: string = '未操作'
private mainWindow: window.Window | null = null
}
@StorageProp 是单向绑定,从 AppStorage 读取值到组件内,AppStorage 更新时组件自动刷新。statusBarHeight 和 navBarHeight 是在 ImmersiveDemo 的 initWindow() 中通过 getWindowAvoidArea 获取后存入 AppStorage 的。
三个 @State 布尔值分别控制:全屏布局开关、状态栏隐藏开关、导航栏隐藏开关。activeTab 控制 Tabs 的当前选中项。statusText 显示当前操作状态,mainWindow 缓存窗口实例避免重复查找。
initWindow 初始化方法
private initWindow(): void {
try {
const mainWindow: window.Window | undefined = AppStorage.get<window.Window>('mainWindow');
if (!mainWindow) {
this.statusText = '窗口实例未获取,请重启应用';
return;
}
this.mainWindow = mainWindow;
const systemArea: window.AvoidArea = this.mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('statusBarHeight', systemArea.topRect.height);
const navArea: window.AvoidArea = this.mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
AppStorage.setOrCreate('navBarHeight', navArea.bottomRect.height);
this.statusText = '已获取窗口: 状态栏' + systemArea.topRect.height.toString() + 'px, 导航栏' + navArea.bottomRect.height.toString() + 'px';
} catch (e) {
this.statusText = '获取窗口失败: ' + String(e);
}
}
initWindow 在 aboutToAppear 生命周期中调用。它从 AppStorage 直接读取窗口实例(由 EntryAbility 在 loadContent 回调中通过 getMainWindowSync() 获取并存入),无需再通过 window.findWindow 查找。这样做避免了 findWindow 的名字匹配问题和 “window state is abnormal” 错误。拿到窗口后立即获取避让区高度并存入 AppStorage,供 @StorageProp 响应式绑定使用。
顶部导航栏
Row() {
Text('< ')
.fontSize(18)
.fontColor('#1a73e8')
.onClick(() => { router.back(); })
Blank()
Text('沉浸式与安全区域')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Blank()
Text(' ').fontSize(18).fontColor(Color.Transparent)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
这个顶部导航栏用了 expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]),让 Row 的背景色延伸到状态栏区域。不管当前是不是全屏布局模式,这个修饰符都能让顶部导航栏的背景和状态栏"连成一片"。
左右各一个 Blank() 加上对称的 Text,实现标题居中的效果。左侧 < 文字带 onClick 返回上一页。
Tab 一:沉浸式控制面板
Tabs({ index: this.activeTab }) {
TabContent() {
Scroll() {
Column() {
// 避让区数据展示
// 控制面板
// expandSafeArea 示例
}
}
}
.tabBar('沉浸式控制')
第一个 Tab 是一个可滚动的交互面板,分三个区块:
避让区数据展示——实时显示状态栏和导航栏的高度值:
Row() {
Text('状态栏高度:')
.fontSize(13).fontColor('#333333').width('45%')
Text(this.statusBarHeight.toString() + ' px')
.fontSize(13).fontColor('#1a73e8').fontWeight(FontWeight.Medium).layoutWeight(1)
}.width('100%').padding({ top: 4, bottom: 4 })
Row() {
Text('导航栏高度:')
.fontSize(13).fontColor('#333333').width('45%')
Text(this.navBarHeight.toString() + ' px')
.fontSize(13).fontColor('#1a73e8').fontWeight(FontWeight.Medium).layoutWeight(1)
}.width('100%').padding({ top: 4, bottom: 4 })
这里直接显示 px 值,方便调试时确认避让区尺寸。在生产代码中记得用 px2vp 转换后再用于布局。
控制面板——三个 Toggle 开关:
Row() {
Text('全屏布局')
.fontSize(13).fontColor('#333333').layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.isFullScreen })
.onChange((isOn: boolean) => { this.toggleFullScreen(isOn); })
}
Row() {
Text('隐藏状态栏')
.fontSize(13).fontColor('#333333').layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.isStatusBarHidden })
.onChange((isOn: boolean) => { this.toggleStatusBar(isOn); })
}
Row() {
Text('隐藏导航栏')
.fontSize(13).fontColor('#333333').layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.isNavBarHidden })
.onChange((isOn: boolean) => { this.toggleNavBar(isOn); })
}
三个开关分别触发 toggleFullScreen、toggleStatusBar、toggleNavBar 三个方法,后文会解析。
expandSafeArea 可视化示例——两个色块,直观展示延伸效果:
// 顶部色块——扩展到状态栏
Column() {
Text('顶部色块(扩展到状态栏)')
.fontSize(12)
.fontColor('#ffffff')
.padding(12)
.width('100%')
.backgroundColor('#1a73e8')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
}
.width('90%')
.borderRadius(8)
.clip(true)
// 底部色块——扩展到导航栏
Column() {
Text('底部色块(扩展到导航栏)')
.fontSize(12)
.fontColor('#ffffff')
.padding(12)
.width('100%')
.backgroundColor('#34a853')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
}
.width('90%')
.borderRadius(8)
.clip(true)
.margin({ top: 8 })
蓝色色块向上延伸到状态栏,绿色色块向下延伸到导航条。外层 Column 的 .clip(true) + borderRadius(8) 让圆角生效的同时不会在安全区边缘露出底色。这里 .clip(true) 裁剪的是外层 Column 本身的边界,而 expandSafeArea 是让内部 Text 的背景延伸出去,两者并不冲突——因为圆角容器的 expandSafeArea 延伸区域会在圆角处被裁掉,这正是我们想要的效果。
Tab 二:方案详解
第二个 Tab 是纯文本说明,对比两种沉浸式方案的关键差异,列出关键 API,以及折叠屏适配提醒。
折叠屏提醒部分特别标注了三条要点:
- 避让区尺寸可能随折叠状态动态变化
- 注册
avoidAreaChange监听动态调整 - 使用
px2vp转换像素单位
这三条是折叠屏沉浸式适配的最低要求,缺一不可。
Toggle 方法实现
private toggleFullScreen(isOn: boolean): void {
this.isFullScreen = isOn;
if (!this.mainWindow) {
this.statusText = '窗口未获取,尝试重新初始化';
this.initWindow();
return;
}
try {
this.mainWindow.setWindowLayoutFullScreen(isOn);
this.statusText = isOn ? '已开启全屏布局' : '已关闭全屏布局';
} catch (e) {
this.statusText = '全屏设置失败: ' + String(e);
}
}
通过缓存的 mainWindow 引用直接调用 setWindowLayoutFullScreen。如果窗口引用为 null,则尝试重新调用 initWindow() 初始化。
private toggleStatusBar(isOn: boolean): void {
this.isStatusBarHidden = isOn;
if (!this.mainWindow) {
this.statusText = '窗口未获取,尝试重新初始化';
this.initWindow();
return;
}
try {
this.mainWindow.setSpecificSystemBarEnabled('status', !isOn);
this.statusText = isOn ? '已隐藏状态栏' : '已显示状态栏';
} catch (e) {
this.statusText = '状态栏设置失败: ' + String(e);
}
}
隐藏状态栏。注意参数是 !isOn——Toggle 的 isOn 为 true 表示"用户要隐藏",而 setSpecificSystemBarEnabled 的第二个参数 true 表示"启用/显示",所以要取反。
private toggleNavBar(isOn: boolean): void {
this.isNavBarHidden = isOn;
if (!this.mainWindow) {
this.statusText = '窗口未获取,尝试重新初始化';
this.initWindow();
return;
}
try {
this.mainWindow.setSpecificSystemBarEnabled('navigationIndicator', !isOn);
this.statusText = isOn ? '已隐藏导航栏' : '已显示导航栏';
} catch (e) {
this.statusText = '导航栏设置失败: ' + String(e);
}
}
隐藏底部导航条。参数名是 'navigationIndicator',不是 'navigation'——这个命名容易混淆,写错了不会报编译错误但运行时不生效。
三个方法都先检查 mainWindow 是否为 null,如果为空则尝试重新初始化。窗口实例由 EntryAbility 在 loadContent 回调中通过 getMainWindowSync() 获取并存入 AppStorage,ImmersiveDemo 在 aboutToAppear 时直接从 AppStorage 读取。
常见踩坑与解决方案
坑一:全屏布局下内容被状态栏遮挡
开了 setWindowLayoutFullScreen(true) 但忘了加 padding,标题栏文字和状态栏时间/电量重叠。
解法:给顶部容器加 padding({ top: px2vp(statusBarHeight) })。或者干脆不用全屏布局方案,改用 expandSafeArea 只做视觉延伸。
坑二:expandSafeArea 不生效
几种常见原因:
- Tabs 默认 clip(true):Tabs 组件默认开启了 clip,子组件的 expandSafeArea 延伸会被裁掉。给 Tabs 设置
.clip(false)即可。 - 父容器 clip(true):手动设了 clip(true) 又想要延伸,互相矛盾。要么去掉 clip,要么接受裁剪。
- 方向写错:想在底部延伸但写了
SafeAreaEdge.TOP,方向和实际需求不匹配。 - 安全区类型写错:应该用
SafeAreaType.SYSTEM但写了SafeAreaType.KEYBOARD,类型不对自然不生效。
坑三:折叠屏展开后避让区高度不对
只在 onWindowStageCreate 里获取了一次避让区高度,折叠屏展开后没有更新。
解法:注册 avoidAreaChange 监听,在回调中更新 AppStorage。Demo 中的 @StorageProp 绑定会自动刷新 UI。
坑四:px 直接当 vp 用
getWindowAvoidArea 返回的是 px,ArkUI 布局单位默认是 vp。直接拿 px 值设 padding/margin,在高密度屏上效果会严重偏大。
解法:统一用 this.getUIContext().px2vp() 转换。养成习惯,凡是来自窗口 API 的数值,用到布局里之前一律过一遍 px2vp。
坑五:setSpecificSystemBarEnabled 参数名写错
'navigationIndicator' 容易写成 'navigation' 或 'navigationBar',写错了不会报错但就是不生效。
正确参数:
- 状态栏:
'status' - 导航条:
'navigationIndicator'
推荐实践方案
针对不同类型的页面,推荐的沉浸式适配策略:
普通页面(首页、列表、详情)
组件安全区方案。默认模式,不需要 setWindowLayoutFullScreen。顶部标题栏加 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]),底部 Tab 栏加 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])。滚动容器的背景也可以延伸。
视频播放页
全屏布局 + 隐藏系统栏。播放时:
win.setWindowLayoutFullScreen(true);
win.setSpecificSystemBarEnabled('status', false);
win.setSpecificSystemBarEnabled('navigationIndicator', false);
暂停/显示控制栏时恢复系统栏:
win.setSpecificSystemBarEnabled('status', true);
win.setSpecificSystemBarEnabled('navigationIndicator', true);
相机/扫码页
全屏布局 + 保留系统栏。取景器需要铺满全屏,但状态栏信息(时间、电量)在非拍摄状态下仍需可见。
win.setWindowLayoutFullScreen(true);
// 不隐藏系统栏,但取景器组件用 expandSafeArea 延伸到避让区
折叠屏适配页面
全屏布局 + avoidAreaChange 监听。折叠/展开时避让区尺寸变化,需要动态调整:
mainWindow.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
AppStorage.setOrCreate('statusBarHeight', data.area.topRect.height);
} else if (data.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
AppStorage.setOrCreate('navBarHeight', data.area.bottomRect.height);
}
});
页面中用 @StorageProp 绑定,配合 px2vp 实时转换,避让区变化时 UI 自动适配。
总结
沉浸式适配在 HarmonyOS NEXT 上有两条清晰的路径:
- 窗口全屏布局(
setWindowLayoutFullScreen):内容铺满全屏,手动处理避让。适合需要深度沉浸的场景。 - 组件安全区(
expandSafeArea):默认避让,只延伸背景绘制。适合大多数应用。
两者不冲突——你可以全局开全屏布局,同时在具体组件上用 expandSafeArea 做精细控制。也可以不开全屏布局,纯靠 expandSafeArea 实现视觉沉浸。
不管选哪条路,折叠屏适配都是必须考虑的。avoidAreaChange 监听 + px2vp 转换 + @StorageProp 响应式绑定,这套组合拳能覆盖绝大多数动态避让场景。
隐藏系统栏用 setSpecificSystemBarEnabled,注意参数名——'status' 和 'navigationIndicator',写错不报错但不生效,这个坑踩过一次就记住了。
Demo 代码在 entry/src/main/ets/pages/ImmersiveDemo.ets,可以跑起来实际看看各开关的效果,比看文章直观得多。
参考文档:HarmonyOS 5 窗口沉浸式效果
更多推荐



所有评论(0)