在这里插入图片描述

每日一句正能量

锤炼细节,在积累中实现螺旋式上升。
“锤炼细节”是动作,“螺旋式上升”是结果。它不是直线上升,而是有回旋、有看似重复的过程。你可能觉得自己在做同样的事,但其实每一次回旋都在更高的层面上。细节就是螺旋中的那个支点,你抓住了它,上升才有依托。


一、前言

深色模式(Dark Mode)作为现代移动操作系统的基础能力,已经从"锦上添花"演变为"必备功能"。根据人机交互领域的研究,深色模式在弱光环境下能有效降低屏幕亮度对眼睛的刺激,减少视觉疲劳;对于OLED屏幕设备,深色模式还能显著降低功耗,延长续航时间。此外,系统级深色模式的普及使得用户对应用的一致性体验提出了更高要求——当系统切换至深色模式后,未适配的应用会呈现出刺眼的高对比度界面,严重破坏使用体验。

HarmonyOS ArkUI框架为深色模式适配提供了完善的资源目录隔离机制与系统事件监听能力,但开发者仍需在颜色语义化设计、状态栏适配、媒体资源切换、WebView处理等多个维度进行系统性适配。本文将从实际痛点出发,完整讲解企业级深色模式适配的技术方案与最佳实践。


二、深色模式适配的必要性与挑战

2.1 为什么必须适配深色模式

  • 用户体验一致性:系统级深色模式切换后,未适配的应用会出现"白底黑字弹窗在深色背景上刺眼"、"状态栏文字不可见"等视觉灾难。
  • OLED功耗优化:深色像素在OLED屏幕上接近关闭状态,可节省30%~60%的显示功耗。
  • 弱光环境护眼:深色背景配合低亮度文字,减少蓝光辐射,降低视疲劳。

2.2 适配前后效果对比

以下对比图展示了未适配应用与已适配应用在深色模式下的视觉差异:

在这里插入图片描述

未适配的应用在深色模式下会出现四类典型问题:状态栏文字与背景对比度过低导致不可见、浅色卡片在深色背景上形成刺眼的高反差、黑色图标融入深色背景消失、浅色分割线在深色背景上不可见。已适配的应用则通过系统化的颜色映射,确保所有元素在深色模式下保持清晰的视觉层级与舒适的对比度。


三、深色模式适配完整架构

3.1 系统触发流程与资源加载机制

HarmonyOS深色模式适配的核心机制是资源目录隔离配合系统配置变更监听。当用户在系统设置中切换深浅色模式时,框架层会触发一系列事件通知应用更新UI:

在这里插入图片描述

系统触发流程如下:

  1. 系统设置切换:用户在系统设置中切换深浅色模式。
  2. 框架层通知:系统框架检测到配置变更,通知所有运行中的应用。
  3. onConfigurationUpdate回调:应用的AbilityStageUIAbility收到配置变更事件。
  4. AppStorage更新:将最新的colorMode保存到全局状态存储。
  5. 组件自动刷新:通过@StorageProp@Watch监听的组件自动重绘。

资源加载机制则是通过在resources目录下创建与base同结构的dark目录,系统会根据当前颜色模式自动加载对应目录下的同名资源。例如$r('app.color.text_primary')在浅色模式下读取base/element/color.json,在深色模式下自动切换至dark/element/color.json


四、颜色资源适配

4.1 语义化颜色命名

深色模式适配的首要原则是拒绝硬编码颜色值,采用语义化命名规范。语义化命名描述颜色的用途而非外观,使得深浅色切换时只需修改颜色值,无需改动组件代码。

在这里插入图片描述

推荐的颜色Token命名规范如下:

语义Token 浅色模式 深色模式 用途说明
text_primary #1A1A1A #F3F4F6 主标题、关键信息
text_secondary #666666 #D1D5DB 正文内容、段落
text_disabled #999999 #9CA3AF 禁用状态文字
bg_page #F8F9FA #111827 页面最底层背景
bg_surface #FFFFFF #1F2937 卡片、列表等表面背景
bg_elevated #FFFFFF #374151 弹窗、浮层等更高层级
border_default #E5E7EB #374151 常规分割线、边框
brand_primary #2563EB #3B82F6 品牌主色(深色模式下适当提亮)

4.2 资源文件配置

src/main/resources目录下创建dark目录,保持与base完全相同的目录结构:

resources/
├── base/
│   ├── element/
│   │   └── color.json
│   └── media/
│       └── banner.png
└── dark/
    ├── element/
    │   └── color.json      # 同名资源,不同色值
    └── media/
        └── banner.png      # 同名资源,深色版本
// resources/base/element/color.json
{
  "color": [
    { "name": "text_primary", "value": "#FF1A1A1A" },
    { "name": "text_secondary", "value": "#FF666666" },
    { "name": "bg_page", "value": "#FFF8F9FA" },
    { "name": "bg_surface", "value": "#FFFFFFFF" },
    { "name": "border_default", "value": "#FFE5E7EB" },
    { "name": "brand_primary", "value": "#FF2563EB" }
  ]
}
// resources/dark/element/color.json
{
  "color": [
    { "name": "text_primary", "value": "#FFF3F4F6" },
    { "name": "text_secondary", "value": "#FFD1D5DB" },
    { "name": "bg_page", "value": "#FF111827" },
    { "name": "bg_surface", "value": "#FF1F2937" },
    { "name": "border_default", "value": "#FF374151" },
    { "name": "brand_primary", "value": "#FF3B82F6" }
  ]
}

4.3 Surface层级设计

在浅色模式下,设计师通常通过白色卡片叠加阴影(Elevation)来区分层级。但在深色模式下,纯黑阴影会消失不见,因此需要通过背景色亮度差异来构建层级:

  • Level 0(页面背景)#111827,最暗,作为底层画布。
  • Level 1(卡片Surface)#1F2937,比页面背景亮约8%,用于卡片、列表项。
  • Level 2(弹窗/浮层)#374151,比Surface再亮约8%,用于Dialog、BottomSheet。
  • Level 3(交互元素)#4B5563,用于按钮、输入框等需要强调交互性的元素。

五、状态栏与导航栏适配

5.1 状态栏适配流程

沉浸式布局下,状态栏背景色与应用背景色保持一致,此时必须手动控制状态栏文字颜色,避免"黑字黑底"或"白字白底"的对比度灾难:

在这里插入图片描述

完整的状态栏适配流程如下:

// entryability/EntryAbility.ets
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

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

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 初始化时将当前颜色模式存入AppStorage
    AppStorage.setOrCreate('currentColorMode', this.context.config.colorMode);
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'EntryAbility', 'Failed to load content');
        return;
      }
      // 获取窗口实例并缓存
      windowStage.getMainWindow((err, data) => {
        if (err.code) {
          hilog.error(0x0000, 'EntryAbility', 'Failed to get main window');
          return;
        }
        this.windowObj = data;
        AppStorage.setOrCreate('windowClass', this.windowObj);
        // 初始设置状态栏
        this.updateStatusBar();
      });
    });
  }

  // 系统配置变更回调(包括深浅色模式切换)
  onConfigurationUpdate(config: Configuration): void {
    hilog.info(0x0000, 'EntryAbility', `onConfigurationUpdate, colorMode: ${config.colorMode}`);
    AppStorage.setOrCreate('currentColorMode', config.colorMode);
    this.updateStatusBar();
  }

  private updateStatusBar(): void {
    if (!this.windowObj) return;
    const colorMode = AppStorage.get<number>('currentColorMode');
    try {
      if (colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
        this.windowObj.setWindowSystemBarProperties({
          statusBarContentColor: '#000000'  // 浅色模式:黑字
        });
      } else if (colorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
        this.windowObj.setWindowSystemBarProperties({
          statusBarContentColor: '#FFFFFF'  // 深色模式:白字
        });
      }
    } catch (error) {
      hilog.error(0x0000, 'EntryAbility', `setWindowSystemBarProperties failed: ${JSON.stringify(error)}`);
    }
  }
}

5.2 页面中监听颜色模式

// pages/Index.ets
import { ConfigurationConstant } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

@Entry
@Component
struct IndexPage {
  // 监听全局颜色模式变化
  @StorageProp('currentColorMode') @Watch('onColorModeChange') 
  currentMode: number = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;

  @State statusBarColor: string = '#000000';
  @State pageBgColor: ResourceColor = $r('app.color.bg_page');

  onColorModeChange(): void {
    // 颜色模式变化时更新状态栏
    this.updateStatusBarColor();
  }

  aboutToAppear(): void {
    this.updateStatusBarColor();
  }

  private updateStatusBarColor(): void {
    const windowClass = AppStorage.get<window.Window>('windowClass');
    if (!windowClass) return;

    if (this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
      this.statusBarColor = '#000000';
      windowClass.setWindowSystemBarProperties({ statusBarContentColor: '#000000' });
    } else {
      this.statusBarColor = '#FFFFFF';
      windowClass.setWindowSystemBarProperties({ statusBarContentColor: '#FFFFFF' });
    }
  }

  build() {
    Column() {
      Text('深色模式适配演示')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .margin({ top: 40, bottom: 20 })

      Column({ space: 12 }) {
        // 卡片示例
        Column({ space: 8 }) {
          Text('设置项标题')
            .fontSize(16)
            .fontColor($r('app.color.text_primary'))
            .fontWeight(FontWeight.Medium)
          Text('这是描述文字内容,用于展示深色模式下的文字对比度效果')
            .fontSize(14)
            .fontColor($r('app.color.text_secondary'))
        }
        .width('90%')
        .padding(16)
        .backgroundColor($r('app.color.bg_surface'))
        .borderRadius(12)
        .border({ width: 1, color: $r('app.color.border_default') })

        // 按钮示例
        Button('主要操作按钮')
          .width('90%')
          .height(48)
          .backgroundColor($r('app.color.brand_primary'))
          .fontColor(Color.White)
          .borderRadius(8)

        // 分割线示例
        Divider()
          .width('90%')
          .color($r('app.color.border_default'))
          .strokeWidth(1)

        Text('当前模式: ' + (this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK ? '深色' : '浅色'))
          .fontSize(14)
          .fontColor($r('app.color.text_secondary'))
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.bg_page'))
  }
}

六、媒体资源与图标适配

6.1 图片资源适配

对于PNG、WEBP等非矢量图片,需在dark/media目录下放置同名深色版本:

// 系统会自动根据当前模式加载对应目录下的图片
Image($r('app.media.banner'))
  .width('100%')
  .height(200)
  .objectFit(ImageFit.Cover)

6.2 SVG图标适配

SVG图标可通过fillColor属性绑定颜色资源,实现自动切换:

// resources/base/element/color.json 中定义 icon_primary
Image($r('app.media.ic_settings'))
  .width(24)
  .height(24)
  .fillColor($r('app.color.icon_primary'))  // 浅色:#606266 深色:#CFD3DC

6.3 Symbol图标适配

使用HarmonyOS Symbol时,通过fontColor绑定颜色资源:

SymbolGlyph($r('sys.symbol.star_fill'))
  .fontSize(24)
  .fontColor([$r('app.color.brand_primary')])

七、WebView深色模式适配

如果应用内嵌了WebView加载H5页面,需要额外处理Web内容的深色适配:

// components/DarkWebView.ets
import { webview } from '@kit.ArkWeb';

@Component
struct DarkWebView {
  @StorageProp('currentColorMode') currentMode: number = 0;
  controller: webview.WebviewController = new webview.WebviewController();

  aboutToAppear(): void {
    // 注入CSS变量或JS脚本适配深色模式
    const isDark = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
    const jsCode = `
      document.documentElement.style.colorScheme = '${isDark ? 'dark' : 'light'}';
      document.body.style.backgroundColor = '${isDark ? '#1A1A1A' : '#FFFFFF'}';
      document.body.style.color = '${isDark ? '#EEEEEE' : '#333333'}';
    `;
    this.controller.runJavaScript(jsCode);
  }

  build() {
    Web({ src: 'https://example.com', controller: this.controller })
      .width('100%')
      .height('100%')
      .backgroundColor($r('app.color.bg_page'))
  }
}

对于可控的H5页面,推荐在CSS中使用prefers-color-scheme媒体查询:

/* H5页面中的CSS */
@media (prefers-color-scheme: dark) {
  body {
    background-color: #1A1A1A;
    color: #EEEEEE;
  }
}

八、常见遗漏清单与对比度标准

深色模式适配中最容易遗漏的元素往往是不起眼的"边缘组件"。以下是经过多个项目验证的遗漏清单:

组件/元素 浅色默认值 深色应有值 遗漏后果
分割线 Divider #E4E7ED #363636 看不见或太刺眼
图标颜色 Icon #606266 #CFD3DC 图标融入背景不可见
投影 Shadow rgba(0,0,0,0.08) 更深或去掉 暗色下变成脏斑
WebView 背景 #FFFFFF #1D1D1D 页面加载白闪
Toast/Snackbar 白底黑字 深底浅字 弹窗刺眼
Skeleton 骨架屏 #F2F3F5 #2A2A2A 看不出加载效果
对话框遮罩 rgba(0,0,0,0.5) rgba(0,0,0,0.7) 暗色下遮罩不够暗
TabBar 底栏 白底 深底 与页面不协调
Loading 遮罩 半透明黑 半透明白 遮罩不够明显
进度条 Progress 彩色轨道 降低饱和度 过于刺眼抢焦点

对比度标准:正文文字与背景的对比度应 >= 4.5:1(WCAG AA级),大文字/图标 >= 3:1,禁用状态 >= 2:1。


九、跟随系统与手动控制

应用可以提供两种深浅色切换策略:

9.1 跟随系统

// 设置为未指定,应用自动跟随系统
this.context.getApplicationContext().setColorMode(
  ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
);

9.2 手动控制

// 强制浅色
this.context.getApplicationContext().setColorMode(
  ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
);

// 强制深色
this.context.getApplicationContext().setColorMode(
  ConfigurationConstant.ColorMode.COLOR_MODE_DARK
);

十、性能优化建议

优化策略 实现方式 效果
资源预加载 basedark目录资源打包 切换时无网络请求延迟
避免硬编码 全部使用$r()引用资源 切换时无需修改代码
状态栏缓存 缓存Window实例避免重复获取 减少IPC调用开销
过渡动画 animation修饰器平滑过渡 视觉无闪烁
对比度检测 设计阶段使用工具检测 避免上线后视觉问题

十一、总结

本文系统讲解了HarmonyOS ArkUI框架下深色模式适配的完整技术方案,从资源目录隔离、语义化颜色命名、Surface层级设计,到状态栏手动适配、媒体资源切换、WebView处理,覆盖了企业级应用深色模式适配的全部关键环节。

深色模式适配的核心在于前瞻性设计:在项目初期就建立语义化颜色Token体系,避免后期逐页修改硬编码颜色值。同时,必须建立走查清单,在深色模式下逐页检查分割线、图标、阴影、弹窗、骨架屏等边缘元素,确保无一遗漏。

希望本文能为鸿蒙生态开发者在构建高品质、高一致性用户体验的应用时提供实用的技术参考。


转载自:https://blog.csdn.net/u014727709/article/details/163450520
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐