【HarmonyOS】應用設置全屏和安全區域詳解
一、前言
IDE創建的鴻蒙應用,默認采取組件安全區布局方案。頂部會預留狀態欄區域,底部會預留導航條區域。這就是所謂的安全區域。
如果不處理,界面效果很割裂。所以業內UI交互設計,都會設置應用為全屏布局。將頁面繪制區域沾滿整個界面。
或者將安全區域的顏色與應用UI設置為一致。
以上兩種方式都是沉浸式布局的處理。所以全屏非沉浸式,概念不可混為一談。
在移動應用開發中,"沉浸式效果"早已不是新鮮詞,但要真正實現自然、和諧的沉浸式體驗,卻需要對系統布局、交互邏輯有深入理解。
二、什么是應用沉浸式效果?
簡單來說,應用沉浸式效果是通過優化狀態欄、應用界面與底部導航區域(導航條或三鍵導航)的視覺融合與交互適配,減少系統界面的突兀感,讓用戶注意力更聚焦于應用內容本身。
典型的界面元素包含三部分:
狀態欄:顯示時間、電量等系統信息的頂部區域
應用界面:承載應用核心內容的區域
底部導航區域:提供系統導航操作的底部區域
其中狀態欄和底部導航區域被稱為"避讓區",其余區域為"安全區"。沉浸式開發的核心就是處理好這兩個區域與應用內容的關系,主要涉及兩類問題:
UI元素避讓:避免可交互元素或關鍵信息被避讓區遮擋
視覺融合:讓避讓區與應用界面的顏色、風格保持一致
三、如何設置沉浸式布局?
綜上所述,我們可知,設置沉浸式布局有以下兩種方式,如圖所示:
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')}
}