【高心星出品】

深色主题切换

概述

深色模式(Dark Mode)又称为暗色模式,是与日常应用使用过程中的浅色模式(Light Mode)相对应的一种UI主题。深色模式最早来源于人机交互领域的研究和实践,该模式并非简单地将页面背景变为黑色,文字内容变为白色,而是提供一整套适配深色模式的应用配色主题。深色模式相较浅色模式更加柔和,能减少亮度对用户眼睛造成的刺激和疲劳,此外深色模式能在一定程度上降低应用功耗,提升续航表现。

应用深色模式适配,需遵循基本的UX设计原则,保障应用页面内容的易读性、舒适性和一致性。应用适配过程主要包含字体颜色、元素背景色等颜色资源的适配,媒体资源如图片图标的适配,以及系统状态栏的适配,此外需要对一些特殊情况如使用了Web组件加载的Web页面进行处理。

本文主要将介绍深色模式的适配过程,同时会列举出适配过程中的常见问题及解决方案。

实现原理

当系统切换到深色模式后,应用内可能会出现部分内容切换到深色主题的情况,例如状态栏、弹窗背景色、系统控件等,会导致应用内页面效果错乱。

为应对上述情况,需要对应用进行深色模式下的内容适配,目前该适配主要依靠资源目录。当系统对应的设置项发生变化后(如系统语言、深浅色模式等),应用会自动加载对应资源目录下的资源文件。

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

说明

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

一般情况下深浅色模式切换不会导致应用界面产生结构上的变化,而是页面结构一致但是采用不同的主题配色、配图等,使得整个应用在切换到深色模式后依然保持自然美观,以下为深色模式适配的UX示例。

图1 深色模式适配UX示例图
在这里插入图片描述

从上图中可以看到,在应用进行深色模式适配过程中主要的适配项有颜色资源适配、媒体资源适配、状态栏适配,除此之外若应用内使用了Web组件加载的Web页面,那么还需对Web页面适配深色模式,具体适配方案可点击对应链接跳转到具体章节查看。

目前业内应用向用户提供的深浅色模式切换有以下两种常见方式。

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

    实现上,需要开发者使用setColorMode()方法将ColorMode设置为COLOR_MODE_NOT_SET(未设置颜色模式),然后应用在运行过程中就可以自动感知到系统颜色模式切换,若应用完成了深浅色模式适配,将自动切换到对应的颜色模式。

  • 应用内提供手动控制深浅色的开关供用户自行选择

    实现上,切换深色模式需要调用setColorMode()方法将ColorMode设置为COLOR_MODE_DARK(深色模式),切换浅色模式需要将ColorMode设置为COLOR_MODE_LIGHT(浅色模式),这样就可以完成对应用深浅色的手动控制。

综上分析,深色模式适配内容如下表所示。

表1 深色模式适配内容

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

深色模式适配

颜色资源适配

颜色资源适配是将页面元素的颜色抽离到限定词目录中,让应用在不同的深浅色模式下使用不同限定词目录中的颜色值,从而达成应用页面元素在深浅色下不同的颜色表现。若应用适配代码存在错误,那么在切换到深色模式后,页面元素会由于对比度过低导致用户识别困难,以下为颜色资源适配错误的效果示例。

表2 颜色资源适配深色模式错误效果示例

浅色模式 深色模式
在这里插入图片描述 在这里插入图片描述

上述页面效果在浅色模式下显示正常,但是当切换到深色模式后 ,弹窗内文字与弹窗背景色不满足背景色对比度不低于5:1,用户识别弹窗内容困难。上述效果的关键问题在于使用自定义弹窗时,若未手动指定弹窗背景色,系统默认对弹窗背景色做了深浅色适配,但是弹窗内的具体内容特别是开发者的自定义内容无法自动适配深色模式,于是当系统切换到深色模式下,弹窗背景色自动深色,而弹窗内容保持与浅色模式一致的颜色,导致内容无法看清,该类问题对应解决方案有以下两种。

  • 方式一:使用系统资源(优先建议)。使用受支持的系统资源会自动适配深色模式,开发者可查看系统资源获取受支持的系统资源。

  • 方式二:使用自定义主题,若开发者需要定制在深浅色模式下不同的颜色表现,就需要使用自定义主题,以下为具体实现步骤参考。

    1. 在src/main/resources/base/element/color.json文件中定义页面元素在浅色模式下的颜色值,此处定义了弹窗内文字在浅色模式下颜色为黑色。

      {
        "color": [
          {
            "name": "text_color",
            "value": "#000000"
          }
        ]
      }
      
    2. 在src/main/resources/dark/element/color.json文件中定义页面元素在深色模式下的颜色值(若有不存在的目录或文件需自行创建),此处定义了弹窗内文字在深色模式下颜色为白色。

      {
        "color": [
          {
            "name": "text_color",
            "value": "#FFFFFF"
          }
        ]
      }
      
    3. 在代码中引用自定义的颜色资源值,使用$r加载自定义颜色资源,系统将自动在应用深浅色变化时,加载对应限定词目录下的资源文件,从而改变页面元素的颜色完成深浅色适配。

      @Entry
      @Component
      struct Index {
        private customDialogComponentId: number = 0;
        private promptAction = this.getUIContext().getPromptAction();
      
        @Builder
        customDialogPositiveExample() {
          Column() {
            Text('授权成功')
              .fontColor($r('app.color.text_color'))
              // ...
            Text('授权码:441132')
              .fontColor($r('app.color.text_color'))
      
            Row({ space: 8 }) {
              Button('复制', { buttonStyle: ButtonStyleMode.TEXTUAL })
                .fontColor($r('app.color.text_color'))
                // ...
              Button('确定', { buttonStyle: ButtonStyleMode.TEXTUAL })
              // ...
            }
            // ...
          }
          // ...
        }
      
        build() {
          // ...
        }
      }
      

      代码逻辑走读:

      1. 组件定义与私有变量初始化
        • 使用@Entry@Component装饰器定义了一个名为Index的组件。
        • 初始化了两个私有变量:customDialogComponentId(类型为number)和promptAction(通过getUIContext().getPromptAction()获取)。
      2. 自定义对话框构建
        • 使用@Builder装饰器定义了一个名为customDialogPositiveExample的方法,用于构建自定义对话框的内容。
        • Column布局中,添加了两个Text组件,分别显示“授权成功”和“授权码:441132”。
        • Row布局中,添加了两个Button组件,分别用于“复制”和“确定”操作。
      3. 构建方法
        • 定义了build方法,用于构建组件的整体界面,但具体实现细节未在提供的代码中展示。

表3 颜色资源完成深色模式适配效果示例

浅色模式 深色模式
在这里插入图片描述 在这里插入图片描述

媒体资源适配

媒体资源适配即在深浅模式下采用不同颜色表现的图片或图标等媒体资源,从而达成更好的用户体验,以下为应用内的图标未适配深色模式的效果示例,未适配内容以黄虚线框出。

表4 应用内图标未适配深色模式效果示例

浅色模式 深色模式
在这里插入图片描述 在这里插入图片描述

上述错误示例效果的关键问题在于对于应用内的图标并未做深色模式下的适配,于是图标的颜色与应用浅色时一致,而两者对比度过低,导致切换到深色模式后应用内图标无法看清,媒体资源的适配有以下两种方式。

  • 方式一:若适配简单图标并且图标格式为SVG类型,那么只需要结合颜色资源适配并使用Image组件的fillColor属性(若使用Symbol则使用SymbolGlyph的fontColor属性),在不同的深浅色下设置为不同的填充色即可完成深色模式的适配。

  • 方式二:若需要适配图片或适配图标,但图标不为SVG类型,那么就需要使用资源目录的方式进行深色模式的适配,具体实现步骤参考如下。

    1. 在src/main/resources/base/media目录中放入浅色模式下的图片资源,并按需重命名。

    2. 在src/main/resources/dark/media目录中放入深色模式下的图片资源(若有不存在的目录需自行创建),并保证资源名称与上一步放入的资源名称一致。

    3. 在代码中使用Image组件加载对应的图片资源,此处放入资源名称为bell。

      @Component
      struct Home {
        // ...
      
        build() {
          Scroll() {
            Column() {
              // ...
      
              Stack({ alignContent: Alignment.TopStart }) {
                Image($r('app.media.bell'))
                  .width('100%')
                  .borderRadius(12)
                  .objectFit(ImageFit.Cover)
      
                // ...
              }
              // ...
            }
          }
          // ...
        }
      }
      

      代码逻辑走读:

      1. 组件定义:使用 @Component装饰器定义了一个名为 Home的组件,表明这是一个可重用的UI组件。
      2. 构建方法:定义了 build()方法,该方法负责描述组件的UI结构。
      3. 滚动视图:在 build()方法中,使用 Scroll()组件创建了一个可滚动的视图,允许用户在垂直方向上滚动内容。
      4. 列布局:在 Scroll()组件内部,使用 Column()组件创建了一个垂直布局,用于排列子组件。
      5. 堆叠布局:在 Column()组件内部,使用 Stack({ alignContent: Alignment.TopStart })组件创建了一个堆叠布局,允许子组件重叠显示。
      6. 图片组件:在 Stack()组件内部,使用 Image($r('app.media.bell'))组件加载并显示一张图片,图片的宽度设置为100%,并应用了圆角和覆盖式填充效果。
      7. 注释和占位符:代码中有多个注释和占位符(// ...),表明在实际应用中可能会有额外的逻辑或样式代码。

表5 应用内图标完成深色模式适配效果示例

浅色模式 深色模式
在这里插入图片描述 在这里插入图片描述

状态栏适配

状态栏适配即在深浅色模式下,采用不同的状态栏背景色与字体颜色。若应用未启用沉浸式,那么默认情况下,浅色模式下状态栏为白底黑字,深色模式下状态栏为黑底白字。当应用启用了沉浸式,状态栏背景色与应用背景色保持一致,而状态栏文字会默认在浅色模式下保持黑色,而在深色模式下保持白色,若应用在浅色模式下设置了深色背景或在深色模式下设置了浅色背景,都会造成状态栏背景色与状态栏字体颜色对比度过低而显示异常。错误效果示例如下图,应用设置了沉浸式并在浅色模式下具有纯黑色的背景色,导致状态栏的日期电量等文本内容无法看清。

图2 状态栏适配错误效果
在这里插入图片描述

上述错误效果的主要问题在于页面的背景色固定为黑色,当系统切换到浅色模式后,状态栏文字默认切换到黑色,此时状态栏背景色与文字颜色一致,于是状态栏中的文字就不可见了,此类问题修改方案有以下两种。

  • 若背景色可以做深浅色适配,则采用颜色资源适配的方案对应用背景色进行适配,背景色适配时需考虑到状态栏文字在深浅色模式下的默认表现。

    // src/main/ets/pages/Index.ets
    @Entry
    @Component
    struct Index {
      // ...
      build() {
        Navigation(this.navPathStack) {
          // ...
        }
        .backgroundColor($r('app.color.app_background_color'))
        .hideTitleBar(true)
        // ...
      }
    }
    

    代码逻辑走读:

    1. 组件定义:使用@Entry@Component装饰器定义了一个名为Index的组件,表明这是一个入口组件。

    2. 成员变量与构造函数:组件内部可能定义了一些成员变量和构造函数,但具体代码未提供。

    3. build方法

      :这是组件的核心方法,用于构建UI界面。

      • 调用Navigation(this.navPathStack)方法来创建一个导航容器,navPathStack可能是用于管理导航路径的栈结构。
      • Navigation容器内部,可能定义了多个子组件或导航路径。
      • 使用.backgroundColor($r('app.color.app_background_color'))设置导航容器的背景颜色,颜色值从应用资源中获取。
      • 使用.hideTitleBar(true)隐藏标题栏,使得界面更加简洁。
    4. 其他设置:代码中可能还有其他设置,如状态栏的设置,但由于代码未完全显示,具体内容无法确定。

  • 若背景色无法做深浅色适配,或做了深浅色适配,但是沉浸式颜色与默认的状态栏文字颜色对比度较低,这种情况下需要获取当前的深浅色并动态设置状态栏字体颜色。

    1. 在EntryAbility中获取并维护当前深浅色状态,在onCreate时将当前colorMode放在AppStorage中,并在配置变化的

      onConfigurationUpdate()

      回调中动态更新深浅色状态。

      export default class EntryAbility extends UIAbility {
        onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
          AppStorage.setOrCreate<ConfigurationConstant.ColorMode>('currentColorMode', this.context.config.colorMode);
          hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
        }
      
        // ...
      
        onConfigurationUpdate(newConfig: Configuration): void {
          const currentColorMode: ConfigurationConstant.ColorMode | undefined = AppStorage.get('currentColorMode');
          if (currentColorMode !== newConfig.colorMode) {
            AppStorage.setOrCreate<ConfigurationConstant.ColorMode>('currentColorMode', newConfig.colorMode);
          }
        }
      }
      

      代码逻辑走读:

      1. 类定义与继承

        • export default class EntryAbility extends UIAbility:定义了一个名为 EntryAbility的类,并继承自 UIAbility,表示这是一个用户界面能力类。
      2. 生命周期方法

        • onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void
          

          这是EntryAbility的构造函数,在应用创建时被调用。

          • AppStorage.setOrCreate<ConfigurationConstant.ColorMode>('currentColorMode', this.context.config.colorMode);:将当前颜色模式存储到 AppStorage中。
          • hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');:记录日志信息,表示应用的创建事件。
      3. 配置更新处理

        • onConfigurationUpdate(newConfig: Configuration): void
          

          当应用的配置更新时被调用。

          • const currentColorMode: ConfigurationConstant.ColorMode | undefined = AppStorage.get('currentColorMode');:从 AppStorage中获取当前颜色模式。

          • if (currentColorMode !== newConfig.colorMode)
            

            检查当前颜色模式是否与新配置中的颜色模式不同。

            • AppStorage.setOrCreate<ConfigurationConstant.ColorMode>('currentColorMode', newConfig.colorMode);:如果不同,则更新 AppStorage中的颜色模式为新配置中的颜色模式。
    2. 在页面内监听深浅色模式状态变量的变化,并根据变化后的深浅色模式来动态设置状态栏文本颜色。

      @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) => {
            this.windowObj = data;
          })
        }
      
        onCurrentColorModeChange(): void {
          if (!this.windowObj) {
            return;
          }
          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'
            })
          }
        }
      
        // ...
      
        build() {
          // ...
        }
      }
      

      代码逻辑走读:

      1. 组件定义与属性初始化
        • 使用@Entry@Component装饰器定义了一个名为Index的组件。
        • 初始化了一个带有@StorageProp@Watch装饰器的currentColorMode属性,用于存储当前的颜色模式,并监听其变化。
        • 初始化了一个windowObj属性,用于存储窗口对象,初始值为null
      2. 生命周期方法aboutToAppear
        • 当组件即将出现时,调用window.getLastWindow方法获取当前窗口对象,并将其赋值给windowObj属性。
      3. 颜色模式变化处理
        • 定义了一个名为onCurrentColorModeChange的方法,用于处理currentColorMode属性的变化。
        • 如果windowObjnull,则直接返回,不进行任何操作。
        • 根据currentColorMode的值,设置窗口的系统栏属性:
          • 如果为亮色模式(COLOR_MODE_LIGHT),则设置状态栏内容颜色为黑色(#000000)。
          • 如果为暗色模式(COLOR_MODE_DARK),则设置状态栏内容颜色为白色(#FFFFFF)。
      4. 构建方法build
        • 定义了一个build方法,用于构建组件的UI结构,但具体实现未在提供的代码中展示。

图3 状态栏适配深色模式后效果
onCurrentColorModeChange的方法,用于处理currentColorMode属性的变化。 - 如果windowObjnull,则直接返回,不进行任何操作。 - 根据currentColorMode的值,设置窗口的系统栏属性: - 如果为亮色模式(COLOR_MODE_LIGHT),则设置状态栏内容颜色为黑色(#000000)。 - 如果为暗色模式(COLOR_MODE_DARK),则设置状态栏内容颜色为白色(#FFFFFF)。 4. **构建方法build**: - 定义了一个build`方法,用于构建组件的UI结构,但具体实现未在提供的代码中展示。

图3 状态栏适配深色模式后效果
在这里插入图片描述

Logo

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

更多推荐