《主题颜色自定义》三、沉浸光感主题自定义案例指南

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 等系统组件
  • fontPrimarybackgroundPrimary 控制文本和背景的基调
  • 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'
  },
  // ... 暮光之城、星海深邃
]

关键点ThemeConfigCustomTheme(给系统组件用)和自定义视觉参数(给渐变、光晕用)统一管理,便于主题切换时一次性更新。

步骤 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() { ... }
}

光感原理

  1. 大圆 + blur(100):产生柔和的弥散光晕效果
  2. 低 opacity(0.3):光晕不过于强烈,保持沉浸感
  3. linearGradient:从品牌色渐变到纯色,过渡自然
  4. 两个不同位置的光晕:产生层次感,模拟光线散射

步骤 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()
}

解析:切换主题时同时更新三个状态变量:

  1. currentThemeIndex:当前选中索引(-1 表示系统默认)
  2. currentConfig:当前 ThemeConfig(包含渐变、光晕等视觉参数)
  3. currentCustomTheme:传递给 WithThemeCustomTheme 实例

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
}

解析:当 WithThemetheme 参数变化时,会触发此回调。通过 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)
  })
})

七、总结

本案例通过以下技术组合实现了沉浸光感主题界面:

  1. CustomTheme + CustomColors:定义三套光感主题的 Token 色值
  2. WithTheme:控制页面主题作用域,驱动系统组件换肤
  3. colorModeIndex 手动深浅色管理:避免 ThemeColorMode 枚举兼容性问题,实现浅色/深色/跟随系统三种模式
  4. linearGradient + Circle.blur:营造沉浸光感背景
  5. 半透明卡片 + shadow:实现玻璃拟态卡片效果
  6. animateTo:主题切换时的平滑过渡动画
  7. onWillApplyTheme:感知主题变化获取当前颜色

这套方案可以作为应用主题系统的核心架构,通过扩展 ThemeConfig 支持更多视觉参数,构建完整的品牌主题体系。


参考文档

  1. animateTo:主题切换时的平滑过渡动画
  2. onWillApplyTheme:感知主题变化获取当前颜色

这套方案可以作为应用主题系统的核心架构,通过扩展 ThemeConfig 支持更多视觉参数,构建完整的品牌主题体系。


参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值