Android values-night + style 继承到 HarmonyOS resources/dark:深浅色迁移要先拆主题链

这次看深浅色。

很多迁移会先从颜色文件开始。Android 有 res/values/colors.xmlres/values-night/colors.xml,HarmonyOS 有 resources/base/element/color.jsonresources/dark/element/color.json。如果只是“同名颜色在浅色和深色下取不同值”,这层可以迁。

旧 Android 项目麻烦在另一层:颜色经常不直接写在布局里,而是先进入 theme attribute,再进入 style。style 还可以继承、覆写、通过点号继续派生,最后才被某个 TextViewButton、自定义 View 或 Material 组件消费。深浅色变化发生在资源选择阶段,但影响会沿着 style/theme 链传到 View 树。

HarmonyOS 可以做深浅色资源适配,也可以在应用或局部 UI 上设置 color mode。但它没有 Android XML style 那种“主题属性 + 样式继承 + 控件默认样式 + 局部 theme overlay”组合出来的解析链。迁移时要先把 Android 的主题链拆开,再决定哪些变成资源 token,哪些变成 ArkUI 组件参数。

先看一个 Android 里很常见的写法

假设有一个消息卡片,浅色模式下是白底深字,深色模式下是深灰底浅字。Android 项目里可能不是直接在 layout 里写颜色,而是分散在这些文件里:

app/src/main/res/values/colors.xml
app/src/main/res/values-night/colors.xml
app/src/main/res/values/attrs.xml
app/src/main/res/values/themes.xml
app/src/main/res/values/styles.xml
app/src/main/res/layout/item_message_card.xml

先看默认颜色:

<!-- res/values/colors.xml -->
<resources>
    <color name="surface_message">#FFFFFF</color>
    <color name="on_surface_message">#1F2937</color>
    <color name="surface_message_highlight">#EAF2FF</color>
</resources>

深色资源只保留同名资源,改值:

<!-- res/values-night/colors.xml -->
<resources>
    <color name="surface_message">#1A1C20</color>
    <color name="on_surface_message">#F3F4F6</color>
    <color name="surface_message_highlight">#263449</color>
</resources>

到这里,只是 Android alternative resources 的基本能力:同一个 @color/surface_message,在 night configuration 下会命中 values-night 里的值。

但真实项目通常还会再加一层自定义属性:

<!-- res/values/attrs.xml -->
<resources>
    <attr name="messageCardBackground" format="color|reference" />
    <attr name="messageCardTextColor" format="color|reference" />
    <attr name="messageCardHighlightedBackground" format="color|reference" />
</resources>

然后把这些属性挂到 app theme 上:

<!-- res/values/themes.xml -->
<resources>
    <style name="Theme.Chat" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="messageCardBackground">@color/surface_message</item>
        <item name="messageCardTextColor">@color/on_surface_message</item>
        <item name="messageCardHighlightedBackground">@color/surface_message_highlight</item>
    </style>
</resources>

这一步把颜色变成了语义插槽。布局和控件不直接关心浅色/深色,也不直接关心 surface_message 这个资源名。它们只认 ?attr/messageCardBackground 这种 theme attribute。

style 继承把差异继续往下传

接下来是 style:

<!-- res/values/styles.xml -->
<resources>
    <style name="Widget.Chat.MessageCard">
        <item name="android:background">?attr/messageCardBackground</item>
        <item name="android:paddingStart">16dp</item>
        <item name="android:paddingEnd">16dp</item>
        <item name="android:paddingTop">12dp</item>
        <item name="android:paddingBottom">12dp</item>
    </style>

    <style name="TextAppearance.Chat.MessageCard" parent="TextAppearance.MaterialComponents.Body1">
        <item name="android:textColor">?attr/messageCardTextColor</item>
        <item name="android:textSize">16sp</item>
    </style>
</resources>

布局里只引用 style:

<!-- res/layout/item_message_card.xml -->
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    style="@style/Widget.Chat.MessageCard">

    <TextView
        android:id="@+id/messageTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.Chat.MessageCard"
        android:text="Message title" />

</LinearLayout>

这时深浅色链路是这样的:

系统 night mode
  -> Theme.MaterialComponents.DayNight
  -> res/values-night/colors.xml
  -> Theme.Chat 中的 messageCardBackground / messageCardTextColor
  -> Widget.Chat.MessageCard / TextAppearance.Chat.MessageCard
  -> item_message_card.xml

这段链路说明了 Android 深浅色的真实工作方式。values-night 只是入口,后面还有 theme attr 和 style 继承。读一个布局文件时,style="@style/Widget.Chat.MessageCard" 背后可能已经包含背景、padding、字体、状态色、默认控件风格和夜间模式差异。

style 还可以继续扩展:

<style name="Widget.Chat.MessageCard.Highlight">
    <item name="android:background">?attr/messageCardHighlightedBackground</item>
</style>

这个写法利用了 Android style 的点号继承。Widget.Chat.MessageCard.Highlight 会继承 Widget.Chat.MessageCard 的 padding 等属性,只覆写背景。也可以显式写 parent="@style/Widget.Chat.MessageCard"

<style
    name="Widget.Chat.MessageCard.Compact"
    parent="@style/Widget.Chat.MessageCard">
    <item name="android:paddingTop">8dp</item>
    <item name="android:paddingBottom">8dp</item>
</style>

点号继承容易被漏看,因为它看起来像普通命名分组。Widget.Chat.MessageCard.Highlight 这个名字里没有写 parent,但只要项目里存在 Widget.Chat.MessageCard,它就会被当成父 style。再往下还可以继续接:

<style name="Widget.Chat.MessageCard.Highlight.Dense">
    <item name="android:paddingTop">8dp</item>
    <item name="android:paddingBottom">8dp</item>
</style>

这时继承链已经进入样式解析关系,不能按普通字符串前缀处理:

Widget.Chat.MessageCard
  -> Widget.Chat.MessageCard.Highlight
  -> Widget.Chat.MessageCard.Highlight.Dense

Dense 可能只写了上下 padding,但运行时仍然带着 MessageCard 的左右 padding、Highlight 的背景色,以及父 style 中的其他属性。迁移时如果只把 Dense 里明面上的两行复制到 ArkUI,就会漏掉它从父 style 继承来的样式。

还有一个细节:如果同一个 style 同时写了 parent,显式 parent 优先,点号继承不再按名字前缀推断:

<style
    name="Widget.Chat.MessageCard.Highlight"
    parent="@style/Widget.Chat.AlertCard">
    <item name="android:background">?attr/messageCardHighlightedBackground</item>
</style>

这种情况下 Highlight 继承的是 Widget.Chat.AlertCard,不是 Widget.Chat.MessageCard。所以迁移时不能只按点号拆名字,还要看有没有显式 parent

于是 Android 的深浅色灵活度不只在于“有一份 night 资源”。它可以把同一个视觉系统拆成几层:

  • color:浅色/深色下的实际值。
  • attr:业务语义名,比如 message card 背景色。
  • theme:把语义名绑定到当前应用、Activity 或局部 View 树。
  • style:把背景、文字、间距、状态样式打包给控件。
  • style inheritance:在默认样式之上继续扩展、覆写。
  • layout:只引用 style,避免每个 XML 重复写属性。

一个组件的深色适配,可以不改 layout,只改 values-night,或者只改某个 Activity 的 theme,或者只改一个派生 style。灵活度来自这几层之间的间接引用。

还有一层:局部 theme overlay

Android 的 android:theme 也可以挂到某个 View 或容器上,让这一段 View 树使用另一组 theme attr。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.Chat.Important">

    <TextView
        style="@style/TextAppearance.Chat.MessageCard"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Important message" />

</LinearLayout>

overlay 里只覆写局部需要的属性:

<style name="ThemeOverlay.Chat.Important">
    <item name="messageCardTextColor">@color/on_surface_important</item>
</style>

这样 TextAppearance.Chat.MessageCard 还是同一个 style,但它里面的 ?attr/messageCardTextColor 会在局部 theme 里重新解析。同一个 XML style,在不同 theme scope 下会变出不同颜色。迁移时如果只看 style 文件本身,会漏掉局部 theme 带来的差异。

迁移旧 Android 项目时,如果只搜 values-night,会漏掉这类差异。还要搜:

?attr/
<attr name=
parent=
style=
android:theme=
ThemeOverlay
TextAppearance
Widget.

这些也是深浅色扩散到 UI 的路径。

HarmonyOS:resources/dark 能承接同名颜色,但不承接 XML style 链

HarmonyOS 的资源可以这样组织:

entry/src/main/resources/base/element/color.json
entry/src/main/resources/dark/element/color.json
entry/src/main/resources/base/profile/main_pages.json
entry/src/main/module.json5

浅色颜色:

// resources/base/element/color.json
{
  "color": [
    {
      "name": "message_card_bg",
      "value": "#FFFFFF"
    },
    {
      "name": "message_card_text",
      "value": "#1F2937"
    },
    {
      "name": "message_card_highlight_bg",
      "value": "#EAF2FF"
    }
  ]
}

深色颜色:

// resources/dark/element/color.json
{
  "color": [
    {
      "name": "message_card_bg",
      "value": "#1A1C20"
    },
    {
      "name": "message_card_text",
      "value": "#F3F4F6"
    },
    {
      "name": "message_card_highlight_bg",
      "value": "#263449"
    }
  ]
}

ArkUI 页面里引用同名资源:

@Component
struct MessageCard {
  title: string = ''
  highlighted: boolean = false

  build() {
    Column() {
      Text(this.title)
        .fontSize(16)
        .fontColor($r('app.color.message_card_text'))
        .lineHeight(22)
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
    .borderRadius(12)
    .backgroundColor(
      this.highlighted
        ? $r('app.color.message_card_highlight_bg')
        : $r('app.color.message_card_bg')
    )
  }
}

这段代码能完成资源层迁移:同一个 $r('app.color.message_card_bg') 在浅色和深色下取不同值。启动页也可以使用同一套资源机制。比如 module.json5 里:

{
  "abilities": [
    {
      "name": "EntryAbility",
      "startWindowBackground": "$color:start_window_background"
    }
  ]
}

然后在 resources/base/element/color.jsonresources/dark/element/color.json 里分别定义 start_window_background。这样启动窗口背景也可以跟随深浅色。

应用级模式可以在 EntryAbility 中设置:

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

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    this.context
      .getApplicationContext()
      .setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
  }
}

COLOR_MODE_NOT_SET 表示不强制应用色彩模式,跟随系统或默认配置。若产品需要应用内切换,也可以改成明确的 light/dark 模式。ArkUI 里还有局部主题能力,例如 WithThemecolorMode 支持 ThemeColorMode.SYSTEMThemeColorMode.LIGHTThemeColorMode.DARK,适合处理一小段 UI 的显示模式。

这些能力并不等价于 Android 的 XML style 继承链。HarmonyOS 这里有资源同名选择,有应用/局部 color mode,有 ArkUI 组件属性;缺的是 Android 那种:

theme attr -> style -> style parent -> style dot inheritance -> view XML

这条链迁不过来。迁移时要把它重新设计成 ArkUI 组件结构。

这更像 UI 复用方式换了

Android View/XML 体系里,style 承担了大量“复用一组控件属性”的工作。背景、padding、文字外观、状态色、局部 theme 都能放进 XML 资源,再通过 parent 和点号继承扩展。它适合长期演进的 View 项目,也让旧项目里积累了大量隐式样式关系。

ArkUI 的复用入口更靠近组件本身。一个消息卡片的视觉变体,通常不再写成:

@style/Widget.Chat.MessageCard.Highlight.Dense

而是写成:

MessageCard({
  title: 'Important',
  highlighted: true,
  compact: true
})

这种写法少了 XML style 继承的自由度,但组件边界更明显。哪些是颜色 token,哪些是组件默认属性,哪些是业务状态,代码里能直接看到。迁移 Android 老项目时,重点不在于给 styles.xml 找一个 HarmonyOS 同名文件,而在于把 style 树拆成设计 token 和 ArkUI 组件 API。

迁移时要把 style 拆成三类东西

拿前面的 Widget.Chat.MessageCard 举例,迁到 HarmonyOS 时可以这样拆。

第一类是颜色 token,放进资源:

message_card_bg
message_card_text
message_card_highlight_bg

这些有深浅色差异,放到 base/element/color.jsondark/element/color.json

第二类是稳定结构,写进 ArkUI 组件:

.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.borderRadius(12)

这些不是深浅色差异,不需要做成资源。Android 里它们可能被塞在 style 里,是为了 XML 复用;HarmonyOS 里可以通过自定义组件复用。

第三类是变体,写成组件参数:

@Component
struct MessageCard {
  title: string = ''
  highlighted: boolean = false
  compact: boolean = false

  build() {
    Column() {
      Text(this.title)
        .fontSize(16)
        .fontColor($r('app.color.message_card_text'))
    }
    .padding({
      left: 16,
      right: 16,
      top: this.compact ? 8 : 12,
      bottom: this.compact ? 8 : 12
    })
    .backgroundColor(
      this.highlighted
        ? $r('app.color.message_card_highlight_bg')
        : $r('app.color.message_card_bg')
    )
    .borderRadius(12)
  }
}

Android 里的:

@style/Widget.Chat.MessageCard
@style/Widget.Chat.MessageCard.Highlight
@style/Widget.Chat.MessageCard.Compact

在 HarmonyOS 里更适合变成:

MessageCard({ title: 'Normal' })
MessageCard({ title: 'Highlighted', highlighted: true })
MessageCard({ title: 'Compact', compact: true })

迁移思路也随之变化:Android 用 XML style 继承表达组件变体;HarmonyOS 更适合用 ArkUI 组件封装、参数和状态表达组件变体。

不能只做一张颜色名映射表

深浅色迁移很容易写成这样:

Android HarmonyOS
res/values/colors.xml resources/base/element/color.json
res/values-night/colors.xml resources/dark/element/color.json
@color/name $r('app.color.name')

这张表有用,但只覆盖资源文件这一层。旧 Android 项目还要检查四条链。

第一条是资源链:

@color/name -> values / values-night

这条可以直接迁成:

$r('app.color.name') -> resources/base / resources/dark

第二条是属性链:

?attr/colorSurface
?attr/messageCardBackground
?android:attr/textColorPrimary

这条不能机械迁。要先判断它是平台语义、Material 语义,还是业务语义。业务语义可以保留成 message_card_bg 这类 token;平台/Material 语义要结合 HarmonyOS 系统 token 或项目自己的设计 token 重命名。

第三条是 style 链:

Widget.Chat.MessageCard
TextAppearance.Chat.MessageCard
Widget.Chat.MessageCard.Highlight
Widget.Chat.MessageCard.Highlight.Dense

这条要先还原完整继承结果,再拆成组件默认参数和变体参数。尤其是点号继承,不能只看最后一级 style 写了什么;最后一级可能只覆写一两个属性,其余都来自父 style。不要在 HarmonyOS 里追求复刻 XML style 层级。

第四条是局部 theme 链:

android:theme="@style/ThemeOverlay.Chat.Important"

这条要逐个场景看。只改颜色的,可以考虑组件参数或局部 WithTheme;涉及整段页面视觉体系的,更适合在页面根组件里集中提供一组 token 或配置对象。

一个可执行的迁移清单

整理旧 Android 深浅色时,可以按下面顺序查:

  1. values-night,列出所有同名资源。
  2. ?attr/?android:attr/,找出布局和 style 里的间接颜色引用。
  3. <attr name=,找出项目自定义的主题属性。
  4. parent= 和点号 style,例如 Widget.App.Button.PrimaryWidget.App.Button.Primary.Small,确认 style 继承关系。
  5. android:theme=ThemeOverlay,确认局部覆写。
  6. 把颜色类差异迁到 resources/base / resources/dark
  7. 对每个派生 style 先展开父链,写出最终生效的背景、文字、间距、圆角、状态色。
  8. 把 style 里的布局、间距、圆角、字体尺寸迁到 ArkUI 组件默认属性。
  9. 把 style 派生类迁成 ArkUI 组件参数,比如 varianthighlightedcompact
  10. 把局部 theme overlay 改成页面级配置、组件参数或 WithTheme

迁完以后,重点看三类页面:启动页、普通内容页、带强调/选中/禁用状态的组件。很多问题不出在“深色资源没有命中”,而是出在某个 Android style 派生类原来覆写了文字色、背景或状态色,迁到 ArkUI 时被当成普通组件默认值漏掉了。

小结

Android 深浅色的灵活度来自一整套资源和样式系统:

能力 Android 里的位置 迁到 HarmonyOS 时怎么处理
深浅色资源选择 values / values-night resources/base / resources/dark
语义颜色 attr + theme item 设计 token / app.color.*
控件默认样式 style / TextAppearance / Widget.* ArkUI 自定义组件默认属性
样式继承与覆写 parent / 点号继承 组件参数、状态、少量工具函数
局部主题 android:theme / ThemeOverlay 页面级配置、组件参数、WithTheme
应用模式切换 DayNight / AppCompatDelegate / UiModeManager setColorMode / ThemeColorMode

这类迁移可以保留两句话:

第一,values-night 可以对应到 resources/dark,同名颜色资源这层可以迁。

第二,Android style/theme 继承链不能原样迁。它要被拆成 HarmonyOS 的资源 token、ArkUI 组件封装、组件参数和局部主题控制。

参考

// Kai@CodeHubble // 观测坐标:Android-HarmonyOS/2026-07-08

上一篇