HarmonyOS 深色模式适配:dark 限定词资源与 isDark 状态管理

一、引言

深色模式早已不是"锦上添花"的视觉选项,而是短视频、视频播放这类高频使用的应用的必备能力:夜间刷视频时,刺眼的白色页面会显著加速视觉疲劳;而在沉浸式全屏播放场景下,深色界面更是保证画面观感一致的前提。multi-short-video 工程覆盖直板机、折叠屏、平板、PC、电视与手表六类设备,深浅色适配既要做到"资源零成本切换",又要处理"页签切换带动主题变化"这类业务态联动,还要为 PC 端的窗口装饰(标题栏按钮)单独设置深色样式。

HarmonyOS 的深色模式适配遵循一条核心理念:差异化是声明出来的,不是判断出来的。颜色、图片等资源通过 dark 限定词目录在编译期声明两套取值,系统在运行期自动匹配;业务代码中真正需要"判断"的,只有跟随业务状态变化的主题切换逻辑(如本工程首页"推荐页浅色、个人页深色"的产品诉求)。本文以工程内真实代码为例,拆解 dark 限定词资源、color.json 管理、isDark 状态同步与深色图标四部分。

breakpoint-system

二、dark 限定词目录与 color.json 深浅色资源管理

限定词(Qualifier)是资源自适应的核心机制:目录名中 dark 表示"设备处于深色模式时命中此目录",base 则是任何条件下的兜底目录。系统按"最匹配优先"规则选择资源,命不中任何限定词时回退 base。本工程在 features/multishortvideocommentfeatures/multishortvideoindividual 两个模块中同时维护了 basedark 两份颜色资源,代码统一用 $r('app.color.xxx') 引用,深浅色切换在运行期自动完成。

以评论模块为例,浅色与深色下的颜色取值对比:

资源名base(浅色)dark(深色)用途

text_input_bg_xs#F2F2F2#37393A小屏评论输入框背景
text_input_bg_md#2C2E2D#2C2E2D大屏输入框背景(恒深色)
text_input_row_bg_xs#FFFFFF#66000000评论输入行背景
send_bg_xs#FFFFFF#FF8B9194小屏发送按钮背景
font_emphasize_light#0A59F7#0A59F7强调色(浅色变体)
font_emphasize_dark#317AF7#317AF7强调色(深色变体)

对应的两份资源文件内容(节选):

// d:\HarmonyOS\WorkSpace\multi-short-video\features\multishortvideocomment\src\main\resources\base\element\color.json
{
  "color": [
    { "name": "text_input_bg_xs", "value": "#F2F2F2" },
    { "name": "text_input_row_bg_xs", "value": "#FFFFFF" },
    { "name": "send_bg_xs", "value": "#FFFFFF" }
  ]
}

// d:\HarmonyOS\WorkSpace\multi-short-video\features\multishortvideocomment\src\main\resources\dark\element\color.json
{
  "color": [
    { "name": "text_input_bg_xs", "value": "#37393A" },
    { "name": "text_input_row_bg_xs", "value": "#66000000" },
    { "name": "send_bg_xs", "value": "#FF8B9194" }
  ]
}

注意一个细节:dark 目录只需覆盖"需要变化"的资源,未列出的资源自动回退 base 取值,因此 text_input_bg_md 在两份文件中值相同(都是深色输入框,属正常复用)。个人作品页模块同样遵循该模式,introduction_bg_xs 从浅色的 #FFFFFF 变为深色的 #000000tags_bg_xs#F5F5F5 变为 #37393A,说明同一套机制可以低成本复制到每个模块

三、isDark 状态同步与主题切换

资源层解决了"系统深浅色"的自动适配,但本工程还有一个业务层面的诉求:首页页签下"推荐"页呈现浅色,"我的"个人页呈现深色,且折叠屏等大屏设备上要求"我的"页恒为深色。这个状态与系统主题无关,必须在代码层维护并下发到所有需要感知的组件。工程的做法是:入口页用 @Provider('isDark') 提供状态,页签切换时按业务规则更新,公共组件与子页面用 @Consumer('isDark') 接收。

// d:\HarmonyOS\WorkSpace\multi-short-video\products\default\src\main\ets\view\Index.ets
@Entry
@ComponentV2
struct Index {
  @Provider('isDark') isDark: boolean = false;
  // ...
  build() {
    Navigation(this.pathStack) {
      MSVTabs({
        data: this.data,
        isDark: this.isDark,
        onIndexChange: (index: number) => {
          if (index === 0 && this.subTabIndex === 4) {
            this.isDark = false;   // 推荐页:浅色
          } else if (index === 4) {
            // 我的页:小屏深色,大屏按断点决定
            this.isDark = new WidthBreakpointType<boolean>(true, true, false, false)
              .getValue(this.windowInfo.widthBp)
          } else {
            this.isDark = true;    // 其余页签:深色
          }
        }
      })
    }
  }
}

isDark 通过 Provider/Consumer 树向整个页面子树广播:MSVTabs 的 TabBar 用 this.isDark ? params.iconDark : params.icon 选择图标、用 this.isDark ? this.selectedDarkColor : this.selectedLightColor 选择文字颜色(见 common/multishortvideobase/src/main/ets/components/MSVTabs.ets);个人页组件 Individual.ets 内声明 @Consumer('isDark') 消费同一份状态,用于选择 ic_modify / ic_modify_dark 等图标。这套 V2 装饰器方案相比逐层传参,解耦了"状态来源"与"状态消费方",新增一个需要感知主题的组件只需声明一个 @Consumer 即可。

四、图片与图标的深色资源管理

深色模式下,纯黑色图标会融入深色背景导致"看不见",因此图标需要成对提供。本工程有三种组织方式,按复用范围从小到大排列。

第一种是后缀命名 + 代码判断:如评论模块的点赞心形图标,浅色版 ic_heart.svg 填充色为黑色半透明(fill-opacity="0.6"),深色版 ic_heart_light.svg 填充色为白色半透明:

<!-- features/multishortvideocomment/src/main/resources/base/media/ic_heart.svg(节选) -->
<path id="矢量 1" d="M1.68001 6.22669C..." fill="rgb(0,0,0)" fill-opacity="0.600000024" />

<!-- features/multishortvideocomment/src/main/resources/base/media/ic_heart_light.svg(节选) -->
<path id="矢量 1" d="M1.68001 6.22669C..." fill="rgb(255,255,255)" fill-opacity="0.600000024" />

评论页用 WidthBreakpointType<Resource> 结合断点选择:小屏用 ic_heart,大屏(PC/平板,通常配深色背景)用 ic_heart_lightComment.ets 第 86~91 行):

MSVTextIcon({
  src: new WidthBreakpointType<Resource>($r('app.media.ic_heart'),
    $r('app.media.ic_heart'), $r('app.media.ic_heart_light'),
    $r('app.media.ic_heart_light')).getValue(this.windowInfo.widthBp),
  iconSize: deviceInfo.deviceType === 'tv' ? 28 : 16
})

第二种是 MSVDataModel 的 iconDark 字段:首页 TabBar 的"+"号图标通过数据模型同时携带深浅两版资源(products/default/.../viewmodel/MainTabsViewModel.ets):

this.mainTabsData.push(new MSVDataModel(add, '', true,
  $r('app.media.ic_plus'), $r('app.media.ic_plus_dark')));

MSVTabs 渲染时按 isDark 直接二选一,与第一种方式相比,把"成对资源"收进数据模型,UI 层不再需要条件表达式。

第三种是纯限定词目录方案:不区分后缀、将图片放进 dark 目录,靠系统自动切换。工程中 PC 模块的 ic_person.svg / ic_person_dark.svgic_ellipsis_message_badge_circle.svg / ic_ellipsis_message_badge_circle_dark.svg 目前采用后缀方案。当主题变体增多时,迁移到 dark/media 目录是更彻底的演进方向——代码引用不变,差异化完全由资源系统承担。

五、深色下视频页表现与 PC 端窗口装饰

视频页面天然是"深色优先":全屏沉浸式播放时,播放器区域为视频画面本身,四周控件浮层采用半透明深色底,与系统深色模式天然兼容。工程中 AdaptiveVideo 的交互浮层(点赞、评论、分享等按钮)叠加在视频之上,控件本身使用白色系图标,深浅色感知主要由外层页面承担,这正是短视频应用常见的设计取舍——视频场景深色是常态,浅色适配反而要克制

PC 端还多一层"窗口装饰"的深色问题:窗口标题栏的"最小化/最大化/关闭"按钮样式不随页面主题变化,需要显式设置。products/pc/.../pcability/MultiShortVideoPcAbility.ets 中:

import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';

windowStage?.getMainWindowSync().setDecorButtonStyle({
  colorMode: ConfigurationConstant.ColorMode.COLOR_MODE_DARK
});

这行代码把 PC 窗口装饰按钮固定为深色样式,与工程"PC 端内容区默认深色"的产品定位保持一致。类似地,状态栏文字颜色通过 setWindowSystemBarProperties({ statusBarContentColor: '#FFFFFF' }) 设置,保证深色内容区上的状态栏文字可读。

多设备视角下,深色策略还应随形态收敛。手表端 products/wearable/.../view/Index.ets 同样声明 @Provider('isDark') isDark: boolean = false,由 SubTabsComponent 消费——小屏设备上深色判断简单(内容区深色、控件浮层透明),无需复杂的断点逻辑;TV 端则更特殊:大屏沉浸式观看场景下,除了页面深色,还要保证遥控器焦点框与内容的对比度,工程让 TV 的页签与内容区保持深色基调,避免焦点框与浅色内容混叠。因此在推进深色适配时,各产品模块的 isDark 初始值与切换规则可以独立演进,公共组件(MSVTabs)只负责按 isDark 渲染、不感知具体业务规则——这再次体现了"公共层管能力、产品层管策略"的分层思想。

六、深浅色切换的验证与常见问题

深色模式上线前,需要一套可执行的验证清单,避免"代码写得对、实际切不过来"的尴尬。第一类是系统主题切换验证:在真机/模拟器的系统设置中切换浅色与深色,逐个页面检查颜色资源是否跟随变化——重点检查依赖 dark/element/color.json 的模块(评论输入框、个人页标签背景),确认未列出的资源正确回退 base。第二类是业务态主题验证:在首页"推荐/关注/我的"页签间来回切换,确认 @Provider('isDark') 按预期更新,MSVTabs 的文字色、图标与下划线同步变化,且折叠屏展开/折叠(断点变化)时"我的"页的深色策略(WidthBreakpointType<boolean>(true, true, false, false))正确命中。

工程实践中容易踩的三类典型问题值得提前防范:

  • 资源名冲突被"静默覆盖":不同模块定义了同名颜色(如都叫 text_input_bg_xs),模块内引用各自命中本模块资源、表面正常,但跨模块复用时容易拿错取值。规避手段是保持资源名语义化,并在评审时核对资源归属模块(对应第 55 篇文章的资源命名纪律)。
  • 深色图标与背景错配:图标成对资源靠 _dark/_light 后缀约定,但"系统深色模式"与"业务深色页面"是两套判断:ic_heart_light 的选用跟随断点而非系统主题,若产品后续要求手机端"我的"页也走深色,需同步调整 WidthBreakpointType 的取值分支,否则会出现浅色图标压在深色背景上不可见的问题。
  • 系统栏与页面主题脱节:状态栏文字颜色用 statusBarContentColor: '#FFFFFF' 写死为白色,在浅色页面会与浅色背景融为一体。更稳妥的做法是跟随页面主题动态设置,或在深色页面统一白字、浅色页面统一黑字,并纳入验证清单逐页检查。

把验证清单固化为"深浅色遍历"用例(配合第 54 篇文章的 UI 测试),每次发版前跑一遍,深色相关问题基本可以归零。

七、总结与最佳实践

深色模式适配在本工程中沉淀为四条可复制的最佳实践:

  • 颜色一律走 $r('app.color.xxx') 与限定词:深浅色取值声明在 base/dark 两份 color.json 中,业务代码不做任何条件判断,系统自动匹配;dark 目录只覆盖需要变化的资源,其余自动回退。
  • 业务态主题用 Provider/Consumer 管理:当"哪个页面是深色"由业务逻辑决定(而非系统主题)时,用 @Provider('isDark') 广播、@Consumer('isDark') 消费,避免逐层透传;断点参与决策时组合 WidthBreakpointType
  • 图标成对管理、命名可识别:深色变体统一 _dark/_light 后缀(ic_heart_light.svgic_plus_dark.png),或用 MSVDataModel.iconDark 字段把成对资源收进模型;变体增多后迁移到 dark/media 目录实现零判断切换。
  • 平台专属深色单独处理:PC 窗口装饰按钮用 setDecorButtonStyle({ colorMode: COLOR_MODE_DARK }),系统栏文字用 statusBarContentColor 配置,这类"窗口层"深色不走资源限定词,需要在 Ability 生命周期内显式设置。

遵循这四条,新模块接入深色模式时,只需补齐 dark/element/color.json 与成对图标,即可与既有页面保持一致体验。

Logo

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

更多推荐