一、前言

打开一款体验优秀的 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/mediadark/media 两套图片,文件名必须完全一致

resources/
├── base/
│   └── media/
│       ├── logo.png        // 浅色模式 logo
│       └── banner.png      // 浅色模式 banner
└── dark/
    └── media/
        ├── logo.png        // 深色模式 logo(同名!)
        └── banner.png      // 深色模式 banner(同名!)

代码中照常引用:

Image($r('app.media.logo'))

系统会根据当前模式自动从 base/mediadark/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/mediadark/media 放同名图片
状态栏适配 状态栏文字颜色 onConfigurationUpdate 中调用 setWindowSystemBarProperties
模式切换同步 全局颜色状态同步 AppStorage + @StorageProp + onConfigurationUpdate

六、避坑要点

  1. expandSafeArea 必须配背景色:否则延伸过去的区域是透明空白,反而更难看。设置背景色的组件就是加 expandSafeArea 的组件。
  2. 从第一天起就用 $r 引用资源:不要图省事写死颜色值 / 图片路径,否则后期深色适配要满项目改代码。
  3. dark 目录与 base 目录资源必须同名:颜色资源名一致、图片文件名一致,系统才能根据模式自动匹配。
  4. 别忘了状态栏文字:页面颜色变了状态栏文字也要跟着变,否则对比度过低导致看不清。
  5. setWindowLayoutFullScreen(true) 后要自行处理避让区:用 getWindowAvoidArea 拿到状态栏/导航条高度,存入 AppStorage,页面据此设置 padding
  6. 工具类抽离是好习惯:把颜色模式相关操作封装到 DarkModeManager,业务代码只调三个语义化函数,便于维护。
  7. onCreate 初始化 + onConfigurationUpdate 实时更新:两个回调配合,才能保证 App 颜色状态始终正确,包括启动时和运行中系统切换。
  8. Web 组件要单独适配:若页面中嵌入 Web,需要通过媒体查询单独设置深色样式,并用 darkMode() 属性控制 Web 是否启用深色模式。

七、总结

沉浸式适配与深浅色适配看似是两个独立的功能,实则共享同一套"资源引用"思想:

  • 沉浸式的核心是 expandSafeArea(或 setWindowLayoutFullScreen + 避让区处理),让页面铺满全屏。
  • 深浅色的核心是 base / dark 双资源目录 + $r 引用,让颜色、图标、图片自动跟随模式切换。
  • 两者通过 setWindowSystemBarProperties 在状态栏层面交汇——沉浸式让状态栏背景与应用融为一体,深浅色让状态栏文字与应用背景保持对比度。
Logo

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

更多推荐