news 2026/10/27 1:49:10

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
告别手动注册窗口监听:用 @Env 获取 WINDOW_AVOID_AREA【鸿蒙心迹】

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的App!
📌关注本专栏《零基础学鸿蒙开发》,一起变强!
每一节内容我都会持续更新,配图+代码+解释全都有,欢迎点个关注,不走丢,我是小白酷爱学习,我们一起上路 🚀

全文目录:

    • 先说场景
    • 一、先把涉及的接口版本对清楚
    • 二、手动监听写法:它做了哪些事
    • 三、@Env 写法:能省掉什么
    • 四、两种写法拆开比较
    • 五、几个值得单独关注的点
    • 六、排查优先顺序
    • 开发经验总结

先说场景

做全面屏适配时,有一个绕不过去的问题:页面内容要怎么知道顶部状态栏和底部导航条占了多少空间?

最直接的答案是获取窗口规避区(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.etsimport{AbilityConstant,UIAbility,Want}from'@kit.AbilityKit';import{window}from'@kit.ArkUI';exportdefaultclassEntryAbilityextendsUIAbility{onWindowStageCreate(windowStage:window.WindowStage):void{windowStage.loadContent('pages/Index',(err)=>{if(err.code){return;}});// 拿到主窗口实例letwindowClass=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);}elseif(data.type===window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR){AppStorage.setOrCreate('bottomAvoidArea',data.area.bottomRect);}});}}

第二步:在页面组件里通过 AppStorage 读取数据。

// Index.etsimport{window}from'@kit.ArkUI';@Entry@Componentstruct 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@Componentstruct 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])}}

真正需要关注的地方只有两个:

  1. @Env(SystemProperties.WINDOW_AVOID_AREA)的参数是SystemProperties上的枚举键,不是字符串字面量。
  2. 变量的初始值必须写完整的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
支持 API9+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,按以下顺序排查:

  1. 确认 API Level:工程compileSdkVersion是否达到 12?低于 12 时WINDOW_AVOID_AREA不存在,变量会一直是初始值。

  2. 确认 import 路径:Environment和SystemProperties需要从@kit.ArkUI导入,不是单独的@ohos.environment。

  3. 确认初始值结构完整:AvoidAreaOptions的四个方向Rect是否都给了默认值?缺少字段可能导致渲染时读到undefined。

  4. 确认@Env写在@Component装饰的组件内:不能写在普通 class 或函数里。

  5. 确认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,请留言,我帮你踩坑!
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 1:48:59

【零基础学AI】第 18 章——用线性回归预测数值

第 18 章 用线性回归预测数值 文章目录第 18 章 用线性回归预测数值🌈 关于《AI 零基础 36 讲》1. 先把历史价格数据读对2. 建一个很简单的基线3. 单特征回归就是拟合一条直线4. 多特征模型把几项输入放进同一个式子5. 用误差回答模型错了多少6. 平均误差之外&#…

作者头像 李华
网站建设 2026/10/27 1:47:11

Python 日期处理必学技能:date.isoformat() 一招搞定标准日期格式转换

Python date 类型 isoformat() 的用法:让日期格式转换变得简单大家好,今天我们学习 Python 日期处理中一个非常实用的方法:date.isoformat()很多刚开始学习 Python 的同学,在处理日期时都会遇到类似问题:“为什么我的日…

作者头像 李华
网站建设 2026/10/27 1:45:28

Java实战项目:助贷CRM客户管理系统(SpringBoot+MyBatis)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华