告别手动注册窗口监听:用 @Env 获取 WINDOW_AVOID_AREA【鸿蒙心迹】

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的App!
📌 关注本专栏《零基础学鸿蒙开发》,一起变强!
每一节内容我都会持续更新,配图+代码+解释全都有,欢迎点个关注,不走丢,我是小白酷爱学习,我们一起上路 🚀
先说场景
做全面屏适配时,有一个绕不过去的问题:页面内容要怎么知道顶部状态栏和底部导航条占了多少空间?
最直接的答案是获取窗口规避区(Avoid Area)。规避区描述的是屏幕上被系统 UI 占据的区域,包括状态栏、导航条、刘海屏开孔等,应用的内容布局应当主动回避这些区域,或者在需要背景延伸时,至少要知道这些区域的尺寸再做对齐。
问题在于,获取这个信息的方式在 HarmonyOS API 的演进过程中发生了变化。API 12 之前,开发者通常在 Ability 里手动注册窗口事件监听,通过 AppStorage 把尺寸数据跨层透传到页面组件。这套机制能用,但代码量可观,涉及到监听注册、状态存储、跨层绑定三个环节,维护成本不低。
API 12 起,SystemProperties 新增了 WINDOW_AVOID_AREA 属性,配合 API 11 引入的 @Env 装饰器,可以在组件内直接声明一个与规避区自动同步的状态变量,框架负责其余的事情。
这篇文章只比较这两种写法,看清楚各自做了什么、省掉了什么,再说清楚用哪个更合适。
一、先把涉及的接口版本对清楚
在动手之前,有几个版本约束需要确认。
手动监听方式涉及的核心接口:
window.getAvoidArea(type: AvoidAreaType):从 API 9 起支持,属于@kit.ArkUI中的window模块。window.on('avoidAreaChange', callback):同为 API 9 起支持。window.AvoidAreaType:枚举类型,TYPE_SYSTEM对应状态栏,TYPE_NAVIGATION_INDICATOR对应底部导航条。
@Env 方式涉及的接口:
@Env装饰器:API 11 起引入,定义在@kit.ArkUI的Environment和SystemProperties中。SystemProperties.WINDOW_AVOID_AREA:API 12 起新增,类型为AvoidAreaOptions。AvoidAreaOptions:包含topRect、bottomRect、leftRect、rightRect、avoidAreaType五个字段,每个Rect字段为RectOptions类型(含left、top、width、height)。
以上信息来源:华为开发者联盟 SystemProperties 文档、@Env 装饰器文档、Display Cutout 适配最佳实践。
如果工程的 compileSdkVersion 低于 12,SystemProperties.WINDOW_AVOID_AREA 不可用,只能走手动监听。
二、手动监听写法:它做了哪些事
手动监听的完整链路横跨两个文件:EntryAbility.ets 和页面组件。
第一步:在 Ability 里拿到窗口实例并注册监听。
// EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
return;
}
});
// 拿到主窗口实例
let windowClass = windowStage.getMainWindowSync();
// 页面加载时先取一次当前规避区
AppStorage.setOrCreate(
'topAvoidArea',
windowClass.getAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect
);
AppStorage.setOrCreate(
'bottomAvoidArea',
windowClass.getAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect
);
// 注册监听,后续规避区变化时更新
windowClass.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
AppStorage.setOrCreate('topAvoidArea', data.area.topRect);
} else if (data.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
AppStorage.setOrCreate('bottomAvoidArea', data.area.bottomRect);
}
});
}
}
第二步:在页面组件里通过 AppStorage 读取数据。
// Index.ets
import { window } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@StorageProp('topAvoidArea') topAvoidArea: window.Rect = {
left: 0, top: 0, width: 0, height: 0
};
@StorageProp('bottomAvoidArea') bottomAvoidArea: window.Rect = {
left: 0, top: 0, width: 0, height: 0
};
build() {
Column() {
Row()
.width('100%')
.height(this.topAvoidArea.height)
.backgroundColor(Color.Blue)
.opacity(0.3)
Text('主内容区')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.fontSize(24)
Row()
.width('100%')
.height(this.bottomAvoidArea.height)
.backgroundColor(Color.Blue)
.opacity(0.3)
}
.width('100%')
.height('100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
}
三、@Env 写法:能省掉什么
用 @Env(SystemProperties.WINDOW_AVOID_AREA) 之后,上面那两个文件可以压缩成一个。
Ability 里不需要做任何与规避区有关的操作。页面组件里直接声明变量:
// Index.ets(使用 @Env 方式)
import { Environment, SystemProperties } from '@kit.ArkUI';
@Entry
@Component
struct Index {
// 声明后框架自动从 Environment 同步,规避区变化时组件自动重新渲染
@Env(SystemProperties.WINDOW_AVOID_AREA) avoidArea: AvoidAreaOptions = {
topRect: { left: 0, top: 0, width: 0, height: 0 },
bottomRect: { left: 0, top: 0, width: 0, height: 0 },
leftRect: { left: 0, top: 0, width: 0, height: 0 },
rightRect: { left: 0, top: 0, width: 0, height: 0 },
avoidAreaType: AvoidAreaType.TYPE_SYSTEM
};
build() {
Column() {
Row()
.width('100%')
.height(this.avoidArea.topRect.height)
.backgroundColor(Color.Blue)
.opacity(0.3)
Text('主内容区')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.fontSize(24)
Row()
.width('100%')
.height(this.avoidArea.bottomRect.height)
.backgroundColor(Color.Blue)
.opacity(0.3)
}
.width('100%')
.height('100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
}
真正需要关注的地方只有两个:
@Env(SystemProperties.WINDOW_AVOID_AREA)的参数是SystemProperties上的枚举键,不是字符串字面量。- 变量的初始值必须写完整的
AvoidAreaOptions结构,四个方向的RectOptions都要给默认值,否则在规避区数据还没从框架传入之前,组件读到的会是undefined,可能导致高度渲染为 NaN。
avoidAreaType 字段的初始值写 AvoidAreaType.TYPE_SYSTEM 只是一个占位,实际值会由框架覆盖,不影响运行结果,但写得准确一些可以减少阅读代码时的歧义。
四、两种写法拆开比较
从代码结构角度看,差异体现在四个维度。
初始化时机
手动监听需要在 onWindowStageCreate 时主动调用一次 getAvoidArea 获取初始值,再调用 on('avoidAreaChange', ...) 注册监听。如果只注册了监听而没有主动取初始值,页面首次渲染时 AppStorage 里是空的,可能出现布局闪烁。
@Env 方式不需要操心这件事,框架在组件初始化时会把当前值作为初始状态注入,随后变化时自动触发更新。
状态管理位置
手动监听的数据通过 AppStorage 存储,整个应用共享一份,key 是字符串,拼错了编译不报错。
@Env 的数据存在 Environment 里,key 来自 SystemProperties 的属性名,有类型约束。
跨文件耦合
手动监听方式,Ability 写监听,页面写 @StorageProp,两边都要改才能维护。
@Env 方式整个逻辑收在组件内,Ability 里不留痕迹。
灵活性
这是手动监听方式唯一占优的场合。如果需要同时处理多个不同类型的规避区(比如分别响应 TYPE_SYSTEM、TYPE_NAVIGATION_INDICATOR、TYPE_CUTOUT 三种类型),手动监听里 data.type 的判断天然支持,@Env 的 WINDOW_AVOID_AREA 目前封装的是通用规避区数据,没有提供按类型分拆的多变量声明方式。如果业务需要精细区分不同类型,手动监听仍然是更直接的选择。
| 对比维度 | 手动监听 | @Env |
|---|---|---|
| 支持 API | 9+ | 12+(WINDOW_AVOID_AREA) |
| 初始值获取 | 需手动调用 | 框架自动注入 |
| 状态存储 | AppStorage(手动维护) | Environment(框架管理) |
| 文件范围 | Ability + 页面 | 仅页面组件 |
| 多类型规避区分拆 | 支持 | 不直接支持 |
| 代码量 | 较多 | 较少 |
五、几个值得单独关注的点
WINDOW_AVOID_AREA 与 KEYBOARD_AVOID_AREA 不是同一个属性
SystemProperties 在 API 14 起还增加了 KEYBOARD_AVOID_AREA(软键盘规避区)和 NAVIGATION_INDICATOR_AVOID_AREA(导航条规避区)两个独立属性。WINDOW_AVOID_AREA 对应的是系统通用规避区,两者服务于不同场景,不要混用。如果项目需要单独响应软键盘弹出,API 14 起可以考虑 KEYBOARD_AVOID_AREA。
@Env 只能在 @Component 装饰的自定义组件中使用
官方文档明确限定了这一点。如果试图在非组件的普通类或工具函数里用 @Env,运行时不会生效。
expandSafeArea 是前提条件
无论哪种方式,如果没有调用 .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) 让背景延伸到系统区域,规避区的高度值虽然能读到,但主动对齐的意义就变小了——系统本身已经把内容推开了,你再手动留出等高的间距反而是双重偏移。这两件事要配套理解。
@Env 是单向同步
Environment → @Env 是单向的,组件内无法反向修改规避区的值(也没有理由这样做)。这与 @StorageProp 的行为一致,但理解清楚方向有助于排查数据不更新的问题:如果值没变,先检查框架是否收到了系统环境变更通知,而不是检查组件侧的写法。
六、排查优先顺序
如果用了 @Env 方式却发现规避区高度始终是初始值 0,按以下顺序排查:
-
确认 API Level:工程
compileSdkVersion是否达到 12?低于 12 时WINDOW_AVOID_AREA不存在,变量会一直是初始值。 -
确认 import 路径:
Environment和SystemProperties需要从@kit.ArkUI导入,不是单独的@ohos.environment。 -
确认初始值结构完整:
AvoidAreaOptions的四个方向Rect是否都给了默认值?缺少字段可能导致渲染时读到undefined。 -
确认
@Env写在@Component装饰的组件内:不能写在普通 class 或函数里。 -
确认
expandSafeArea已配置:没有扩展安全区域的情况下,系统默认已经为内容留出了偏移,此时观察到的布局效果和规避区高度不直接对应,容易产生混淆。
如果手动监听方式发现初次渲染时数据是空的,八成是 onWindowStageCreate 里只注册了监听、没有主动调用 getAvoidArea 获取初始值。监听只能覆盖「变化」事件,不会在注册时触发一次回调推送当前状态。
开发经验总结
@Env(SystemProperties.WINDOW_AVOID_AREA)从 API 12 起可用,对应AvoidAreaOptions类型,组件内声明即生效,框架负责初始化和后续同步。- 手动监听方式(
window.on('avoidAreaChange', ...))从 API 9 起支持,适合需要分类处理多种规避区类型的场景,代价是要在 Ability 和页面两侧各维护代码。 - 两种方式都需要配合
.expandSafeArea()使用,才能使背景延伸和规避区对齐产生实际意义。 - 使用
@Env方式时,初始值的结构完整性比用什么占位值更重要,防的是首帧渲染时字段为undefined。 KEYBOARD_AVOID_AREA(API 14)和WINDOW_AVOID_AREA(API 12)是独立属性,软键盘场景不要混用。
如果你的项目已经在用手动监听方式,可以检查一下:除了状态栏和导航条,是否还有对 TYPE_CUTOUT 的处理逻辑?如果只处理了两种常规类型,迁移到 @Env 方式基本没有障碍,值得考虑一下。
❤️ 如果本文帮到了你…
- 请点个赞,让我知道你还在坚持阅读技术长文!
- 请收藏本文,因为你以后一定还会用上!
- 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
更多推荐




所有评论(0)