详解)
一、开篇这个 API 很容易被误用HarmonyOS 开发里窗口避让区域AvoidArea是一个经常被提及但实现效果参差不齐的能力。很多人第一次接触时以为只是单纯的“把内容往下挪一点”结果发现真机上的导航栏、状态栏、挖孔区域经常把按钮或重要信息挡住。官方文档描述了on(avoidAreaChange)和getAvoidArea这两个方法但实际开发中单纯调用它们并不够——避让区域的变化时机、类型区分、与页面布局的同步机制才是真正容易出问题的地方。这篇内容就集中解决一个问题如何让窗口内容自动避开系统UI状态栏、导航栏、挖孔屏的占用并且在设备旋转、手势切换等场景下布局能实时更新。二、避让区域AvoidArea解决什么问题在 HarmonyOS 手机上系统UI会占用一部分屏幕空间状态栏显示时间、电量、信号导航栏三键或手势条区域挖孔/刘海区域摄像头、传感器如果应用直接绘制这些区域就会出现内容被遮挡的问题。早期一些应用通过硬编码固定边距来适配但这种方法在面对不同设备平板、折叠屏、挖孔位置不同、不同导航方式手势 vs 三键时非常脆弱。避让区域机制则是系统主动告诉你“哪些位置被占了你的内容应该避开这些区域”。它通过AvoidAreaType区分不同类型的系统元素类型说明场景TYPE_SYSTEM系统UI如状态栏、导航栏顶部状态栏 底部导航栏TYPE_CUTOUT屏幕挖孔区域打孔屏、刘海屏TYPE_SYSTEM_GESTURE系统手势区域手势条区域TYPE_KEYBOARD软键盘区域键盘弹起核心思路不直接硬编码边距监听避让区域变化用事件驱动去更新布局边距。三、环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机支持手势、三键、挖孔屏四、核心实现自动避让的示例页面功能目标页面内容自动避开状态栏和导航栏。当导航方式从“手势”切换为“三键”时底部边距自动调整。当设备横竖屏切换时避让区域重新计算。4.1 获取窗口实例并注册避让区域监听创建一个WindowManagerService.ets专门管理窗口相关的操作// services/WindowManagerService.etsimport{window}fromkit.ArkUI;exportclassWindowManagerService{privatestaticinstance:WindowManagerService;privatemainWindow:window.Window|undefined;privateavoidAreaCallBack:((area:window.AvoidArea,type:window.AvoidAreaType)void)|undefined;privateconstructor(){// 单例模式}publicstaticgetInstance():WindowManagerService{if(!WindowManagerService.instance){WindowManagerService.instancenewWindowManagerService();}returnWindowManagerService.instance;}publicasyncinit(win:window.Window){this.mainWindowwin;// 注册避让区域变化监听this.mainWindow.on(avoidAreaChange,(data:window.AvoidAreaEvent){console.info(AvoidAreaChange, type:${data.type});constareawin.getWindowAvoidArea(data.type);if(this.avoidAreaCallBack){this.avoidAreaCallBack(area,data.type);}});}publicgetAvoidArea(type:window.AvoidAreaType):window.AvoidArea{if(!this.mainWindow){return{topRect:{left:0,top:0,width:0,height:0},bottomRect:{left:0,top:0,width:0,height:0}};}returnthis.mainWindow.getWindowAvoidArea(type);}publiconAvoidAreaChange(callback:(area:window.AvoidArea,type:window.AvoidAreaType)void){this.avoidAreaCallBackcallback;}publicdestroy(){if(this.mainWindow){this.mainWindow.off(avoidAreaChange);}this.avoidAreaCallBackundefined;}}说明封装在一个 Service 类里避免在页面组件里直接引用window实例方便管理和测试。监听avoidAreaChange事件每次变化时主动调用回调通知页面更新。getWindowAvoidArea方法是同步的可以直接返回当前避让区域。注意在页面销毁时必须调用destroy()去掉监听否则组件回收后回调还有引用会导致内存泄漏。4.2 页面组件使用避让区域更新布局创建pages/AvoidAreaDemo.ets// pages/AvoidAreaDemo.etsimport{window}fromkit.ArkUI;import{WindowManagerService}from../services/WindowManagerService;EntryComponentstruct AvoidAreaDemo{// 分别存储顶部和底部边距StatetopInset:number0;StatebottomInset:number0;StateleftInset:number0;StaterightInset:number0;privatewms:WindowManagerServiceWindowManagerService.getInstance();aboutToAppear():void{// 获取当前窗口实例constcontextgetContext(this)asUIAbilityContext;// 注意获取窗口实例需要从 UIAbility 中的 context 拿到// 这里为了演示假设外部已经初始化了窗口实例// 实际项目中建议在 UIAbility 的 onWindowStageCreate 中初始化constwinwindow.getLastWindow(context);// 需要传入 contextif(win){this.wms.init(win);// 注册回调this.wms.onAvoidAreaChange((area,type){this.updateInsets(type,area);});// 主动获取一次初始化边距constsystemAreathis.wms.getAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);this.updateInsets(window.AvoidAreaType.TYPE_SYSTEM,systemArea);}}updateInsets(type:window.AvoidAreaType,area:window.AvoidArea):void{if(typewindow.AvoidAreaType.TYPE_SYSTEM||typewindow.AvoidAreaType.TYPE_SYSTEM_GESTURE){// 顶部边距状态栏区域this.topInsetarea.topRect.height;// 底部边距导航栏/手势条区域this.bottomInsetarea.bottomRect.height;// 左右边距处理折叠屏等场景this.leftInsetarea.leftRect.width;this.rightInsetarea.rightRect.width;}}build(){Column(){// 顶部留白区模拟状态栏Column().width(100%).height(this.topInset).backgroundColor(#33000000)// 主内容区域Column(){Text(这是主内容区域).fontSize(24)Text(顶部边距${this.topInset}px)Text(底部边距${this.bottomInset}px)Text(左边距${this.leftInset}px)Text(右边距${this.rightInset}px)}.width(100%).height(100%).justifyContent(FlexAlign.Center)// 底部留白区模拟导航栏Column().width(100%).height(this.bottomInset).backgroundColor(#33000000)}.width(100%).height(100%).padding({left:this.leftInset,right:this.rightInset}).backgroundColor(Color.White)}aboutToDisappear():void{this.wms.destroy();}}这段代码做了什么在aboutToAppear中获取窗口实例初始化 WindowManagerService。注册避让区域变化回调当系统UI变化时更新State变量。在build方法中通过State变量动态控制顶部、底部、左右边距。页面销毁时清除监听避免泄漏。为什么这样写使用State驱动 UIArkUI 会自动重新渲染。把监听逻辑从 UI 组件抽离到 Service 层当有多个页面需要避让时可以复用。主动调用一次getAvoidArea初始化避免首次加载时没有任何边距信息。4.3 在 UIAbility 中初始化// entryability/EntryAbility.etsimport{UIAbility,window}fromkit.ArkUI;import{WindowManagerService}from../services/WindowManagerService;exportdefaultclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{// 获取主窗口实例constmainWindowwindowStage.getMainWindowSync();// 初始化 WindowManagerService传入窗口实例WindowManagerService.getInstance().init(mainWindow);// 加载页面windowStage.loadContent(pages/AvoidAreaDemo,(err){if(err){console.error(Failed to load content:${err.code});}});}}说明在onWindowStageCreate中尽早获取窗口实例并完成注册。这样可以确保页面创建前窗口已经能监听到避让区域变化。五、踩坑记录坑1getWindowAvoidArea在页面未渲染时返回全为 0现象在aboutToAppear中直接调用getWindowAvoidArea返回的area.topRect.height为 0。原因getWindowAvoidArea是同步方法但窗口的避让区域信息需要等到页面渲染完成后才完整。在aboutToAppear阶段页面还在构建中系统尚未完成布局因此返回的避让区域信息不完整。解法不要在aboutToAppear中获取第一帧数据。改为使用postTask等延迟执行或监听on(avoidAreaChange)事件系统会在首次渲染后触发一次。推荐做法在aboutToAppear中只注册监听首次数据由on(avoidAreaChange)的回调提供。坑2on(avoidAreaChange)在 API 12 和 API 11 中的行为不同现象在 API 11 的设备上当用户从手势导航切换到三键导航时avoidAreaChange事件不会触发导致底部边距没有更新。但在 API 12 的设备上切换时能正常触发。原因这是原生的 API 行为差异。API 11 下avoidAreaChange只会在窗口创建、销毁、旋转等场景下触发而导航方式的切换属于系统UI变更但它不在这个事件的通知列表中。解法可以在 API 11 设备上结合window.on(windowSizeChange)事件在窗口尺寸变化时主动重新获取一次避让区域。// 在 init 方法中补充this.mainWindow.on(windowSizeChange,(){constareathis.mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);if(this.avoidAreaCallBack){this.avoidAreaCallBack(area,window.AvoidAreaType.TYPE_SYSTEM);}});六、最佳实践不要在 build() 中频繁调用getWindowAvoidArea。ArkUI 的 build 方法会被多次调用直接在里面获取避让区域会导致性能浪费。应该通过State绑定一次并在回调中更新。避让区域的类型要区分使用。普通应用只需要监听TYPE_SYSTEM和TYPE_SYSTEM_GESTURE即可。TYPE_CUTOUT只在挖孔屏设备上有值且值可能为 0。如果你的应用是阅读器或全屏播放器建议额外关心TYPE_CUTOUT。页面销毁后必须清理监听。如果窗口实例还在但页面组件已经销毁回调里的 UI 操作可能会报错。要么在aboutToDisappear调用off去掉监听要么在回调里加一个标志位判断页面是否已销毁。七、Demo 入口// pages/Index.etsimport{WindowManagerService}from../services/WindowManagerService;EntryComponentstruct Index{build(){Column(){// 首页入口启动后自动跳转避让区域示例NavigateTo({url:pages/AvoidAreaDemo})}.width(100%).height(100%)}}示例代码项目地址项目地址八、FAQQ1为什么真机上避让区域正常但模拟器上一直返回 0A模拟器不支持屏幕方向切换和手势/三键导航切换避让区域数据在模拟器上可能是固定的甚至部分属性为 0。避让区域相关逻辑务必在真机上完整验证。Q2页面返回后底部边距突然消失了为什么A检查页面aboutToDisappear中是否调用了destroy()或off(avoidAreaChange)。如果页面只是被覆盖比如打开了一个半透明弹窗页面实例未被销毁但监听被误删了。建议在onPageHide和onPageShow中重新注册和恢复监听。Q3我在全屏视频播放页面里为什么设置setWindowLayoutFullScreen(true)后避让区域依然存在A全屏模式下状态栏和导航栏会隐藏或变为半透明但避让区域仍然会返回一个较小的值比如状态栏高度变为 0但导航区域可能保留 24dp 左右的手势条。如果你希望内容完全覆盖所有区域可以忽略避让区域但同时要处理好交互穿透的问题否则用户可能在状态栏区域触发手势。建议全屏模式下结合getWindowAvoidRectAvoidArea的结果来判断是否需要忽略顶部区域。