【HarmonyOS】应用设置全屏和安全区域详解

GeorgeGcs
• 阅读 2

【HarmonyOS】应用设置全屏和安全区域详解

一、前言

IDE创建的鸿蒙应用,默认采取组件安全区布局方案。顶部会预留状态栏区域,底部会预留导航条区域。这就是所谓的安全区域。

如果不处理,界面效果很割裂。所以业内UI交互设计,都会设置应用为全屏布局。将页面绘制区域沾满整个界面。

或者将安全区域的颜色与应用UI设置为一致。

以上两种方式都是沉浸式布局的处理。所以全屏非沉浸式,概念不可混为一谈。 在移动应用开发中,"沉浸式效果"早已不是新鲜词,但要真正实现自然、和谐的沉浸式体验,却需要对系统布局、交互逻辑有深入理解。 【HarmonyOS】应用设置全屏和安全区域详解

二、什么是应用沉浸式效果?

简单来说,应用沉浸式效果是通过优化状态栏、应用界面与底部导航区域(导航条或三键导航)的视觉融合与交互适配,减少系统界面的突兀感,让用户注意力更聚焦于应用内容本身。

【HarmonyOS】应用设置全屏和安全区域详解

典型的界面元素包含三部分: 状态栏:显示时间、电量等系统信息的顶部区域 应用界面:承载应用核心内容的区域 底部导航区域:提供系统导航操作的底部区域

其中状态栏和底部导航区域被称为"避让区",其余区域为"安全区"。沉浸式开发的核心就是处理好这两个区域与应用内容的关系,主要涉及两类问题: UI元素避让:避免可交互元素或关键信息被避让区遮挡 视觉融合:让避让区与应用界面的颜色、风格保持一致

三、如何设置沉浸式布局?

综上所述,我们可知,设置沉浸式布局有以下两种方式,如图所示:

【HarmonyOS】应用设置全屏和安全区域详解

1、方案一:窗口全屏布局方案

该方案通过将应用界面强制扩展到全屏(包括状态栏和导航区域),实现深度沉浸式体验。适合需要在避让区放置UI元素的场景,如视频播放器控制栏、游戏界面等。

场景1:保留避让区,需处理UI避让

当需要显示状态栏和导航区域,但希望应用内容延伸至这些区域时,需通过以下步骤实现:

(1)开启全屏布局
在应用启动时调用setWindowLayoutFullScreen接口,让界面突破安全区限制:

   // EntryAbility.ets
   let windowClass = windowStage.getMainWindowSync();
   windowClass.setWindowLayoutFullScreen(true).then(() => {
     console.info("窗口已设置为全屏布局");
   });

(2)获取并监听避让区尺寸
通过getWindowAvoidArea获取状态栏和导航区域高度,并注册avoidAreaChange监听动态变化(如屏幕旋转、折叠屏展开等场景):

   // 获取状态栏高度
   let systemArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
   AppStorage.setOrCreate('statusBarHeight', systemArea.topRect.height);

   // 获取导航区域高度
   let navArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
   AppStorage.setOrCreate('navBarHeight', navArea.bottomRect.height);

   // 动态监听变化
   windowClass.on('avoidAreaChange', (data) => {
     if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
       AppStorage.setOrCreate('statusBarHeight', data.area.topRect.height);
     }
   });

(3)布局中实现UI避让
在页面布局时,通过padding将内容区与避让区隔开,避免UI重叠:

   // Index.ets
   @Component
   struct Index {
     @StorageProp('statusBarHeight') statusBarHeight: number = 0;
     @StorageProp('navBarHeight') navBarHeight: number = 0;

     build() {
       Column() {
         // 应用内容组件...
       }
       .padding({
         top: this.getUIContext().px2vp(this.statusBarHeight),
         bottom: this.getUIContext().px2vp(this.navBarHeight)
       })
     }
   }

场景2:隐藏避让区,实现纯全屏

游戏、视频类应用常需要完全隐藏状态栏和导航区域,仅在用户操作时唤起:

(1)开启全屏布局(同场景1步骤1)
(2)隐藏系统栏
通过setSpecificSystemBarEnabled接口隐藏状态栏和导航区域:

   // 隐藏状态栏
   windowClass.setSpecificSystemBarEnabled('status', false);
   // 隐藏导航区域
   windowClass.setSpecificSystemBarEnabled('navigationIndicator', false);

(3)无需额外避让处理
此时界面已完全全屏,布局中无需设置避让padding,内容可直接铺满屏幕。

2、方案二:组件安全区方案

该方案为默认布局模式,UI元素自动限制在安全区内(无需手动处理避让),仅通过延伸背景绘制实现沉浸式效果。适合大多数普通应用,尤其是不需要在避让区布局UI的场景。

默认情况下,应用UI元素会自动避开避让区,但窗口背景可全屏绘制。通过以下方式优化视觉融合:

(1)状态栏与导航区域颜色相同时
直接设置窗口背景色与应用主背景一致,实现整体沉浸:

   // EntryAbility.ets
   windowStage.getMainWindowSync().setWindowBackgroundColor('#d5d5d5');

(2)颜色不同时:使用expandSafeArea扩展绘制
对顶部/底部组件单独设置expandSafeArea属性,使其背景延伸至避让区:

   // Index.ets
   @Component
   struct Index {
     build() {
       Column() {
         // 顶部组件延伸至状态栏
         Row() {
           Text('顶部内容')
         }
         .backgroundColor('#2786d9')
         .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])

         // 中间内容区...

         // 底部组件延伸至导航区
         Row() {
           Text('底部内容')
         }
         .backgroundColor('#96dffa')
         .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
       }
     }
   }

(3)典型场景适配技巧

背景图/视频场景: 让图片组件延伸至避让区

  Image($r('app.media.bg'))
    .width('100%').height('100%')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

滚动容器场景: 通过父容器扩展实现滚动背景沉浸

  Scroll() {
    Column() {
      // 滚动内容...
    }
    .backgroundColor('rgb(213,213,213)')
  }
  .backgroundColor('rgb(213,213,213)')
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])

底部页签场景Navigation/Tabs组件默认支持背景延伸,自定义页签可手动设置:

  // 自定义底部页签
  Row() {
    // 页签按钮...
  }
  .backgroundColor('#f5f5f5')
  .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])

三、DEMO源码示例:

ImmersiveDemo/ ├── src/main/ets/ │ ├── Ability/ │ │ └── EntryAbility.ets // 应用入口,处理窗口配置 │ ├── pages/ │ │ ├── FullScreenNormal.ets // 窗口全屏布局(不隐藏避让区) │ │ ├── FullScreenHidden.ets // 窗口全屏布局(隐藏避让区) │ │ └── SafeAreaMode.ets // 组件安全区方案 │ └── common/ │ └── Constants.ets // 常量定义

应用入口配置(EntryAbility.ets)


import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { pageMap } from '../common/Constants';

export default class EntryAbility extends UIAbility {
  private mainWindow: window.Window | null = null;

  async onWindowStageCreate(windowStage: window.WindowStage) {
    // 获取主窗口实例
    this.mainWindow = windowStage.getMainWindowSync();
    if (!this.mainWindow) {
      console.error('获取主窗口失败');
      return;
    }

    // 加载首页
    windowStage.loadContent(pageMap.FULL_SCREEN_NORMAL, (err) => {
      if (err.code) {
        console.error(`加载页面失败: ${JSON.stringify(err)}`);
        return;
      }
    });

    // 初始化避让区数据监听
    this.initAvoidAreaListener();
  }

  // 初始化避让区监听
  private initAvoidAreaListener() {
    if (!this.mainWindow) return;

    // 初始获取避让区数据
    this.updateAvoidAreaData();

    // 监听避让区变化
    this.mainWindow.on('avoidAreaChange', (data) => {
      console.info(`避让区变化: ${JSON.stringify(data)}`);
      if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
        AppStorage.setOrCreate('statusBarHeight', data.area.topRect.height);
      } else if (data.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
        AppStorage.setOrCreate('navBarHeight', data.area.bottomRect.height);
      }
    });
  }

  // 更新避让区数据到全局存储
  private updateAvoidAreaData() {
    if (!this.mainWindow) return;

    try {
      // 获取状态栏区域
      const systemArea = this.mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
      AppStorage.setOrCreate('statusBarHeight', systemArea.topRect.height);

      // 获取导航栏区域
      const navArea = this.mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
      AppStorage.setOrCreate('navBarHeight', navArea.bottomRect.height);
    } catch (err) {
      console.error(`获取避让区数据失败: ${JSON.stringify(err)}`);
    }
  }

  // 切换到窗口全屏布局(不隐藏避让区)模式
  public switchToFullScreenNormal() {
    if (!this.mainWindow) return;

    // 开启全屏布局
    this.mainWindow.setWindowLayoutFullScreen(true).then(() => {
      // 显示状态栏和导航栏
      this.mainWindow?.setSpecificSystemBarEnabled('status', true);
      this.mainWindow?.setSpecificSystemBarEnabled('navigationIndicator', true);
      // 加载对应页面
      this.context?.getWindowStage().then((stage) => {
        stage.loadContent(pageMap.FULL_SCREEN_NORMAL);
      });
    });
  }

  // 切换到窗口全屏布局(隐藏避让区)模式
  public switchToFullScreenHidden() {
    if (!this.mainWindow) return;

    // 开启全屏布局
    this.mainWindow.setWindowLayoutFullScreen(true).then(() => {
      // 隐藏状态栏和导航栏
      this.mainWindow?.setSpecificSystemBarEnabled('status', false);
      this.mainWindow?.setSpecificSystemBarEnabled('navigationIndicator', false);
      // 加载对应页面
      this.context?.getWindowStage().then((stage) => {
        stage.loadContent(pageMap.FULL_SCREEN_HIDDEN);
      });
    });
  }

  // 切换到组件安全区模式
  public switchToSafeAreaMode() {
    if (!this.mainWindow) return;

    // 关闭全屏布局(使用默认安全区布局)
    this.mainWindow.setWindowLayoutFullScreen(false).then(() => {
      // 显示状态栏和导航栏
      this.mainWindow?.setSpecificSystemBarEnabled('status', true);
      this.mainWindow?.setSpecificSystemBarEnabled('navigationIndicator', true);
      // 设置窗口背景色(用于安全区方案)
      this.mainWindow?.setWindowBackgroundColor('#d5d5d5');
      // 加载对应页面
      this.context?.getWindowStage().then((stage) => {
        stage.loadContent(pageMap.SAFE_AREA_MODE);
      });
    });
  }
}

2. 常量定义(Constants.ets)


export const pageMap = {
  FULL_SCREEN_NORMAL: 'pages/FullScreenNormal',
  FULL_SCREEN_HIDDEN: 'pages/FullScreenHidden',
  SAFE_AREA_MODE: 'pages/SafeAreaMode'
};

3. 窗口全屏布局(不隐藏避让区)页面


import { EntryAbility } from '../Ability/EntryAbility';
import { pageMap } from '../common/Constants';
import { UIContext } from '@kit.ArkUI';

@Entry
@Component
struct FullScreenNormal {
  @StorageProp('statusBarHeight') statusBarHeight: number = 0;
  @StorageProp('navBarHeight') navBarHeight: number = 0;
  private uiContext: UIContext | null = null;

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Text('窗口全屏模式(不隐藏避让区)')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .color(Color.White)
      }
      .backgroundColor('#2786d9')
      .width('100%')
      .height(50)
      .justifyContent(FlexAlign.Center)

      // 内容区
      Scroll() {
        Column() {
          // 方案说明
          Text('此模式下界面延伸至状态栏和导航栏,但通过padding实现内容避让')
            .fontSize(14)
            .padding(15)
            .backgroundColor('#e6f7ff')
            .margin(10)
            .borderRadius(8)
            .width('90%')

          // 功能按钮区
          Column() {
            Button('切换到全屏隐藏模式')
              .width('80%')
              .margin(5)
              .onClick(() => {
                (getContext(this) as any).ability.switchToFullScreenHidden();
              })

            Button('切换到组件安全区模式')
              .width('80%')
              .margin(5)
              .onClick(() => {
                (getContext(this) as any).ability.switchToSafeAreaMode();
              })
          }
          .margin(20)

          // 示例内容卡片
          ForEach([1, 2, 3, 4], (item) => {
            Row() {
              Text(`内容卡片 ${item}`)
                .fontSize(16)
                .color('#333')
            }
            .backgroundColor(Color.White)
            .width('90%')
            .height(100)
            .borderRadius(10)
            .margin(10)
            .justifyContent(FlexAlign.Center)
          })
        }
        .width('100%')
      }

      // 底部信息栏
      Row() {
        Text('底部操作区')
          .fontSize(16)
          .color(Color.White)
      }
      .backgroundColor('#96dffa')
      .width('100%')
      .height(60)
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#d5d5d5')
    .padding({
      top: this.uiContext ? this.uiContext.px2vp(this.statusBarHeight) : 0,
      bottom: this.uiContext ? this.uiContext.px2vp(this.navBarHeight) : 0
    })
    .onAppear(() => {
      this.uiContext = this.getUIContext();
    })
  }
}

4. 窗口全屏布局(隐藏避让区)页面


import { pageMap } from '../common/Constants';

@Entry
@Component
struct FullScreenHidden {
  build() {
    Column() {
      // 顶部区域
      Row() {
        Text('全屏隐藏模式')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .color(Color.White)
      }
      .backgroundColor('#2786d9')
      .width('100%')
      .height(50)
      .justifyContent(FlexAlign.Center)

      // 内容区
      Scroll() {
        Column() {
          // 提示信息
          Text('状态栏和导航栏已隐藏,上滑底部可唤起导航栏')
            .fontSize(14)
            .padding(15)
            .backgroundColor('#fff3cd')
            .margin(10)
            .borderRadius(8)
            .width('90%')

          // 功能按钮区
          Column() {
            Button('切换到全屏普通模式')
              .width('80%')
              .margin(5)
              .onClick(() => {
                (getContext(this) as any).ability.switchToFullScreenNormal();
              })

            Button('切换到组件安全区模式')
              .width('80%')
              .margin(5)
              .onClick(() => {
                (getContext(this) as any).ability.switchToSafeAreaMode();
              })
          }
          .margin(20)

          // 模拟视频播放区域
          Row() {
            Text('视频播放区域')
              .fontSize(20)
              .color(Color.White)
          }
          .backgroundColor('#333')
          .width('90%')
          .height(200)
          .borderRadius(10)
          .margin(10)
          .justifyContent(FlexAlign.Center)

          // 示例内容卡片
          ForEach([1, 2, 3], (item) => {
            Row() {
              Text(`内容卡片 ${item}`)
                .fontSize(16)
                .color('#333')
            }
            .backgroundColor(Color.White)
            .width('90%')
            .height(100)
            .borderRadius(10)
            .margin(10)
            .justifyContent(FlexAlign.Center)
          })
        }
        .width('100%')
      }

      // 底部操作区
      Row() {
        Text('播放控制区')
          .fontSize(16)
          .color(Color.White)
      }
      .backgroundColor('#96dffa')
      .width('100%')
      .height(60)
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#d5d5d5')
  }
}

5. 组件安全区方案页面


import { SafeAreaEdge, SafeAreaType } from '@kit.ArkUI';
import { pageMap } from '../common/Constants';

@Entry
@Component
struct SafeAreaMode {
  build() {
    Column() {
      // 顶部导航栏(延伸至状态栏)
      Row() {
        Text('组件安全区模式')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .color(Color.White)
      }
      .backgroundColor('#2786d9')
      .width('100%')
      .height(50)
      .justifyContent(FlexAlign.Center)
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) // 延伸至状态栏

      // 内容区
      Scroll() {
        Column() {
          // 方案说明
          Text('此模式下UI元素自动限制在安全区,通过expandSafeArea延伸背景至避让区')
            .fontSize(14)
            .padding(15)
            .backgroundColor('#e6f7ff')
            .margin(10)
            .borderRadius(8)
            .width('90%')

          // 功能按钮区
          Column() {
            Button('切换到全屏普通模式')
              .width('80%')
              .margin(5)
              .onClick(() => {
                (getContext(this) as any).ability.switchToFullScreenNormal();
              })

            Button('切换到全屏隐藏模式')
              .width('80%')
              .margin(5)
              .onClick(() => {
                (getContext(this) as any).ability.switchToFullScreenHidden();
              })
          }
          .margin(20)

          // 示例内容卡片
          ForEach([1, 2, 3, 4], (item) => {
            Row() {
              Text(`内容卡片 ${item}`)
                .fontSize(16)
                .color('#333')
            }
            .backgroundColor(Color.White)
            .width('90%')
            .height(100)
            .borderRadius(10)
            .margin(10)
            .justifyContent(FlexAlign.Center)
          })
        }
        .width('100%')
      }

      // 底部信息栏(延伸至导航区)
      Row() {
        Text('底部导航区')
          .fontSize(16)
          .color(Color.White)
      }
      .backgroundColor('#96dffa')
      .width('100%')
      .height(60)
      .justifyContent(FlexAlign.Center)
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) // 延伸至导航区
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#d5d5d5')
  }
}
点赞
收藏
评论区
推荐文章
Python进阶者 Python进阶者
4年前
一篇文章带你了解HTML的网页布局结构
大家好,我是IT共享者,人称皮皮。这篇我们来讲讲CSS网页布局。一、网页布局网页布局有很多种方式,一般分为以下几个部分:头部区域、菜单导航区域、内容区域、底部区域。1\.头部区域头部区域位于整个网页的顶部,一般用于设置网页的标题或者网页的logo:例CSS项目(runoob.com)bodymargin:0;/头部样式/.heade
可莉 可莉
3年前
015_swiftui_刘海屏适配
swiftUi创建的视图是默认在刘海屏幕的安全区域以内的。之前我们设置图片也用到了这个个属性。现在我们再来使用下吧.edgesIgnoringSafeArea(.all)下面就是图片了。 !(https://imgblog.csdnimg.cn/20200514174703741.png?xossprocess
taskbuilder taskbuilder
8个月前
TaskBuilder主界面介绍
TaskBuilder主界面介绍TaskBuilder的主界面分为如下图所示的7个区域:这7个区域的作用简要介绍如下:2、服务器设置:在此查看和设置任擎服务器的信息。应用系统的代码都是保存在任擎服务器上的,TaskBuilder必须连接任擎服务器才能进行相
陈杨 陈杨
4个月前
鸿蒙原生绘图API:从基础到高阶的绘制之旅(进阶版)
家人们,还记得上次一起探索的鸿蒙绘图API基础用法吗?上手是不是特别容易!今天,咱们就接着深入,开启进阶版的学习,解锁更多复杂又炫酷的绘图技能,让你的鸿蒙应用界面直接“出圈”!我将结合实际开发场景,丰富绘制路径、圆角矩形、绘制图片、画笔与画刷、裁剪区域设置
程序员一鸣 程序员一鸣
1个月前
鸿蒙开发:沉浸式效果实现
沉浸式效果实现后,一定要注意安全区域的内容避让,防止内容延伸后被导航条或者状态栏遮挡,具体是选择安全区域或者窗口管理方式,按照需求进行处理,如果仅仅是某个页面,直接安全区域即可。
GeorgeGcs GeorgeGcs
1个月前
【HarmonyOS 5】AttributeModifier和AttributeUpdater区别详解
【HarmonyOS5】AttributeModifier和AttributeUpdater区别详解\鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财一、AttributeModifier和AttributeUpdater的定义和作用1
布局王 布局王
1个月前
详解鸿蒙Next仓颉开发语言中的全屏模式
大家好,今天跟大家分享一下仓颉开发语言中的全屏模式。和ArkTS一样,仓颉的新建项目默认是非全屏模式的,如果你的应用颜色比较丰富,就会发现屏幕上方和底部的留白,这是应用自动避让了屏幕上方摄像头区域和底部的导航条区域。但是通常我们不需要这些留白,而是希望应用
GeorgeGcs GeorgeGcs
1个月前
【HarmonyOS 5】鸿蒙TEE(可信执行环境)详解
鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财一、TEE是什么?1、TEE的定义:可信执行环境(TrustedExecutionEnvironment),简称TEE,是存在于智能手机、平板或任意移动设备主处理器中的一个安全区域,确保各
GeorgeGcs GeorgeGcs
1个月前
【HarmonyOS 5】鸿蒙中常见的标题栏布局方案
鸿蒙开发能力HarmonyOSSDK应用服务鸿蒙金融类应用(金融理财一、问题背景:鸿蒙中常见的标题栏:矩形区域,左边是返回按钮,右边是问号帮助按钮,中间是标题文字。那有几种布局方式,分别怎么布局呢?常见的思维是,有老铁使用row去布局,怎么都对不齐。二、解
京东云开发者 京东云开发者
9个月前
Taro 鸿蒙技术内幕系列(二):如何让 W3C 标准的 CSS跑在鸿蒙上
作者:京东零售马银涛基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景HarmonyOS采用自研的ArkUI框架作为原生UI开发方案,这套方案有完善的布局系统和样式
GeorgeGcs
GeorgeGcs
Lv1
男 · 金融头部企业 · 鸿蒙应用架构师
HarmonyOS认证创作专家,华为HDE专家,鸿蒙讲师,作者。目前任职鸿蒙应用架构师。 历经腾讯,宝马,研究所,金融。 待过私企,外企,央企。 深耕大应用开发领域十年。 AAE,Harmony(OpenHarmony\HarmonyOS),MAE(Android\IOS),FE(H5\Vue\RN)。
文章
73
粉丝
1
获赞
2