HarmonyOS NEXT 沉浸光感主题自定义案例 — 完整实现指南
关键词:沉浸光感、CustomTheme、WithTheme、渐变背景、光晕效果、状态管理V2
效果

一、案例概述
本案例实现了一个沉浸式光感主题界面,用户可以切换「极光幻境」「暮光之城」「星海深邃」三种光感主题以及系统默认主题,同时支持浅色/深色模式切换。
1.1 效果特点
- 沉浸渐变背景:顶部大面积径向光晕 + 线性渐变,营造光感氛围
- 玻璃拟态卡片:半透明卡片 + 阴影发光,呈现磨砂玻璃质感
- 动态主题切换:切换主题时背景、卡片、文本颜色同步过渡
- 深浅色适配:每种主题都有独立的浅色/深色配色方案
1.2 技术要点
| 技术 | 使用方式 |
|---|---|
CustomTheme / CustomColors | 定义每套主题的 Token 色值 |
WithTheme | 控制页面局部主题作用域 |
onWillApplyTheme | 获取当前生效的主题颜色 |
colorModeIndex + isDarkMode | 手动管理浅色/深色/跟随系统状态 |
linearGradient | 实现渐变背景 |
Circle + blur | 实现光晕效果 |
animateTo | 主题切换过渡动画 |
二、项目结构
entry/src/main/
├── ets/
│ ├── entryability/
│ │ └── EntryAbility.ets # 入口 Ability,设置全屏
│ ├── pages/
│ │ └── LuminousPage.ets # 主页面(沉浸光感界面)
│ └── theme/
│ └── LuminousTheme.ets # 主题颜色定义与配置
├── resources/
│ ├── base/element/
│ │ ├── color.json # 浅色模式颜色资源
│ │ ├── string.json # 字符串资源
│ │ └── float.json # 尺寸资源
│ ├── dark/element/
│ │ └── color.json # 深色模式颜色资源
│ └── base/profile/
│ └── main_pages.json # 页面路由配置
└── module.json5 # 模块配置
三、实现步骤
步骤 1:定义主题颜色体系
创建 theme/LuminousTheme.ets,定义三套光感主题。每套主题包含浅色和深色两组颜色。
import { CustomColors, CustomTheme } from '@kit.ArkUI'
// ===== 极光幻境主题 =====
export class AuroraColors implements CustomColors {
brand: ResourceColor = '#FF6EC7F8' // 极光蓝
fontPrimary: ResourceColor = '#FF1A1A2E' // 深色文本
fontEmphasize: ResourceColor = '#FF6EC7F8' // 强调文本
fontOnPrimary: ResourceColor = '#FFFFFFFF' // 品牌色上的文本
backgroundPrimary: ResourceColor = '#FFF0F8FF' // 主背景
backgroundEmphasize: ResourceColor = '#FF6EC7F8' // 强调背景
compBackgroundPrimary: ResourceColor = '#FFE8F4FD'
compDivider: ResourceColor = '#33B0E0F8'
}
export class AuroraDarkColors implements CustomColors {
brand: ResourceColor = '#FF4DA8D8'
fontPrimary: ResourceColor = '#FFE8F4FD'
fontEmphasize: ResourceColor = '#FF8DD6F8'
fontOnPrimary: ResourceColor = '#FF1A1A2E'
backgroundPrimary: ResourceColor = '#FF0D1B2A'
compBackgroundPrimary: ResourceColor = '#FF1B2838'
compDivider: ResourceColor = '#334DA8D8'
}
export class AuroraTheme implements CustomTheme {
colors: AuroraColors = new AuroraColors()
darkColors: AuroraDarkColors = new AuroraDarkColors()
}
设计思路:
brand色是该主题的核心标识色,影响 Button、Slider 等系统组件fontPrimary和backgroundPrimary控制文本和背景的基调darkColors在深色模式下生效,提供适配的深色配色- 每种主题的
compDivider使用半透明品牌色,保持视觉一致性
步骤 2:定义主题配置管理
除了 CustomTheme 颜色 Token,还需要管理渐变、光晕等视觉参数:
export interface ThemeConfig {
name: string // 主题名称
theme: CustomTheme // CustomTheme 实例
gradientStart: string // 渐变起始色
gradientEnd: string // 渐变结束色
glowColor: string // 光晕颜色
cardGlow: string // 卡片发光颜色
darkGradientStart: string // 深色模式渐变起始色
darkGradientEnd: string // 深色模式渐变结束色
darkGlowColor: string // 深色模式光晕颜色
darkCardGlow: string // 深色模式卡片发光
}
export const THEMES: ThemeConfig[] = [
{
name: '极光幻境',
theme: new AuroraTheme(),
gradientStart: '#FF6EC7F8',
gradientEnd: '#FFB4E0F8',
glowColor: '#406EC7F8',
cardGlow: '#206EC7F8',
darkGradientStart: '#FF1B3A4B',
darkGradientEnd: '#FF0D1B2A',
darkGlowColor: '#404DA8D8',
darkCardGlow: '#204DA8D8'
},
// ... 暮光之城、星海深邃
]
关键点:ThemeConfig 将 CustomTheme(给系统组件用)和自定义视觉参数(给渐变、光晕用)统一管理,便于主题切换时一次性更新。
步骤 3:实现沉浸式渐变背景
使用 Stack + Circle + blur + linearGradient 组合实现沉浸光感背景:
Stack() {
// 光晕装饰层
Column() {
Stack() {
// 左上角光晕
Circle()
.width(300)
.height(300)
.fill(this.getGradientStart())
.blur(100) // 高斯模糊,营造光晕感
.opacity(0.3) // 低透明度,柔和不刺眼
.position({ x: -60, y: -80 })
// 右侧辅助光晕
Circle()
.width(200)
.height(200)
.fill(this.getGradientEnd())
.blur(80)
.opacity(0.25)
.position({ x: 200, y: -40 })
}
}
// 整体线性渐变
.linearGradient({
angle: 180,
colors: [
[this.getGradientStart(), 0], // 顶部:品牌色
[this.getGradientEnd(), 0.3], // 30%:渐变过渡色
[this.isDarkMode ? '#FF0A0A1E' : '#FFF8F8FF', 0.6] // 60%:纯色背景
]
})
// 主内容(Scroll 滚动区域)
Scroll() { ... }
}
光感原理:
- 大圆 + blur(100):产生柔和的弥散光晕效果
- 低 opacity(0.3):光晕不过于强烈,保持沉浸感
- linearGradient:从品牌色渐变到纯色,过渡自然
- 两个不同位置的光晕:产生层次感,模拟光线散射
步骤 4:实现玻璃拟态卡片
卡片使用半透明背景 + 发光阴影 + 细边框,实现毛玻璃质感:
@Builder
GlowCard(title: string, subtitle: string, icon: string) {
Column({ space: 8 }) {
Row({ space: 12 }) {
// 图标 + 光晕
Stack() {
Circle()
.width(48)
.height(48)
.fill(this.getGlowColor())
.blur(20) // 图标周围的光晕
Text(icon)
.fontSize(24)
}
Column() {
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.getTextColor())
Text(subtitle)
.fontSize(12)
.fontColor(this.getSubTextColor())
}
}
}
.padding(20)
.borderRadius(20)
.backgroundColor(this.getCardBg()) // 半透明背景
.border({ width: 1, color: this.getCardBorder() }) // 细边框
.shadow({ // 发光阴影
radius: 16,
color: this.getCardGlow(),
offsetX: 0,
offsetY: 4
})
}
玻璃拟态三要素:
| 属性 | 浅色值 | 深色值 | 作用 |
|---|---|---|---|
backgroundColor | #CCFFFFFF (80%白) | #22FFFFFF (13%白) | 半透明底色 |
border | #30000000 (19%黑) | #18FFFFFF (9%白) | 微妙边界 |
shadow | 品牌色 20% 透明 | 品牌色 20% 透明 | 卡片发光 |
步骤 5:实现 WithTheme 主题作用域
使用 WithTheme 包裹整个页面内容,实现主题的统一控制:
build() {
WithTheme({ theme: this.currentCustomTheme }) {
Stack() {
// 渐变背景 + 光晕
// 主内容区域
// 底部主题选择器
}
}
}
关键:WithTheme 的作用域内,所有使用 $r('sys.color.*') 的组件自动跟随主题变化。自定义颜色通过 ThemeConfig 中的参数传递。
注意:
WithTheme只传递theme参数,深浅色模式通过自定义的colorModeIndex状态 +isDarkMode手动管理,避免ThemeColorMode枚举在某些 SDK 版本中不可导出的兼容性问题。
步骤 6:实现深浅色模式切换
通过 colorModeIndex 数字状态控制模式(0=跟随系统、1=浅色、2=深色),结合 animateTo 实现平滑过渡:
// 0: 跟随系统, 1: 浅色, 2: 深色
@State colorModeIndex: number = 0
@State isDarkMode: boolean = false
// 深浅色模式更新逻辑
updateDarkMode(): void {
if (this.colorModeIndex === 2) {
this.isDarkMode = true
} else if (this.colorModeIndex === 1) {
this.isDarkMode = false
} else {
this.isDarkMode = this.systemColorMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
}
}
// 循环切换:跟随系统 → 浅色 → 深色 → 跟随系统
switchColorMode(): void {
this.colorModeIndex = (this.colorModeIndex + 1) % 3
this.updateDarkMode()
}
// 切换按钮
Button() {
Text(this.isDarkMode ? '☽' : '☀')
.fontSize(22)
}
.onClick(() => {
animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
this.switchColorMode()
})
})
模式切换逻辑:
- 跟随系统 → 浅色 → 深色 → 跟随系统(循环)
updateDarkMode()方法根据colorModeIndex和系统实际模式计算isDarkMode- 使用
@StorageProp('currentColorMode')监听系统深浅色变化
设计决策:不直接使用
ThemeColorMode枚举,而是在组件内部通过数字索引管理深浅色状态,自行控制所有颜色值切换。这种方式避免了 SDK 版本兼容性问题,同时拥有更灵活的颜色控制能力。
步骤 7:实现底部主题选择器
底部浮层式主题选择器,选中项带有发光指示效果:
@Builder
ThemeSelector() {
Column({ space: 16 }) {
Text('选择主题')
.fontSize(15)
.fontWeight(FontWeight.Medium)
Row({ space: 12 }) {
// 系统默认选项
Column({ space: 6 }) {
Stack() {
Circle()
.width(52)
.height(52)
.fill(SYSTEM_THEME_CONFIG.gradientStart)
.shadow({
radius: this.currentThemeIndex === -1 ? 12 : 0, // 选中时发光
color: this.currentThemeIndex === -1
? SYSTEM_THEME_CONFIG.glowColor : '#00000000'
})
Text('⚙').fontSize(22)
}
Text('系统').fontSize(11)
}
.onClick(() => {
animateTo({ duration: 350, curve: Curve.EaseInOut }, () => {
this.switchTheme(-1)
})
})
// 自定义主题选项
ForEach(THEMES, (config: ThemeConfig, index: number) => {
Column({ space: 6 }) {
Stack() {
Circle().width(52).height(52).fill(config.gradientStart)
.shadow({ radius: this.currentThemeIndex === index ? 12 : 0, ... })
// 内圆使用纯色 + 透明度,避免 GradientType 不可用问题
Circle().width(48).height(48)
.fill(config.gradientStart)
.opacity(0.6)
}
Text(config.name.substring(0, 2)).fontSize(11)
}
.onClick(() => { ... })
}, ...)
}
}
.padding(20)
.borderRadius(24)
.backgroundColor(this.getCardBg())
.shadow({ radius: 16, color: this.getCardGlow(), offsetX: 0, offsetY: -4 })
}
步骤 8:配置 EntryAbility 全屏模式
沉浸光感需要全屏显示,在 EntryAbility 中设置:
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'
onWindowStageCreate(windowStage: window.WindowStage): void {
let mainWin: window.Window = windowStage.getMainWindowSync()
mainWin.setWindowLayoutFullScreen(true).then(() => {
hilog.info(DOMAIN, 'testTag', 'Full-screen mode enabled.')
}).catch((err: BusinessError) => {
hilog.error(DOMAIN, 'testTag', 'Full-screen failed: %{public}s', JSON.stringify(err))
})
windowStage.loadContent('pages/LuminousPage', (err) => { ... })
}
步骤 9:配置资源文件
base/element/color.json(浅色模式):
{
"color": [
{ "name": "luminous_bg_light", "value": "#FFF8F8FF" },
{ "name": "luminous_card_light", "value": "#CCFFFFFF" },
{ "name": "luminous_text_primary", "value": "#FF1A1A2E" },
{ "name": "aurora_brand", "value": "#FF6EC7F8" },
{ "name": "twilight_brand", "value": "#FFE07050" },
{ "name": "ocean_brand", "value": "#FF4A7FD9" }
]
}
dark/element/color.json(深色模式):
{
"color": [
{ "name": "luminous_bg_light", "value": "#FF0A0A1E" },
{ "name": "luminous_card_light", "value": "#22FFFFFF" },
{ "name": "luminous_text_primary", "value": "#FFE8E8F0" },
{ "name": "aurora_brand", "value": "#FF4DA8D8" },
{ "name": "twilight_brand", "value": "#FFC85A3E" },
{ "name": "ocean_brand", "value": "#FF3A6BC0" }
]
}
main_pages.json:
{
"src": [
"pages/Index",
"pages/LuminousPage"
]
}
四、关键代码详解
4.1 主题切换核心逻辑
switchTheme(index: number): void {
if (index === -1) {
this.currentThemeIndex = -1
this.currentConfig = SYSTEM_THEME_CONFIG
this.currentCustomTheme = SYSTEM_THEME_CONFIG.theme
} else {
this.currentThemeIndex = index
this.currentConfig = THEMES[index]
this.currentCustomTheme = THEMES[index].theme
}
this.updateDarkMode()
}
解析:切换主题时同时更新三个状态变量:
currentThemeIndex:当前选中索引(-1 表示系统默认)currentConfig:当前ThemeConfig(包含渐变、光晕等视觉参数)currentCustomTheme:传递给WithTheme的CustomTheme实例
4.2 深浅色自适应颜色获取
getGradientStart(): string {
return this.isDarkMode ? this.currentConfig.darkGradientStart : this.currentConfig.gradientStart
}
getCardBg(): string {
return this.isDarkMode ? '#22FFFFFF' : '#CCFFFFFF'
}
getTextColor(): string {
return this.isDarkMode ? '#FFE8E8F0' : '#FF1A1A2E'
}
解析:每个颜色获取方法根据 isDarkMode 状态返回对应的浅色/深色值。这种方式比 $r() 资源引用更灵活,因为渐变、光晕等参数无法通过资源文件配置。
4.3 底部浮层渐变蒙版
// 底部主题选择器区域
Column() {
Blank().height(12)
this.ThemeSelector()
}
.position({ y: '100%' })
.translate({ y: -120 })
.linearGradient({
angle: 0,
colors: [
['#00000000', 0], // 顶部透明
[this.isDarkMode ? '#FF0A0A1E' : '#FFF8F8FF', 0.15] // 底部纯色
]
})
解析:底部浮层使用从透明到纯色的渐变蒙版,让选择器与页面内容自然融合,避免生硬的分界线。
4.4 onWillApplyTheme 主题感知
onWillApplyTheme(theme: Theme): void {
this.brandColor = theme.colors.brand
}
解析:当 WithTheme 的 theme 参数变化时,会触发此回调。通过 theme.colors.brand 获取当前主题的 brand 色,可用于需要手动设置颜色的自定义组件。
五、设计思路解析
5.1 光感设计的视觉层次
┌──────────────────────────┐
│ 光晕层(Circle + blur) │ ← 最底层:弥散光源
├──────────────────────────┤
│ 渐变层(linearGradient) │ ← 中间层:色彩过渡
├──────────────────────────┤
│ 内容层(Scroll + Cards) │ ← 最上层:交互内容
├──────────────────────────┤
│ 蒙版层(底部渐变) │ ← 浮层:主题选择器
└──────────────────────────┘
5.2 颜色设计原则
| 原则 | 实现 |
|---|---|
| 光感沉浸 | 顶部大面积渐变 + 光晕,让颜色从屏幕"溢出" |
| 通透轻盈 | 卡片半透明(80% 白 / 13% 白),透出背景渐变 |
| 层次分明 | 阴影使用品牌色,保持色彩统一的同时区分层级 |
| 深浅和谐 | 深色模式降低饱和度和亮度,但不改变色相 |
5.3 三种主题的色相定位
| 主题 | 色相 | 情感 | 品牌色 |
|---|---|---|---|
| 极光幻境 | 冷色系(蓝-青) | 科技、未来 | #6EC7F8 |
| 暮光之城 | 暖色系(橙-红) | 温暖、浪漫 | #E07050 |
| 星海深邃 | 中性色(蓝-紫) | 深邃、沉稳 | #4A7FD9 |
六、扩展建议
6.1 添加更多主题
只需在 LuminousTheme.ets 中添加新的颜色类和 ThemeConfig 即可:
export class ForestColors implements CustomColors {
brand: ResourceColor = '#FF66BB6A'
// ... 其他 Token
}
// 添加到 THEMES 数组
{
name: '翡翠森林',
theme: new ForestTheme(),
gradientStart: '#FF66BB6A',
gradientEnd: '#FFA5D6A7',
// ...
}
6.2 持久化主题偏好
使用 AppStorage 持久化用户选择的主题:
aboutToAppear(): void {
let savedIndex = AppStorage.get<number>('themeIndex')
if (savedIndex !== undefined) {
this.switchTheme(savedIndex)
}
}
switchTheme(index: number): void {
// ... 切换逻辑
AppStorage.setOrCreate('themeIndex', index)
}
6.3 主题切换动画增强
为卡片添加交错动画效果:
.onClick(() => {
animateTo({
duration: 500,
curve: Curve.FastOutSlowIn
}, () => {
this.switchTheme(index)
})
})
七、总结
本案例通过以下技术组合实现了沉浸光感主题界面:
CustomTheme+CustomColors:定义三套光感主题的 Token 色值WithTheme:控制页面主题作用域,驱动系统组件换肤colorModeIndex手动深浅色管理:避免ThemeColorMode枚举兼容性问题,实现浅色/深色/跟随系统三种模式linearGradient+Circle.blur:营造沉浸光感背景- 半透明卡片 +
shadow:实现玻璃拟态卡片效果 animateTo:主题切换时的平滑过渡动画onWillApplyTheme:感知主题变化获取当前颜色
这套方案可以作为应用主题系统的核心架构,通过扩展 ThemeConfig 支持更多视觉参数,构建完整的品牌主题体系。
参考文档
animateTo:主题切换时的平滑过渡动画onWillApplyTheme:感知主题变化获取当前颜色
这套方案可以作为应用主题系统的核心架构,通过扩展 ThemeConfig 支持更多视觉参数,构建完整的品牌主题体系。
参考文档

8658

被折叠的 条评论
为什么被折叠?



