在这里插入图片描述
在这里插入图片描述

HarmonyOS ArkTS 安全区域(SafeArea)适配完全指南:从原理到实战

一、为什么需要安全区域适配?

随着全面屏、刘海屏、挖孔屏等异形屏设备的普及,以及系统导航手势的全面推广,应用内容不再天然占据整个屏幕——系统状态栏、导航手势区域、屏幕挖孔等「非安全区域」会遮挡应用内容。

典型的非安全区域包括:

  • 顶部状态栏:显示时间、电量、信号,高度约 24~48vp
  • 底部导航条/手势指示条:三键导航或手势横线区域
  • 屏幕刘海/挖孔:前置摄像头区域的异形切割
  • 圆角屏幕边缘:四角圆角裁剪

「安全区域适配」就是要让内容自动避开这些区域,同时在全屏沉浸场景(视频播放器、游戏、阅读器)中,能够有选择地将背景延伸到屏幕边缘。


二、核心技术概念

2.1 expandSafeArea —— ArkTS 的安全区域控制

在 ArkTS 中,expandSafeArea 是组件层面的安全区域控制属性。它决定组件是否允许扩展到非安全区域。

关键语义区分expandSafeArea 不是「避开」安全区域,而是「允许进入」非安全区域。默认组件已被约束在安全区域内。

这与 Flutter 的 SafeArea widget 正好相反——Flutter 默认内容可铺满全屏,SafeArea 通过 padding 推离非安全区域;ArkTS 默认内容在安全区域内,expandSafeArea 允许越界。

expandSafeArea 接收两个数组参数:

参数 类型 说明
types Array<SafeAreaType> 要扩展的安全区域类型(可组合)
edges Array<SafeAreaEdge> 要扩展的边(默认全部)

SafeAreaType 枚举SYSTEM(状态栏/导航栏)、CUTOUT(刘海/挖孔)、KEYBOARD(键盘)

SafeAreaEdge 枚举TOPBOTTOMSTART(LTR 为左)、END(LTR 为右)

使用示例:

// 允许扩展到所有系统安全区域
Column().expandSafeArea()

// 仅允许顶部扩展到系统和刘海区域
Stack().expandSafeArea(
  [SafeAreaType.SYSTEM, SafeAreaType.CUTOUT],
  [SafeAreaEdge.TOP]
)

2.2 安全区域边距(SafeAreaInsets)

通过 getWindowAvoidArea() API 获取精确的安全区域边距数值:

const systemArea = mainWin.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
// systemArea.topRect.height  → 顶部安全边距
// systemArea.bottomRect.height → 底部安全边距

AvoidAreaType 枚举TYPE_SYSTEM(系统级)、TYPE_NAVIGATION_INDICATOR(手势指示条)、TYPE_CUTOUT(刘海/挖孔)

实际开发中需要组合多个类型取最大值:

const safeAreaTop = Math.max(systemArea.topRect.height, cutoutArea.topRect.height);
const safeAreaBottom = Math.max(
  systemArea.bottomRect.height, navIndicatorArea.bottomRect.height
);

2.3 AppStorage —— 全局状态共享

AppStorage 在安全区域方案中充当了 Flutter 中 MediaQuery 的角色:

  • Ability 层(EntryAbility):启动时获取安全区域边距,存入 AppStorage
  • Page 层(Index):通过 @StorageLink 自动读取,响应式绑定
// Ability 写入
AppStorage.setOrCreate<number>('safeAreaTop', safeAreaTop);

// Page 读取(响应式,数据变化自动刷新 UI)
@StorageLink('safeAreaTop') safeAreaTop: number = 0;

三、Flutter 与 ArkTS 概念对照

功能点 Flutter ArkTS
避开安全区域 SafeArea(child: ...) 默认行为(组件自动在安全区域内)
扩展到安全区域 移除 SafeArea .expandSafeArea()
获取边距 MediaQuery.of(context).padding getWindowAvoidArea() + AppStorage
状态栏控制 SystemChrome.setEnabledSystemUIMode(...) window.setWindowSystemBarEnable(...)
全屏模式 SystemChrome.setEnabledSystemUIMode(immersive) setWindowLayoutFullScreen(true)

布局模式对比:

Flutter 方案:

Stack(
  children: [
    Container(/* 背景层,铺满全屏 */),
    SafeArea(child: Column(/* 内容层,自动 padding */)),
  ],
)

ArkTS 等效实现:

Stack() {
  Column()                          // 背景层
    .expandSafeArea([SYSTEM, CUTOUT])
  Column() { /* 内容层 */ }         // 内容层
    .padding({top: safeAreaTop + 8, bottom: safeAreaBottom + 8})
}

四、代码实现深度解析

4.1 EntryAbility —— 窗口配置与数据注入

核心流程如下:

onWindowStageCreate
  ├─ getMainWindowSync() → 获取主窗口
  ├─ setWindowLayoutFullScreen(true) → 全屏布局
  │    ├─ setWindowSystemBarEnable([]) → 隐藏状态栏
  │    ├─ updateSafeAreaInsets() → 获取安全区域
  │    │    ├─ getWindowAvoidArea(TYPE_SYSTEM)
  │    │    ├─ getWindowAvoidArea(TYPE_NAVIGATION_INDICATOR)
  │    │    ├─ getWindowAvoidArea(TYPE_CUTOUT)
  │    │    └─ AppStorage.setOrCreate() × 4
  │    └─ loadContent() → 加载首页

关键要点

  1. setWindowLayoutFullScreenwindow.Window 的方法,不是 WindowStage 的。必须通过 getMainWindowSync() 获取 Window 对象后再调用。

  2. 各方向安全边距取多个 AvoidAreaType 的最大值,因不同设备的非安全区域来源不同:

    • 刘海屏:cutoutArea.topRect.height 含状态栏+刘海
    • 普通屏:systemArea.topRect.height 仅状态栏
    • 取最大值确保在各种设备上都正确
  3. 所有回调参数必须显式标注 BusinessError | null 类型,满足 ArkTS 严格模式要求。

4.2 Index 页面 —— 三种适配模式

模式一:expandSafeArea 自动扩展

适用于全屏背景、视频播放器、游戏启动画面。背景层扩展至屏幕所有边缘营造沉浸感。

Column()
  .width('100%').height('100%')
  .expandSafeArea([SafeAreaType.SYSTEM, SafeAreaType.CUTOUT],
    [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END])
  .linearGradient({ direction: GradientDirection.Bottom, colors: [...] })

注意SafeAreaEdge.LEFT/RIGHT 不存在,应使用 START/END(支持 RTL 布局)。

模式二:手动 Padding 适配

适用于需要精确控制每个方向边距的内容区域。边距值来源于 AppStorage

Column() { /* 实际内容 */ }
  .width('100%').height('100%')
  .padding({
    top: this.safeAreaTop + 8,
    bottom: this.safeAreaBottom + 8,
    left: this.safeAreaLeft + 16,
    right: this.safeAreaRight + 16
  })

@StorageLink 实现响应式绑定,当安全区域数据变化时 UI 自动更新。

模式三:混合模式(推荐)

结合上述两种方案的优点,适用于绝大多数全屏应用:

Stack
  ├─ 背景层:expandSafeArea() → 全屏沉浸
  └─ 内容层:padding(safeAreaInsets) → 避开非安全区域

4.3 组件内实时查询安全区域

在状态栏切换、设备旋转等场景下,可以实时查询安全区域:

const uiContext = this.getUIContext();
if (!uiContext) return;                    // 必须 null 检查
const hostCtx = uiContext.getHostContext();
if (!hostCtx) return;                      // 必须 null 检查(返回 Context | undefined)

window.getLastWindow(hostCtx).then((win) => {
  const systemArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
  this.localInsets = {
    top: systemArea.topRect.height,
    bottom: systemArea.bottomRect.height,
    left: systemArea.leftRect.width,
    right: systemArea.rightRect.width
  };
});

双重 null 检查的必要性getUIContext() 在组件生命周期早期(如 aboutToAppear 刚触发时)可能返回 undefinedgetHostContext() 同样可能返回 undefined。两个调用都做检查是 ArkTS 严格类型安全的要求。


五、沉浸模式切换实现

5.1 状态栏切换 API

// 隐藏状态栏(沉浸模式)
win.setWindowSystemBarEnable([]);

// 显示状态栏
win.setWindowSystemBarEnable(['status']);

5.2 完整切换逻辑

toggleStatusBar(): void {
  this.statusBarVisible = !this.statusBarVisible;
  const uiContext = this.getUIContext();
  if (!uiContext) return;
  const hostCtx = uiContext.getHostContext();
  if (!hostCtx) return;

  window.getLastWindow(hostCtx).then((win) => {
    if (this.statusBarVisible) {
      win.setWindowSystemBarEnable(['status']);
    } else {
      win.setWindowSystemBarEnable([]);
    }
    this.queryLocalSafeArea();  // 切换后重新查询安全区域
  });
}

5.3 全屏布局与沉浸模式的区别

功能 全屏布局 隐藏状态栏
API setWindowLayoutFullScreen(true) setWindowSystemBarEnable([])
效果 内容可延伸到状态栏区域 状态栏元素隐藏
安全区域 系统仍保留安全区域 边距变为 0
适用 所有全屏应用的基础配置 需要完全沉浸时使用

推荐配置策略

  • 通用全屏应用(社交/资讯 App):fullScreen=true + 保留状态栏显示 → 内容可延伸,但用户能看到时间通知
  • 沉浸式全屏应用(视频/游戏):fullScreen=true + 隐藏状态栏 → 完全沉浸,提供手动切换按钮

六、不同设备的适配策略

设备类型 顶部边距 底部边距 特殊说明
刘海屏手机 状态栏 + 刘海(~54vp) 导航栏/手势条(~48/24vp) 横向时注意左右边距
挖孔屏手机 状态栏(~24vp) 同上 挖孔在左上/右上角时 left/right > 0
有导航栏手机 状态栏(~24vp) 导航栏(~48vp) 切换手势模式后底部边距减小
平板/折叠屏 状态栏(~24vp) 导航栏 + 手势条 展开/折叠后需重新查询安全区域

七、常见问题 FAQ

Q1:子组件继承了父组件的 expandSafeArea 怎么办?

父组件设置 expandSafeArea 后,子组件默认继承此设置。如需子组件不扩展,显式设置空数组覆盖:

Text('内容').expandSafeArea([])  // 禁止扩展

Q2:aboutToAppear 中获取 UIContext 失败?

aboutToAppear 时 UI 上下文的初始化可能尚未完成。始终对返回值做 null 检查,或移入 onPageShow 中调用。

Q3:旋转屏幕后安全区域不更新?

在 Ability 中监听 onConfigurationUpdated 回调重新获取安全区域,或在 Page 层使用 onConfigurationUpdate 生命周期。

Q4:setWindowLayoutFullScreen 被弃用?

在较新 API 版本中该 API 被标注为弃用但仍有等效功能,可使用 Promise 风格的调用方式:

mainWin.setWindowLayoutFullScreen(true)
  .then(() => { /* 成功 */ })
  .catch((err) => { /* 失败 */ });

Q5:SafeAreaEdge.LEFT/RIGHT 不存在?

使用 STARTEND 替代,它们自动适配 LTR/RTL 布局方向。


八、最佳实践总结

架构建议

EntryAbility                         Index Page
  │                                     │
  ├─ getMainWindowSync()                ├─ @StorageLink 读取安全区域
  ├─ setWindowLayoutFullScreen()        ├─ Stack
  ├─ setWindowSystemBarEnable()         │   ├─ 背景 (expandSafeArea)
  ├─ getWindowAvoidArea() × 3           │   └─ 内容 (padding)
  ├─ AppStorage.setOrCreate() × 4       └─ queryLocalSafeArea() (实时查询)
  └─ loadContent()

代码规范

  1. 类型安全:所有回调参数加显式类型 BusinessError | null,避免 any/unknown
  2. Null 检查:对 getUIContext()getHostContext() 始终做双重 null 检查
  3. 异常处理:可能抛出异常的 API(如 getMainWindowSync())用 try-catch 包裹
  4. 资源引用:使用 $r('app.xxx.xxx') 而非硬编码,便于多分辨率适配
  5. 关注点分离:Ability 层获取和分发数据,Page 层消费和应用数据

测试建议

  1. 务必在真机上测试(模拟器无法准确模拟刘海/挖孔安全区域)
  2. 覆盖刘海屏、挖孔屏、普通屏、平板等不同屏幕形态
  3. 测试竖屏和横屏两种方向的适配效果
  4. 测试状态栏显示/隐藏切换、三键导航/手势导航切换后的布局正确性

九、总结

HarmonyOS ArkTS 的安全区域适配核心机制包括:

  1. expandSafeArea:控制组件是否扩展到非安全区域(默认在安全区域内)
  2. getWindowAvoidArea():获取精确的安全区域边距(支持 SYSTEM / NAVIGATION_INDICATOR / CUTOUT 三种类型)
  3. AppStorage:在 Ability 和 Page 之间共享安全区域数据(等效 Flutter 的 MediaQuery)

推荐的「混合模式」——背景层用 expandSafeArea 扩展到全屏、内容层用 padding 避开非安全区域——能在视觉沉浸和内容安全之间取得最佳平衡。开发者可根据应用场景(社交 App、视频播放器、游戏等)灵活选择或组合上述适配模式。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐