在这里插入图片描述

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

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

  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,请留言,我帮你踩坑!
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐