你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的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])}}真正需要关注的地方只有两个:
@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,请留言,我帮你踩坑!