摘要

项目中文字大小、颜色、圆角、间距、按钮尺寸、阴影等样式值分散写在各个页面,出现多处硬编码 #007DFF、fontSize (18)、padding (12),后期产品统一换肤、调整主色调需要全局逐行修改,维护成本极高。封装全局主题常量 ThemeUtil,统一管理主色 / 辅助色 / 文字色、间距、圆角、字体大小、通用阴影,支持一键切换深色 / 浅色两套主题,所有页面、组件统一引用主题变量,一键全局换色。API23 优化资源常量响应式同步、主题状态联动 AppStorage,修复深色模式切换页面局部样式不刷新、颜色硬编码无法批量替换、组件样式重复冗余等问题。

关键词

OpenHarmony;ArkTS;主题封装;全局色彩;尺寸常量;深色浅色切换;样式统一;Theme

一、引言

1.1 原生硬编码样式开发痛点

  1. 颜色、字号、间距分散在上千行页面代码,改版换色需全局检索替换,极易遗漏;
  2. 浅色、深色两套页面颜色手动分别写,切换时大量判断分支;
  3. 无统一规范:有的页面间距 10,有的 12,UI 视觉割裂不统一;
  4. 按钮、卡片、弹窗圆角大小各处不一致,缺乏设计规范约束;
  5. 无法统一管理阴影、边框、分割线等通用样式;
  6. 新增页面需要反复复制颜色值,开发效率低。

1.2 主题分层结构

  1. 色彩体系:主色、辅助色、成功 / 警告 / 错误色、一级 / 二级 / 占位文字、分割线、背景色(区分深浅两套)
  2. 间距体系:小间距、标准间距、大间距、超大边距
  3. 圆角体系:小圆角、标准卡片圆角、大弹窗圆角、全圆角按钮
  4. 字号体系:标题、正文、辅助小字、提示文字
  5. 通用样式:卡片阴影、输入框边框、分割线

API23 主题相关升级

  1. 支持主题常量配合 GlobalStore 全局深色状态自动动态取值;
  2. 样式常量可全局缓存,减少重复创建资源对象;
  3. 组件 @Builder 样式提取复用,减少重复布局代码;
  4. 修复深色模式切换后部分组件颜色不实时刷新问题。

二、全局主题常量 model/theme.ets

ets

import GlobalStore from '../utils/global_store'
import { GLOBAL_KEY } from './GlobalState'

// 尺寸、圆角、间距 固定常量(深浅共用)
export const SizeConstant = {
  // 间距
  gapXs: 4,
  gapSm: 8,
  gapMd: 12,
  gapLg: 16,
  gapXl: 24,
  pagePadding: 12,
  // 圆角
  radiusXs: 4,
  radiusSm: 8,
  radiusMd: 12,
  radiusLg: 16,
  radiusFull: 999,
  // 字号
  fontTip: 12,
  fontAux: 14,
  fontMain: 16,
  fontSubTitle: 18,
  fontTitle: 22,
  fontBigTitle: 26,
  // 通用控件高度
  btnHeightSm: 36,
  btnHeightMd: 44,
  inputHeight: 48
}

// 浅色主题色彩
const LightColor = {
  primary: "#007DFF",
  success: "#00B42A",
  warning: "#FF7D00",
  danger: "#F53F3F",
  text1: "#1D2129",
  text2: "#4E5969",
  text3: "#86909C",
  textPlaceholder: "#C9CDD4",
  bgPage: "#F5F5F5",
  bgCard: "#FFFFFF",
  line: "#E5E6EB"
}

// 深色主题色彩
const DarkColor = {
  primary: "#36A3FF",
  success: "#37D05B",
  warning: "#FF9500",
  danger: "#FF5C5C",
  text1: "#F2F3F5",
  text2: "#C9CDD4",
  text3: "#86909C",
  textPlaceholder: "#4E5969",
  bgPage: "#121212",
  bgCard: "#1E1E1E",
  line: "#333333"
}

// 主题统一出口,自动判断深浅模式
export function ThemeColor() {
  const isDark = GlobalStore.get(GLOBAL_KEY.DARK_MODE)
  return isDark ? DarkColor : LightColor
}

// 通用卡片阴影样式
export function CardShadow() {
  const color = ThemeColor().line
  return {
    shadowColor: color,
    shadowBlur: 6,
    shadowOffsetX: 0,
    shadowOffsetY: 2
  }
}

三、主题工具辅助封装 utils/theme_util.ets

ets

import { SizeConstant, ThemeColor, CardShadow } from '../model/theme'

class ThemeUtil {
  private static instance: ThemeUtil
  static getInstance() {
    if (!ThemeUtil.instance) ThemeUtil.instance = new ThemeUtil()
    return ThemeUtil.instance
  }

  // 获取当前整套色彩
  getColor() {
    return ThemeColor()
  }

  // 获取尺寸常量
  getSize() {
    return SizeConstant
  }

  // 获取卡片阴影
  getCardShadow() {
    return CardShadow()
  }

  // 快速生成标准卡片通用修饰符
  getCardCommon() {
    const size = this.getSize()
    const color = this.getColor()
    return {
      borderRadius: size.radiusMd,
      backgroundColor: color.bgCard,
      padding: size.gapMd,
      shadow: this.getCardShadow()
    }
  }
}

export default ThemeUtil.getInstance()

四、页面实战全局主题调用示例

4.1 笔记列表卡片页面完整使用

ets

import ThemeUtil from '../utils/theme_util'
import RefreshListView from '../components/common/RefreshListView'
import StateView, { PageState } from '../components/common/StateView'
import { Note } from '../utils/rdb_util'

@Entry
@Component
struct NoteListPage {
  @State pageState: PageState = PageState.LOADING
  @State noteList: Note[] = []

  build() {
    const theme = ThemeUtil.getColor()
    const size = ThemeUtil.getSize()
    const cardStyle = ThemeUtil.getCardCommon()

    Column({ space: size.gapMd }) {
      Row() {
        Text("我的笔记")
          .fontSize(size.fontTitle)
          .fontColor(theme.text1)
          .layoutWeight(1)
        Button("新增笔记")
          .height(size.btnHeightMd)
          .borderRadius(size.radiusFull)
          .backgroundColor(theme.primary)
      }
      .width("95%")

      StateView({ state: this.pageState }) {
        RefreshListView({ list: this.noteList }) {
          (item: Note) => {
            Row()
              .width("100%")
              .padding(cardStyle.padding)
              .borderRadius(cardStyle.borderRadius)
              .backgroundColor(cardStyle.backgroundColor)
              .shadow(cardStyle.shadow)
            {
              Column({ space: size.gapXs }).layoutWeight(1) {
                Text(item.title)
                  .fontSize(size.fontMain)
                  .fontColor(theme.text1)
                Text(item.content)
                  .fontSize(size.fontAux)
                  .fontColor(theme.text3)
              }
              Button("删除")
                .height(size.btnHeightSm)
                .backgroundColor(theme.danger)
                .borderRadius(size.radiusSm)
            }
          }
        }
      }
    }
    .width("100%")
    .height("100%")
    .padding(size.pagePadding)
    .backgroundColor(theme.bgPage)
  }
}

4.2 设置页深色模式切换实时变色

ets

import GlobalStore from '../utils/global_store'
import ThemeUtil from '../utils/theme_util'

@Entry
@Component
struct SettingPage {
  @State dark: boolean = false

  aboutToAppear() {
    this.dark = GlobalStore.get(GLOBAL_KEY.DARK_MODE)
  }

  build() {
    const color = ThemeUtil.getColor()
    const size = ThemeUtil.getSize()
    Column()
      .width("100%")
      .height("100%")
      .padding(size.pagePadding)
      .backgroundColor(color.bgPage)
    {
      Row()
        .width("100%")
        .padding(size.gapLg)
        .borderRadius(size.radiusMd)
        .backgroundColor(color.bgCard)
      {
        Text("深色模式")
          .fontSize(size.fontMain)
          .fontColor(color.text1)
          .layoutWeight(1)
        Toggle({ isOn: this.dark })
          .onChange((v) => GlobalStore.toggleDarkMode(v))
      }
    }
  }
}

五、通用业务组件规范改造(StateView 示例)

ets

@Component
export struct StateView {
  @Param state: PageState = PageState.LOADING
  @BuilderParam contentBuilder: () => void
  @Param onRetry: () => void
  @State rotateAngle: number = 0

  build() {
    const color = ThemeUtil.getColor()
    const size = ThemeUtil.getSize()
    Column() {
      if (this.state === PageState.LOADING) {
        Column({ space: size.gapMd }) {
          Text("⟳").fontSize(42).rotate({ angle: this.rotateAngle })
          Text("加载中...")
            .fontSize(size.fontAux)
            .fontColor(color.text3)
        }
      } else if (this.state === PageState.EMPTY) {
        Column({ space: size.gapMd }) {
          Image($r("sys.media.ohos_ic_public_empty")).width(80).fillColor(color.text3)
          Text("暂无数据")
            .fontSize(size.fontAux)
            .fontColor(color.text3)
        }
      } else if (this.state === PageState.ERROR) {
        Column({ space: size.gapLg }) {
          Image($r("sys.media.ohos_ic_public_fail")).width(80).fillColor(color.text3)
          Text("加载失败,请点击重试")
            .fontSize(size.fontAux)
            .fontColor(color.text3)
          Button("重试")
            .width(140)
            .height(size.btnHeightMd)
            .backgroundColor(color.primary)
            .borderRadius(size.radiusSm)
            .onClick(this.onRetry)
        }
      } else {
        this.contentBuilder()
      }
    }
    .width("100%")
    .layoutWeight(1)
  }
}

六、主题开发编码规范

6.1 常量分层规范

  1. 尺寸 / 圆角 / 字号:写入 SizeConstant,深浅模式共用固定数值,统一设计规范;
  2. 色彩:分 LightColor / DarkColor 两套,通过 ThemeColor () 动态获取;
  3. 复合样式:卡片阴影、通用卡片修饰符封装成函数,多处复用。

6.2 页面使用规范

  1. 页面 build 顶部统一解构 const color = ThemeUtil.getColor(); const size = ThemeUtil.getSize()
  2. 禁止页面直接写 #xxxxxx、12、16 等硬编码数字与色值;
  3. 页面背景、卡片背景、文字颜色全部使用主题变量,深浅自动切换。

6.3 换肤维护规范

  1. 产品改版仅修改 theme.ets 内主色值,全局页面自动同步;
  2. 新增业务色(如会员紫、积分橙)统一在 Light/Dark 两套色彩同步新增;
  3. 统一设计尺寸修改仅调整 SizeConstant 一处,全部页面间距、字号同步变化。

6.4 深色模式联动规范

  1. ThemeColor () 内部读取 GlobalStore 深色全局状态,无需页面额外判断;
  2. 切换 Toggle 修改全局 darkMode 后,页面 @State 自动刷新,颜色实时切换;
  3. 弹窗、列表、按钮等公共组件全部接入主题,深浅无视觉断层。

6.5 组件复用规范

  1. 所有公共基础组件(StateView、RefreshListView、Dialog 弹窗)全部接入 ThemeUtil;
  2. 禁止组件内部写死颜色、尺寸,完全依赖主题入参。

七、高频问题解决方案

问题 1:切换深色模式页面颜色不刷新 解决:页面 UI 依赖 ThemeColor () 动态函数取值,搭配 GlobalStore 响应式状态,自动触发重绘。

问题 2:改版主色调需要修改上百处代码 解决:全部页面引用主题常量,仅修改 theme.ets 中 primary 字段全局生效。

问题 3:各页面卡片圆角、间距大小不统一 解决:强制所有页面使用 SizeConstant 内固定尺寸,禁止手写数字。

问题 4:深色模式分割线、背景发白刺眼 解决:分开维护 LightColor 与 DarkColor 两套独立背景 / 分割线色值,不共用颜色。

问题 5:按钮高度、输入框尺寸到处不一致 解决:统一使用 btnHeightMd、inputHeight 尺寸常量约束控件高度。

八、总结

ThemeUtil 全局主题体系分离固定尺寸常量与深浅两套动态色彩,统一管控项目全部视觉规范,彻底消除颜色、字号、间距硬编码,实现一键全局换肤、深浅模式自动适配。 所有页面、公共 UI 组件统一接入主题工具,和前文 GlobalStore 全局深色状态联动,一套规范贯穿整个 HAR/HSP 分层工程,极大降低 UI 改版、设计规范调整的维护成本,是标准化企业级鸿蒙项目必备视觉底层封装。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐