HarmonyOS实战(解决方案篇)—深色模式适配完全指南:从原理到实践

HarmonyOS实战(解决方案篇)---深色模式适配完全指南:从原理到实践

还记得第一次在深夜里打开某个应用,突然被满屏的白色亮光刺痛眼睛的那一刻吗?那种不适感让我深刻意识到,深色模式不仅仅是一个"酷炫"的界面选项,更是对用户体验的深度关怀。

引言

大家好,我是你们的老朋友木斯佳。最近过年事情比较多,更新频次会低一些。首先祝大家马年快乐。给大家提前拜年啦。

言归正传,深色模式并非简单地将页面背景变为黑色、文字内容变为白色,而是提供一整套适配深色模式的应用配色主题。相较于浅色模式,深色模式更加柔和,能减少亮度对用户眼睛造成的刺激和疲劳。此外,深色模式还能在一定程度上降低应用功耗,提升设备的续航表现。

本文将详细介绍,之前AI助手应用深色模式的适配过程,列举常见问题及对应解决方案,帮助开发者打造更加完善的用户体验。

深色模式的设计原则

在进行深色模式适配前,我们需要遵循基本的UX设计原则,保障应用页面内容的易读性舒适性一致性

  1. 避免纯黑背景:建议使用深灰色(如#121212)作为背景色,而非纯黑色,这样可以减少视觉疲劳
  2. 注意对比度:文字与背景的对比度应保持在至少5:1,确保可读性
  3. 降低饱和度:深色模式下,应适当降低颜色的饱和度,避免过于刺眼
  4. 保持品牌一致性:品牌色可在深色模式下适当调整明度,但保持色相一致

实现原理

当系统切换到深色模式后,应用内可能会出现部分内容自动切换到深色主题的情况,例如状态栏、弹窗背景色、系统控件等。如果不做适配,这些自动切换的内容与应用内未适配的内容组合在一起,会导致页面效果错乱。

资源目录机制

当前深色模式适配主要依靠资源目录。当系统的对应设置项发生变化时(如系统语言、深浅色模式等),应用会自动加载对应资源目录下的资源文件。

系统为深色模式预留了dark目录,该目录在应用创建时默认不存在。在进行深色模式适配时,需要在src/main/resources中手动创建dark目录,将深色模式所需的资源放置到该目录下。浅色模式所需的资源则可放入默认存在的src/main/resources/base目录。

说明 :在进行资源定义时,需要在base目录与dark目录中定义同名 的资源。例如在base/element/color.json文件中定义text_color为黑色,在dark/element/color.json文件中定义text_color为白色,那么当深浅色切换时,应用内使用了$r('app.color.text_color')作为颜色值的元素会自动切换到对应的颜色,无需使用其他逻辑判断进行控制。

深浅色模式切换方式

目前应用向用户提供以下两种深浅色模式切换方式:

1. 应用跟随系统深浅色模式切换

实现上,需要开发者使用setColorMode()方法将ColorMode设置为COLOR_MODE_NOT_SET(未设置颜色模式)。这样应用在运行过程中就能自动感知系统颜色模式切换,并自动切换到对应的颜色模式。

2. 应用内提供手动控制开关

实现上,切换深色模式需要调用setColorMode()方法将ColorMode设置为COLOR_MODE_DARK,切换浅色模式则需要设置为COLOR_MODE_LIGHT

深色模式适配内容

适配项 适配内容 适配方式
颜色资源适配 组件背景色、字体颜色等 使用受支持的系统资源 / 使用color.json资源文件
媒体资源适配 应用内使用到的图片、图标等 SVG图标可使用fillColor()属性 / 使用media资源目录
状态栏适配 深浅模式下不同的状态栏表现 对应用背景色进行深浅色适配 / 动态设置状态栏字体颜色
Web内容适配 应用内使用Web组件加载的Web页面 参考Web组件设置深色模式

深色模式适配实践

复制代码
系统颜色模式变化
    ↓
EntryAbility.onConfigurationUpdate()
    ↓
更新 AppStorage('currentColorMode')
    ↓
各组件通过 @StorageProp 监听变化
    ↓
触发 @Watch 回调或重新渲染
    ↓
UI 更新(颜色、图片等资源自动切换)

1. 颜色资源适配

颜色资源适配是将页面元素的颜色抽离到限定词目录中,让应用在不同的深浅色模式下使用不同限定词目录中的颜色值,从而达成应用页面元素在深浅色下不同的颜色表现。

1.1 使用系统颜色资源
typescript 复制代码
// 在组件中使用系统预定义的颜色资源
Text(title)
  .fontColor(this.currentIndex === targetIndex ? 
    $r('sys.color.icon_emphasize') :      // 强调色
    $r('sys.color.icon_secondary'))        // 次要色
1.2 使用自定义颜色资源

项目结构:

复制代码
entry/src/main/resources/
├── base/
│   └── element/
│       └── color.json          // 浅色模式颜色定义
└── dark/
    └── element/
        └── color.json          // 深色模式颜色定义

使用方式:

typescript 复制代码
Text($r('app.string.today'))
  .fontColor($r('app.color.font_color'))  // 自动根据模式选择对应颜色

知识点:

  • 系统颜色资源(sys.color.*)会自动适配深浅色模式
  • 自定义颜色需要在 basedark 目录分别定义同名资源
  • 使用 $r() 引用资源时,系统会根据当前模式自动选择对应的资源

2. 媒体资源适配

媒体资源适配即在深浅模式下采用不同颜色表现的图片或图标等媒体资源,从而达成更好的用户体验。

2.1 图片资源适配

项目结构:

复制代码
entry/src/main/resources/
├── base/
│   └── media/
│       └── banner.png          // 浅色模式图片
└── dark/
    └── media/
        └── banner.png          // 深色模式图片(同名不同内容)

使用方式:

typescript 复制代码
Image($r('app.media.banner'))
  .width('100%')
  .borderRadius(12)

知识点:

  • 图片资源同样支持深浅色适配
  • base/mediadark/media 放置同名但内容不同的图片
  • 系统会根据当前模式自动加载对应的图片资源

3. 状态栏适配

状态栏适配即在深浅色模式下,采用不同的状态栏背景色与字体颜色。

3.1 动态调整状态栏文字颜色
typescript 复制代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct Index {
  @StorageProp('currentColorMode') 
  @Watch('onCurrentColorModeChange') 
  currentColorMode: ConfigurationConstant.ColorMode = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
  
  private windowObj: window.Window | null = null;

  aboutToAppear(): void {
    // 获取窗口对象
    window.getLastWindow(this.getUIContext().getHostContext(), (err: BusinessError, data) => {
      if (err.code) {
        hilog.error(0x0000, 'Index', `getLastWindow failed. code=${err.code}, message=${err.message}`);
        return;
      }
      this.windowObj = data;
    })
  }

  // 监听颜色模式变化,动态调整状态栏
  onCurrentColorModeChange(): void {
    if (!this.windowObj) {
      return;
    }
    try {
      if (this.currentColorMode === ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT) {
        // 浅色模式:状态栏文字使用深色
        this.windowObj?.setWindowSystemBarProperties({
          statusBarContentColor: '#000000'
        })
      } else if (this.currentColorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK) {
        // 深色模式:状态栏文字使用浅色
        this.windowObj?.setWindowSystemBarProperties({
          statusBarContentColor: '#FFFFFF'
        })
      }
    } catch (error) {
      let err = error as BusinessError;
      hilog.error(0x0000, 'Index', `setWindowSystemBarProperties failed`);
    }
  }
}

知识点:

  • @StorageProp 装饰器用于从 AppStorage 中读取状态,单向同步
  • @Watch 装饰器用于监听状态变化,触发回调函数
  • setWindowSystemBarProperties 用于设置系统状态栏属性
  • 沉浸式布局时需要特别注意状态栏文字颜色与背景的对比度

3.2 沉浸式布局适配
typescript 复制代码
// entry/src/main/ets/entryability/EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
  windowStage.loadContent('pages/Index', (err) => {
    const windowClass = windowStage.getMainWindowSync();
    // 设置全屏布局
    windowClass.setWindowLayoutFullScreen(true);

    // 获取系统安全区域
    const avoidAreaTop = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
    const avoidAreaBottom = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
    const topRectHeight = avoidAreaTop.topRect.height;
    const bottomRectHeight = avoidAreaBottom.bottomRect.height;
    
    // 存储安全区域高度供页面使用
    AppStorage.setOrCreate('topRectHeight', windowClass.getUIContext().px2vp(topRectHeight));
    AppStorage.setOrCreate('bottomRectHeight', windowClass.getUIContext().px2vp(bottomRectHeight));
  });
}
typescript 复制代码
// entry/src/main/ets/pages/Index.ets
@Entry
@Component
struct Index {
  @StorageProp('topRectHeight') topRectHeight: number = 0;
  @StorageProp('bottomRectHeight') bottomRectHeight: number = 0;

  build() {
    Navigation(this.navPathStack) {
      Tabs({ controller: this.tabsController }) {
        // ...
      }
      .barHeight(52 + this.bottomRectHeight)  // 底部导航栏高度 + 安全区域
    }
    .padding({ top: this.topRectHeight })      // 顶部安全区域
  }
}

知识点:

  • setWindowLayoutFullScreen(true) 开启沉浸式布局
  • getWindowAvoidArea() 获取系统安全区域(状态栏、导航栏等)
  • 需要手动处理内容与系统UI的间距,避免内容被遮挡

4. Web页面适配深色模式

Web页面的内容不会自动跟随系统颜色模式进行切换。若需要Web页面进行深浅色适配,需要在Web页面内通过媒体查询的方式单独设置深色模式下的页面样式,并通过Web组件的darkMode()属性来控制Web页面是否启用深色模式。

typescript 复制代码
Web({ src: 'https://example.com', controller: this.controller })
  .darkMode(WebDarkMode.On) // 启用深色模式

同时,在Web页面中添加媒体查询样式:

css 复制代码
@media (prefers-color-scheme: dark) {
  body {
    background-color: #121212;
    color: #ffffff;
  }
}

常见问题及解决方案

参考链接: 主题API

问题1:自定义弹窗无法跟随系统深浅色切换,或者只有部分内容跟随切换

可能原因:自定义弹窗没有设置背景色时,系统对默认弹窗背景色做了深色模式适配,但弹窗内容(特别是自定义内容)无法自动适配。

解决措施:参考上述颜色资源适配方案,对弹窗内容的所有颜色属性进行资源化处理,确保深浅色模式下都有对应的颜色值。

问题2:媒体资源适配时显示"文件已存在"

问题现象:适配媒体资源时,出现对应资源在另外的目录已存在的弹窗提示。

解决措施:直接点击弹窗内的"continue"按钮即可,这不会导致适配错误。这个提示仅表示文件命名冲突,不影响功能。

问题3:深色模式下部分文字对比度不足

可能原因:颜色值设置不当,未考虑深色背景下的可读性要求。

解决措施:检查颜色值,确保深色模式下的文字颜色与背景色对比度符合WCAG标准(至少5:1)。可使用在线对比度检测工具验证。

总结

深色模式适配是现代应用开发中不可或缺的一环,它不仅提升用户体验,还能为用户提供更多选择。通过本文的介绍,我们了解了深色模式适配的核心原理和实现方法:

  1. 资源目录机制是深色模式适配的基础,通过base和dark目录的同名资源实现自动切换
  2. 颜色资源适配需要将颜色值抽离到资源文件中,确保所有颜色都有深浅两种模式下的对应值
  3. 媒体资源适配可通过SVG图标染色或多套资源实现
  4. 状态栏适配需要考虑沉浸式场景下的文字可读性
  5. Web内容适配需要通过Web组件属性和页面内媒体查询共同实现

做好深色模式适配,不仅能让应用在不同场景下都有出色的视觉效果,还能体现开发团队对用户体验的细致考量。


最后借花献佛,用我的好朋友小马哥(jsmask)做的3D卡牌,祝福大家马年代码如骏马,奔跑无 Bug

相关推荐
阿林来了4 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别停止与取消
flutter·语音识别·harmonyos
松叶似针5 小时前
Flutter三方库适配OpenHarmony【secure_application】— 应用生命周期回调注册
flutter·harmonyos
无巧不成书02185 小时前
【RN鸿蒙教学|第12课时】进阶实战+全流程复盘:痛点攻坚与实战项目初始化
react native·华为·开源·交互·harmonyos
键盘鼓手苏苏6 小时前
Flutter for OpenHarmony 实战:flutter_redux 全局状态机与单向数据流
flutter·华为·harmonyos
阿林来了7 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 麦克风权限申请实现
flutter·harmonyos
松叶似针7 小时前
Flutter三方库适配OpenHarmony【secure_application】— 窗口事件监听与应用切换检测
flutter·harmonyos
阿林来了8 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— OpenHarmony 插件工程创建
flutter·harmonyos·鸿蒙
松叶似针8 小时前
Flutter三方库适配OpenHarmony【secure_application】— MethodChannel 通信协议设计
flutter·harmonyos
无巧不成书02189 小时前
Kotlin Multiplatform(KMP)核心解析
android·开发语言·kotlin·交互·harmonyos