OpenHarmony 项目统一全局样式、尺寸、色彩主题封装 ThemeUtil(API23)
摘要
项目中文字大小、颜色、圆角、间距、按钮尺寸、阴影等样式值分散写在各个页面,出现多处硬编码 #007DFF、fontSize (18)、padding (12),后期产品统一换肤、调整主色调需要全局逐行修改,维护成本极高。封装全局主题常量 ThemeUtil,统一管理主色 / 辅助色 / 文字色、间距、圆角、字体大小、通用阴影,支持一键切换深色 / 浅色两套主题,所有页面、组件统一引用主题变量,一键全局换色。API23 优化资源常量响应式同步、主题状态联动 AppStorage,修复深色模式切换页面局部样式不刷新、颜色硬编码无法批量替换、组件样式重复冗余等问题。
关键词
OpenHarmony;ArkTS;主题封装;全局色彩;尺寸常量;深色浅色切换;样式统一;Theme
一、引言
1.1 原生硬编码样式开发痛点
- 颜色、字号、间距分散在上千行页面代码,改版换色需全局检索替换,极易遗漏;
- 浅色、深色两套页面颜色手动分别写,切换时大量判断分支;
- 无统一规范:有的页面间距 10,有的 12,UI 视觉割裂不统一;
- 按钮、卡片、弹窗圆角大小各处不一致,缺乏设计规范约束;
- 无法统一管理阴影、边框、分割线等通用样式;
- 新增页面需要反复复制颜色值,开发效率低。
1.2 主题分层结构
- 色彩体系:主色、辅助色、成功 / 警告 / 错误色、一级 / 二级 / 占位文字、分割线、背景色(区分深浅两套)
- 间距体系:小间距、标准间距、大间距、超大边距
- 圆角体系:小圆角、标准卡片圆角、大弹窗圆角、全圆角按钮
- 字号体系:标题、正文、辅助小字、提示文字
- 通用样式:卡片阴影、输入框边框、分割线
API23 主题相关升级
- 支持主题常量配合 GlobalStore 全局深色状态自动动态取值;
- 样式常量可全局缓存,减少重复创建资源对象;
- 组件 @Builder 样式提取复用,减少重复布局代码;
- 修复深色模式切换后部分组件颜色不实时刷新问题。
二、全局主题常量 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 常量分层规范
- 尺寸 / 圆角 / 字号:写入 SizeConstant,深浅模式共用固定数值,统一设计规范;
- 色彩:分 LightColor / DarkColor 两套,通过 ThemeColor () 动态获取;
- 复合样式:卡片阴影、通用卡片修饰符封装成函数,多处复用。
6.2 页面使用规范
- 页面 build 顶部统一解构
const color = ThemeUtil.getColor(); const size = ThemeUtil.getSize(); - 禁止页面直接写 #xxxxxx、12、16 等硬编码数字与色值;
- 页面背景、卡片背景、文字颜色全部使用主题变量,深浅自动切换。
6.3 换肤维护规范
- 产品改版仅修改 theme.ets 内主色值,全局页面自动同步;
- 新增业务色(如会员紫、积分橙)统一在 Light/Dark 两套色彩同步新增;
- 统一设计尺寸修改仅调整 SizeConstant 一处,全部页面间距、字号同步变化。
6.4 深色模式联动规范
- ThemeColor () 内部读取 GlobalStore 深色全局状态,无需页面额外判断;
- 切换 Toggle 修改全局 darkMode 后,页面 @State 自动刷新,颜色实时切换;
- 弹窗、列表、按钮等公共组件全部接入主题,深浅无视觉断层。
6.5 组件复用规范
- 所有公共基础组件(StateView、RefreshListView、Dialog 弹窗)全部接入 ThemeUtil;
- 禁止组件内部写死颜色、尺寸,完全依赖主题入参。
七、高频问题解决方案
问题 1:切换深色模式页面颜色不刷新 解决:页面 UI 依赖 ThemeColor () 动态函数取值,搭配 GlobalStore 响应式状态,自动触发重绘。
问题 2:改版主色调需要修改上百处代码 解决:全部页面引用主题常量,仅修改 theme.ets 中 primary 字段全局生效。
问题 3:各页面卡片圆角、间距大小不统一 解决:强制所有页面使用 SizeConstant 内固定尺寸,禁止手写数字。
问题 4:深色模式分割线、背景发白刺眼 解决:分开维护 LightColor 与 DarkColor 两套独立背景 / 分割线色值,不共用颜色。
问题 5:按钮高度、输入框尺寸到处不一致 解决:统一使用 btnHeightMd、inputHeight 尺寸常量约束控件高度。
八、总结
ThemeUtil 全局主题体系分离固定尺寸常量与深浅两套动态色彩,统一管控项目全部视觉规范,彻底消除颜色、字号、间距硬编码,实现一键全局换肤、深浅模式自动适配。 所有页面、公共 UI 组件统一接入主题工具,和前文 GlobalStore 全局深色状态联动,一套规范贯穿整个 HAR/HSP 分层工程,极大降低 UI 改版、设计规范调整的维护成本,是标准化企业级鸿蒙项目必备视觉底层封装。
更多推荐

所有评论(0)