HarmonyOS | 沉浸式适配与深浅色适配
一、前言
打开一款体验优秀的 App,你会发现它的页面从顶部状态栏一直延伸到底部导航条,整体浑然一体,没有"被切一刀"的割裂感;而到了夜晚切换到深色模式,整个界面又会变得柔和护眼。这两种体验分别对应了 HarmonyOS 开发中的两个高频考点:
- 沉浸式适配:让页面内容延伸到状态栏、导航条所在的安全区域之外。
- 深浅色适配:让 UI 颜色、图标、图片、状态栏文字在深色 / 浅色模式下协调一致地切换。
这两件事单独看都不复杂,但坑点不少——状态栏文字颜色怎么跟着变?图片在深色模式下怎么处理?颜色资源怎么管理才不会乱成一团?本文将围绕这两大主题,结合代码实战,把全流程讲清楚。
二、沉浸式适配
2.1 什么是沉浸式
默认情况下,App 页面被"夹"在顶部状态栏(时间、电量、信号那一行)和底部导航条之间,无法触及这两块系统区域。
沉浸式布局则是让页面"穿越"状态栏与导航条,一直延伸到屏幕最顶端和最底端,配合与系统区域协调的背景色,形成一整块的视觉效果,就像页面"沉浸"在屏幕里一样。
2.2 两种实现方案
HarmonyOS 提供了两种主流的沉浸式实现方式,可根据项目复杂度选择。
方案一:组件级 expandSafeArea(推荐,最简单)
只需给设置了背景色的根组件加上 expandSafeArea 属性,即可让该组件延伸到指定的安全区域。
Navigation(this.pathInfos) {
Tabs({ controller: this.tabsController }) {
TabContent() { Home() }
.tabBar(this.tabBuilder($r('app.string.home'), $r('sys.symbol.house'), $r('sys.symbol.house_fill'), 0))
TabContent() { Mine() }
.tabBar(this.tabBuilder($r('app.string.mine'), $r('sys.symbol.person'), $r('sys.symbol.person_fill'), 1))
}
.barPosition(BarPosition.End)
.scrollable(false)
.barHeight(52)
.barOverlap(true)
.barBackgroundColor('#1AE9E9E9')
.barBackgroundBlurStyle(BlurStyle.COMPONENT_THICK)
}
.navDestination(this.myRouter)
.hideTitleBar(true)
.hideToolBar(true)
.backgroundColor($r('app.color.app_background_color'))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
关键属性解读:
backgroundColor($r('app.color.app_background_color')):必须先给根组件设置背景色,否则延伸过去的区域是空白透明的,反而显得突兀。expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]):- 第一个参数
[SafeAreaType.SYSTEM]表示要延伸到系统级安全区域(状态栏 + 导航条)。 - 第二个参数
[SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]表示同时向顶部和底部延伸。
- 第一个参数
注意:
expandSafeArea必须加在"设置了背景色的那个组件"上,否则延伸无效。
方案二:窗口级 setWindowLayoutFullScreen(更精细控制)
通过 @ohos.window 提供的 API,在 Ability 入口直接把整个窗口设为全屏布局,再手动处理避让区。
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
const windowClass = windowStage.getMainWindowSync();
// 1. 开启全屏布局
windowClass.setWindowLayoutFullScreen(true);
// 2. 设置状态栏透明背景
windowClass.setWindowSystemBarProperties({
statusBarColor: '#00000000',
statusBarContentColor: '#FFFFFF'
});
// 3. 记录状态栏避让区高度,供页面做沉浸式布局
const avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topRectHeight', windowClass.getUIContext().px2vp(avoidArea.topRect.height));
});
}
}
页面中通过 @StorageProp 读取避让高度,并据此设置 padding:
@Entry
@Component
struct Index {
@StorageProp('topRectHeight') topRectHeight: number = 0;
@StorageProp('bottomRectHeight') bottomRectHeight: number = 0;
build() {
Column() {
Text('沉浸式示例').fontSize(20)
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.app_background_color'))
.padding({ top: this.topRectHeight, bottom: this.bottomRectHeight })
}
}
两种方案对比:
| 维度 | expandSafeArea |
setWindowLayoutFullScreen |
|---|---|---|
| 作用层级 | 组件级 | 窗口级 |
| 控制粒度 | 粗粒度,按 SafeAreaType/Edge | 细粒度,可精确到 px |
| 适用场景 | 单页或局部沉浸 | 全 App 统一沉浸式 |
| 复杂度 | 低 | 中 |
三、深浅色适配
3.1 为什么要做深色模式适配
深色模式早已不是"锦上添花",而是 App 的"必备功课":
- 护眼:低光环境下深色背景对眼睛更友好。
- 省电:OLED 屏幕黑色像素不发光,深色背景可显著省电。
- 一致性:未适配的 App 在系统深色模式下会变成"黑夜里的灯泡",体验割裂。
HarmonyOS 默认 App 颜色模式跟随系统,但开发者也可以通过 API 强制指定。
3.2 模式设置与保存
通过 @kit.AbilityKit 提供的 ConfigurationConstant.ColorMode 可以控制应用级颜色模式。建议封装一个工具类统一管理:
// utils/DarkModeManager.ets
import { common, ConfigurationConstant } from '@kit.AbilityKit';
const setColorMode = (context: common.UIAbilityContext, colorMode: ConfigurationConstant.ColorMode) => {
context.getApplicationContext().setColorMode(colorMode);
};
// 跟随系统
export const setAutoColorMode = (context: common.UIAbilityContext) => {
setColorMode(context, ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
};
// 强制深色
export const setDarkColorMode = (context: common.UIAbilityContext) => {
setColorMode(context, ConfigurationConstant.ColorMode.COLOR_MODE_DARK);
};
// 强制浅色
export const setLightColorMode = (context: common.UIAbilityContext) => {
setColorMode(context, ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT);
};
三种模式枚举含义:
COLOR_MODE_NOT_SET:未设置,应用跟随系统。COLOR_MODE_LIGHT:强制浅色,无视系统设置。COLOR_MODE_DARK:强制深色,无视系统设置。
3.3 在 AbilityStage / UIAbility 中保存与监听颜色模式
仅能切换还不够,还需要把当前颜色模式"记住",并在系统切换时实时同步。利用 AppStorage 这个全局共享存储,配合生命周期回调即可实现。
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { Configuration } from '@ohos.app.ability.Configuration';
export default class EntryAbility extends UIAbility {
onCreate(_want: Want, _launchParam: AbilityConstant.LaunchParam): void {
// 启动时把系统当前颜色模式写入 AppStorage,供各页面 @StorageProp 读取
AppStorage.setOrCreate<ConfigurationConstant.ColorMode>(
'currentColorMode',
this.context.config.colorMode
);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
const windowClass = windowStage.getMainWindowSync();
windowClass.setWindowLayoutFullScreen(true);
const avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topRectHeight', windowClass.getUIContext().px2vp(avoidArea.topRect.height));
});
}
// 系统主题切换时实时同步到 AppStorage
onConfigurationUpdate(newConfig: Configuration): void {
const currentColorMode: ConfigurationConstant.ColorMode | undefined = AppStorage.get('currentColorMode');
if (currentColorMode !== newConfig.colorMode) {
AppStorage.setOrCreate<ConfigurationConstant.ColorMode>('currentColorMode', newConfig.colorMode);
}
}
}
这样就形成了一条完整的"保存 → 监听 → 更新"链路,所有监听了 currentColorMode 的 UI 组件都会在模式切换时自动刷新。
3.4 颜色适配
颜色适配是深色模式中最核心也最繁琐的一环。HarmonyOS 提供两种方式。
方式一:根据 AppStorage 状态手动判断(不推荐)
.backgroundColor(this.isDarkMode ? '#1A1A1A' : '#FFFFFF')
简单直接,但每个用到颜色的地方都要写条件判断,维护成本高,扩展性差。
方式二:资源目录方式(推荐)
在 resources 下分别维护 base/element/color.json(浅色)和 dark/element/color.json(深色),定义同名颜色资源但值不同:
resources/
├── base/
│ └── element/
│ └── color.json // 浅色模式颜色定义
└── dark/
└── element/
└── color.json // 深色模式颜色定义
base/element/color.json:
{
"color": [
{ "name": "app_background_color", "value": "#FFFFFF" },
{ "name": "text_primary_color", "value": "#333333" },
{ "name": "icon_color", "value": "#333333" }
]
}
dark/element/color.json:
{
"color": [
{ "name": "app_background_color", "value": "#1A1A1A" },
{ "name": "text_primary_color", "value": "#E0E0E0" },
{ "name": "icon_color", "value": "#E0E0E0" }
]
}
业务代码中统一用 $r 引用,无需任何条件判断:
.backgroundColor($r('app.color.app_background_color'))
.fontColor($r('app.color.text_primary_color'))
系统会根据当前颜色模式自动从对应目录取值。这也是为什么沉浸式布局里强调"用 $r 而不是写死颜色值"——切换模式时背景色自动跟着变,零改动。
强烈建议从项目第一天起就用
$r('app.color.xxx')管理颜色,避免后期返工。
3.5 媒体资源适配
光颜色变深还不够,图标和图片也要"换装",否则在深色背景下会出现"看不见"或"格格不入"的尴尬。
SVG / Symbol 图标适配
使用 SymbolGlyph 组件,配合 fontColor 资源引用即可自动跟随模式切换:
SymbolGlyph($r('sys.symbol.house'))
.fontColor($r('app.color.icon_color'))
由于 fontColor 用的也是 $r 引用,浅色模式下是深色图标,深色模式下是浅色图标,几乎"零成本"完成适配。
普通图片适配
在 resources 下维护 base/media 与 dark/media 两套图片,文件名必须完全一致:
resources/
├── base/
│ └── media/
│ ├── logo.png // 浅色模式 logo
│ └── banner.png // 浅色模式 banner
└── dark/
└── media/
├── logo.png // 深色模式 logo(同名!)
└── banner.png // 深色模式 banner(同名!)
代码中照常引用:
Image($r('app.media.logo'))
系统会根据当前模式自动从 base/media 或 dark/media 中查找同名文件。
提示:深色模式图片不是简单"反色"或"加滤镜"就能搞定的,需要根据设计稿专门设计一套配色协调的图片资源。
3.6 状态栏适配
这是最容易被忽略、却又最影响体验的一步。页面变深了,状态栏文字却还是黑色,黑字压在深色背景上根本看不清;反之亦然。
解决方案:在颜色模式变化回调中,通过 setWindowSystemBarProperties 同步更新状态栏文字颜色。
onConfigurationUpdate(newConfig: Configuration): void {
if (newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
this.windowClass?.setWindowSystemBarProperties({ statusBarContentColor: '#000000' });
} else if (newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
this.windowClass?.setWindowSystemBarProperties({ statusBarContentColor: '#FFFFFF' });
}
}
要点:
statusBarContentColor控制的是状态栏上时间、电量、信号等内容的颜色。- 浅色模式用
#000000,深色模式用#FFFFFF,保证对比度。 - 使用可选链
?.做防御性调用,避免windowClass未初始化时报错。
四、完整实战:把所有适配点串起来
下面是一个把"沉浸式 + 深浅色"全部打通的完整骨架,可作为新项目模板。
4.1 项目结构
entry/src/main/
├── ets/
│ ├── entryability/EntryAbility.ets // 应用入口
│ ├── pages/Index.ets // 主页面
│ ├── components/
│ │ ├── Home.ets
│ │ └── Mine.ets
│ └── utils/DarkModeManager.ets // 颜色模式管理
└── resources/
├── base/element/color.json // 浅色色值
├── base/media/ // 浅色图片
├── dark/element/color.json // 深色色值
└── dark/media/ // 深色图片
4.2 EntryAbility:初始化 + 监听
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { Configuration } from '@ohos.app.ability.Configuration';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'EntryAbility';
export default class EntryAbility extends UIAbility {
private windowClass: window.Window | null = null;
onCreate(_want: Want, _launchParam: AbilityConstant.LaunchParam): void {
AppStorage.setOrCreate<ConfigurationConstant.ColorMode>('currentColorMode', this.context.config.colorMode);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, TAG, 'loadContent failed: %{public}s', JSON.stringify(err));
return;
}
this.windowClass = windowStage.getMainWindowSync();
this.windowClass.setWindowLayoutFullScreen(true);
const isDark = AppStorage.get<ConfigurationConstant.ColorMode>('currentColorMode')
=== ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
this.windowClass.setWindowSystemBarProperties({
statusBarColor: '#00000000',
statusBarContentColor: isDark ? '#FFFFFF' : '#000000'
});
const avoidArea = this.windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topRectHeight', this.windowClass.getUIContext().px2vp(avoidArea.topRect.height));
});
}
onConfigurationUpdate(newConfig: Configuration): void {
const prev = AppStorage.get<ConfigurationConstant.ColorMode>('currentColorMode');
if (prev !== newConfig.colorMode) {
AppStorage.setOrCreate<ConfigurationConstant.ColorMode>('currentColorMode', newConfig.colorMode);
if (newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
this.windowClass?.setWindowSystemBarProperties({ statusBarContentColor: '#000000' });
} else if (newConfig.colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
this.windowClass?.setWindowSystemBarProperties({ statusBarContentColor: '#FFFFFF' });
}
}
}
}
4.3 主页面:沉浸式 + 资源引用
@Entry
@Component
struct Index {
@StorageProp('topRectHeight') topRectHeight: number = 0;
build() {
Navigation() {
Column() {
Text('沉浸式 + 深浅色适配示例')
.fontSize(20)
.fontColor($r('app.color.text_primary_color'))
SymbolGlyph($r('sys.symbol.house'))
.fontColor($r('app.color.icon_color'))
.fontSize(40)
Image($r('app.media.logo'))
.width(120).height(120)
}
.width('100%')
.height('100%')
.padding({ top: this.topRectHeight })
.backgroundColor($r('app.color.app_background_color'))
}
.hideTitleBar(true)
.backgroundColor($r('app.color.app_background_color'))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
}
4.4 深色模式设置页:让用户自主切换
import { common, ConfigurationConstant } from '@kit.AbilityKit';
import { setAutoColorMode, setDarkColorMode, setLightColorMode } from '../utils/DarkModeManager';
@Entry
@Component
struct DarkModeSetting {
@StorageProp('currentColorMode') currentColorMode: ConfigurationConstant.ColorMode
= ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
build() {
Column({ space: 12 }) {
Text('颜色模式设置').fontSize(20).margin({ top: 20 })
Button('跟随系统')
.onClick(() => setAutoColorMode(this.context))
Button('浅色模式')
.onClick(() => setLightColorMode(this.context))
Button('深色模式')
.onClick(() => setDarkColorMode(this.context))
}
.width('100%').height('100%')
.backgroundColor($r('app.color.app_background_color'))
}
}
五、适配清单速查表
| 适配项 | 适配内容 | 推荐方式 |
|---|---|---|
| 沉浸式布局 | 页面延伸到状态栏/导航条 | 组件级 expandSafeArea 或窗口级 setWindowLayoutFullScreen |
| 颜色资源适配 | 背景、文字、分割线等颜色 | base / dark 双目录 color.json + $r 引用 |
| SVG 图标适配 | Symbol 图标颜色 | SymbolGlyph + fontColor($r(...)) |
| 普通图片适配 | PNG/JPG 等位图 | base/media 与 dark/media 放同名图片 |
| 状态栏适配 | 状态栏文字颜色 | onConfigurationUpdate 中调用 setWindowSystemBarProperties |
| 模式切换同步 | 全局颜色状态同步 | AppStorage + @StorageProp + onConfigurationUpdate |
六、避坑要点
expandSafeArea必须配背景色:否则延伸过去的区域是透明空白,反而更难看。设置背景色的组件就是加expandSafeArea的组件。- 从第一天起就用
$r引用资源:不要图省事写死颜色值 / 图片路径,否则后期深色适配要满项目改代码。 dark目录与base目录资源必须同名:颜色资源名一致、图片文件名一致,系统才能根据模式自动匹配。- 别忘了状态栏文字:页面颜色变了状态栏文字也要跟着变,否则对比度过低导致看不清。
setWindowLayoutFullScreen(true)后要自行处理避让区:用getWindowAvoidArea拿到状态栏/导航条高度,存入AppStorage,页面据此设置padding。- 工具类抽离是好习惯:把颜色模式相关操作封装到
DarkModeManager,业务代码只调三个语义化函数,便于维护。 onCreate初始化 +onConfigurationUpdate实时更新:两个回调配合,才能保证 App 颜色状态始终正确,包括启动时和运行中系统切换。- Web 组件要单独适配:若页面中嵌入 Web,需要通过媒体查询单独设置深色样式,并用
darkMode()属性控制 Web 是否启用深色模式。
七、总结
沉浸式适配与深浅色适配看似是两个独立的功能,实则共享同一套"资源引用"思想:
- 沉浸式的核心是
expandSafeArea(或setWindowLayoutFullScreen+ 避让区处理),让页面铺满全屏。 - 深浅色的核心是
base/dark双资源目录 +$r引用,让颜色、图标、图片自动跟随模式切换。 - 两者通过
setWindowSystemBarProperties在状态栏层面交汇——沉浸式让状态栏背景与应用融为一体,深浅色让状态栏文字与应用背景保持对比度。
更多推荐

所有评论(0)