文章目录
-
- 每日一句正能量
- 前言
- 一、前言:心理疗愈工具的UI革新需求
- 二、核心特性解析与技术选型
-
- [2.1 沉浸光感在心理疗愈中的价值](#2.1 沉浸光感在心理疗愈中的价值)
- [2.2 Face AR在心理疗愈中的创新应用](#2.2 Face AR在心理疗愈中的创新应用)
- 三、项目实战:"心流空间"架构设计
-
- [3.1 应用场景与功能规划](#3.1 应用场景与功能规划)
- [3.2 技术架构图](#3.2 技术架构图)
- 四、环境配置与模块依赖
-
- [4.1 模块依赖配置](#4.1 模块依赖配置)
- [4.2 权限声明(module.json5)](#4.2 权限声明(module.json5))
- 五、核心组件实战
-
- [5.1 窗口沉浸配置(WellnessAbility.ets)](#5.1 窗口沉浸配置(WellnessAbility.ets))
- [5.2 沉浸光感标题栏(ImmersiveTitleBar.ets)](#5.2 沉浸光感标题栏(ImmersiveTitleBar.ets))
- [5.3 Face AR情绪感知与环境响应组件(EmotionResponsiveEnvironment.ets)](#5.3 Face AR情绪感知与环境响应组件(EmotionResponsiveEnvironment.ets))
- [5.4 动态冥想粒子场景(MeditationScene.ets)](#5.4 动态冥想粒子场景(MeditationScene.ets))
- [5.5 悬浮场景导航页签(FloatTabNavigation.ets)](#5.5 悬浮场景导航页签(FloatTabNavigation.ets))
- [5.6 多窗口光效同步管理器(WindowManager.ets)](#5.6 多窗口光效同步管理器(WindowManager.ets))
- [5.7 浮动呼吸引导窗口(BreathWindow.ets)](#5.7 浮动呼吸引导窗口(BreathWindow.ets))
- [5.8 主页面集成(WellnessPage.ets)](#5.8 主页面集成(WellnessPage.ets))
- 六、关键技术总结
-
- [6.1 沉浸光感实现清单](#6.1 沉浸光感实现清单)
- [6.2 Face AR情绪感知实现要点](#6.2 Face AR情绪感知实现要点)
- [6.3 PC端多窗口光效协同](#6.3 PC端多窗口光效协同)
- 七、调试与性能优化
-
- [7.1 真机调试建议](#7.1 真机调试建议)
- [7.2 性能优化策略](#7.2 性能优化策略)
- 八、总结与展望

每日一句正能量
人生如一场修行,得意时要看淡,失意时要看开。」
不因顺境而膨胀,避免乐极生悲。不因逆境而沉沦,相信低谷会过去。
每一个闪闪发光的人,都在背后熬过一个又一个不为人知的黑夜,那才是真正值得我们拥有和赞叹的地方。早安!
前言
摘要 :HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Face AR特性,为心理健康和正念冥想领域提供了全新的交互范式。本文将实战开发一款面向HarmonyOS PC的"心流空间"应用,展示如何利用
systemMaterialEffect打造沉浸式疗愈环境,通过悬浮导航实现多场景快速切换,基于Face AR实现实时情绪识别与响应式环境调节,以及基于多窗口架构构建浮动呼吸引导、情绪日记和白噪音控制窗口的疗愈协作体验。
一、前言:心理疗愈工具的UI革新需求
传统的心理健康应用往往采用静态的界面和固定的功能模块,缺乏与使用者情绪状态的实时互动。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation) 、沉浸光感(Immersive Light Effects)与Face AR特性,为心理疗愈带来了"感知、响应、治愈"的设计可能 。
本文核心亮点:
- 情绪响应光效:根据Face AR识别的实时情绪状态(平静/焦虑/愉悦/疲惫)动态切换环境光色与呼吸节奏
- 悬浮场景导航:底部悬浮页签替代传统菜单,支持拖拽排序与透明度调节
- Face AR情绪感知:实时捕捉微表情变化,自动调节环境色温、音乐节奏和引导语速
- 多窗口疗愈协作:主冥想窗口 + 浮动呼吸引导 + 情绪日记 + 白噪音控制窗口的光效联动
二、核心特性解析与技术选型
2.1 沉浸光感在心理疗愈中的价值
HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为标题栏和导航组件带来细腻的光晕与反射效果 。在心理疗愈场景中,这种材质效果能够:
- 营造安全感:玻璃拟态的半透明层配合柔和光效,模拟心理咨询室的温暖氛围
- 情绪色彩疗法:动态环境光随情绪状态变化(焦虑时冷蓝安抚、疲惫时暖黄提振、平静时柔绿维持)
- 降低认知负荷:通过光效强弱区分窗口焦点状态,多窗口协作时视觉层级柔和不刺眼
2.2 Face AR在心理疗愈中的创新应用
HarmonyOS 6的Face AR能力支持实时精确捕捉人脸表情变化 ,在心理疗愈中可以:
- 情绪状态识别:识别平静、焦虑、愉悦、疲惫、悲伤等基础情绪状态
- 微表情预警:捕捉眉头紧锁、嘴角下垂等微表情,及时触发安抚干预
- 疗效反馈:通过表情变化曲线评估疗愈效果,为后续方案调整提供数据
三、项目实战:"心流空间"架构设计
3.1 应用场景与功能规划
面向HarmonyOS PC的心理疗愈场景,核心功能包括:
| 功能模块 | 技术实现 | 沉浸光感/Face AR应用 |
|---|---|---|
| 主冥想窗口 | Canvas + 动态粒子系统 |
背景光效随情绪状态变化 |
| 悬浮场景导航 | HdsTabs + systemMaterialEffect |
玻璃拟态页签,选中光晕反馈 |
| Face AR情绪感知 | AR Engine + 表情系数 | 实时情绪识别与环境响应 |
| 浮动呼吸引导窗口 | 子窗口 + 动画圆环 | 呼吸节奏光效同步 |
| 情绪日记窗口 | 子窗口 + TextArea |
情绪色标签 |
| 白噪音控制窗口 | 子窗口 + Slider |
音量光效可视化 |
3.2 技术架构图
┌─────────────────────────────────────────────────────────────┐
│ UI Layer (ArkUI) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ ImmersiveTitle│ │ Meditation │ │ FloatTabBar │ │
│ │ Bar (HDS) │ │ View (Canvas)│ │ (HdsTabs) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ AR Engine Layer (Face AR) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Emotion │ │ Micro- │ │ Response │ │
│ │ Detection │ │ Expression │ │ Trigger │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────────┐
│ Window Manager (PC Multi-Window) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Main Window │ │ Breath Win │ │ Diary Win │ │
│ │ (FullScreen) │ │ (Floating) │ │ (Floating) │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌──────────────┐ │
│ │ Ambient Win │ │
│ │ (Floating) │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
四、环境配置与模块依赖
4.1 模块依赖配置
json
{
"name": "flow-space",
"version": "1.0.0",
"description": "Immersive Mental Wellness Platform for HarmonyOS PC",
"dependencies": {
"@kit.AbilityKit": "^6.1.0",
"@kit.ArkUI": "^6.1.0",
"@kit.UIDesignKit": "^6.1.0",
"@kit.BasicServicesKit": "^6.1.0",
"@kit.AREngineKit": "^6.1.0",
"@kit.MultimediaKit": "^6.1.0",
"@kit.GraphicsKit": "^6.1.0"
}
}
4.2 权限声明(module.json5)
json
{
"module": {
"name": "entry",
"type": "entry",
"mainElement": "WellnessAbility",
"deviceTypes": [
"2in1",
"tablet",
"default"
],
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:permission_camera_reason"
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:permission_mic_reason"
},
{
"name": "ohos.permission.INTERNET",
"reason": "$string:permission_internet_reason"
}
]
}
}
五、核心组件实战
5.1 窗口沉浸配置(WellnessAbility.ets)
typescript
// entry/src/main/ets/ability/WellnessAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export default class WellnessAbility extends UIAbility {
private mainWindow: window.Window | null = null;
onWindowStageCreate(windowStage: window.WindowStage): void {
this.initializeWellnessWindow(windowStage);
}
private async initializeWellnessWindow(windowStage: window.WindowStage): Promise<void> {
try {
this.mainWindow = windowStage.getMainWindowSync();
await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
await this.mainWindow.setWindowTitleBarEnable(false);
await this.mainWindow.setWindowLayoutFullScreen(true);
await this.mainWindow.setWindowShadowEnabled(true);
await this.mainWindow.setWindowCornerRadius(12);
await this.mainWindow.setWindowBackgroundColor('#00000000');
AppStorage.setOrCreate('main_window', this.mainWindow);
windowStage.loadContent('pages/WellnessPage', (err) => {
if (err.code) {
console.error('Failed to load wellness content:', JSON.stringify(err));
return;
}
console.info('Flow Space main window initialized');
});
} catch (error) {
console.error('Window initialization failed:', (error as BusinessError).message);
}
}
onWindowStageDestroy(): void {
this.mainWindow = null;
}
}
5.2 沉浸光感标题栏(ImmersiveTitleBar.ets)
typescript
// entry/src/main/ets/components/ImmersiveTitleBar.ets
import { HdsNavigation, SystemMaterialEffect } from '@kit.UIDesignKit';
export enum EmotionState {
CALM = 'calm', // 平静-柔绿
ANXIOUS = 'anxious', // 焦虑-冷蓝
JOYFUL = 'joyful', // 愉悦-暖金
TIRED = 'tired', // 疲惫-暖黄
SAD = 'sad' // 悲伤-淡紫
}
export enum SessionType {
MEDITATION = 'meditation', // 正念冥想
BREATHING = 'breathing', // 呼吸练习
FOCUS = 'focus', // 专注训练
SLEEP = 'sleep' // 睡眠引导
}
@Component
export struct ImmersiveTitleBar {
@Prop currentSession: string = '晨间冥想';
@Prop sessionType: SessionType = SessionType.MEDITATION;
@Prop emotionState: EmotionState = EmotionState.CALM;
@State isWindowFocused: boolean = true;
@State titleBarHeight: number = 48;
// 情绪状态主题色映射
private emotionColors: Map<EmotionState, string> = new Map([
[EmotionState.CALM, '#7ED321'], // 柔绿-平静
[EmotionState.ANXIOUS, '#50E3C2'], // 冷蓝-焦虑
[EmotionState.JOYFUL, '#F5A623'], // 暖金-愉悦
[EmotionState.TIRED, '#F8E71C'], // 暖黄-疲惫
[EmotionState.SAD, '#BD10E0'] // 淡紫-悲伤
]);
// 疗愈类型图标色
private sessionColors: Map<SessionType, string> = new Map([
[SessionType.MEDITATION, '#7ED321'],
[SessionType.BREATHING, '#50E3C2'],
[SessionType.FOCUS, '#F5A623'],
[SessionType.SLEEP, '#9013FE']
]);
aboutToAppear(): void {
AppStorage.watch('window_focused', (focused: boolean) => {
this.isWindowFocused = focused;
});
}
private getThemeColor(): string {
return this.emotionColors.get(this.emotionState) || '#7ED321';
}
private getSessionText(): string {
const texts: Map<SessionType, string> = new Map([
[SessionType.MEDITATION, '正念冥想'],
[SessionType.BREATHING, '呼吸练习'],
[SessionType.FOCUS, '专注训练'],
[SessionType.SLEEP, '睡眠引导']
]);
return texts.get(this.sessionType) || '正念冥想';
}
build() {
HdsNavigation({
title: `心流空间 - ${this.currentSession}`,
subtitle: `${this.getSessionText()} · ${this.getEmotionText(this.emotionState)}`,
systemMaterialEffect: SystemMaterialEffect.IMMERSIVE,
backgroundOpacity: this.isWindowFocused ? 0.85 : 0.55,
height: this.titleBarHeight,
leading: this.buildLeadingActions(),
trailing: this.buildTrailingActions()
})
.width('100%')
.border({
width: { bottom: 1 },
color: this.isWindowFocused
? this.getThemeColor()
: 'rgba(255,255,255,0.1)'
})
.shadow({
radius: this.isWindowFocused ? 15 : 5,
color: this.getThemeColor(),
offsetX: 0,
offsetY: 2
})
.animation({
duration: 300,
curve: Curve.EaseInOut
})
}
@Builder
buildLeadingActions(): void {
Row({ space: 12 }) {
// 情绪状态指示灯(柔和呼吸效果)
Stack() {
Circle()
.width(20)
.height(20)
.fill(this.getThemeColor())
.opacity(0.2)
.animation({
duration: 3000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
.scale({ x: 1.8, y: 1.8 })
Circle()
.width(14)
.height(14)
.fill(this.getThemeColor())
}
.width(24)
.height(24)
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_play_pause'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('session_action', 'toggle_play');
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_timer'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('session_action', 'set_timer');
})
}
.padding({ left: 16 })
}
@Builder
buildTrailingActions(): void {
Row({ space: 12 }) {
// Face AR状态
Circle()
.width(8)
.height(8)
.fill(AppStorage.get<boolean>('face_ar_active') ? '#27C93F' : '#95A5A6')
.shadow({
radius: 8,
color: AppStorage.get<boolean>('face_ar_active') ? '#27C93F' : 'transparent'
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_breath'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('window_action', 'open_breath');
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_diary'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('window_action', 'open_diary');
})
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_ambient'))
.width(18)
.height(18)
.fillColor('#FFFFFF')
}
.width(32)
.height(32)
.backgroundColor('rgba(255,255,255,0.1)')
.onClick(() => {
AppStorage.setOrCreate('window_action', 'open_ambient');
})
}
.padding({ right: 16 })
}
private getEmotionText(emotion: EmotionState): string {
const texts: Map<EmotionState, string> = new Map([
[EmotionState.CALM, '平静'],
[EmotionState.ANXIOUS, '焦虑'],
[EmotionState.JOYFUL, '愉悦'],
[EmotionState.TIRED, '疲惫'],
[EmotionState.SAD, '低落']
]);
return texts.get(emotion) || '平静';
}
}
5.3 Face AR情绪感知与环境响应组件(EmotionResponsiveEnvironment.ets)
核心创新组件,基于AR Engine实现情绪识别并驱动环境参数实时调节 。
typescript
// entry/src/main/ets/components/EmotionResponsiveEnvironment.ets
import { arEngine } from '@kit.AREngineKit';
import { camera } from '@kit.CameraKit';
export interface EnvironmentParameters {
colorTemperature: number; // 色温 2700K-6500K
brightness: number; // 亮度 0.1-1.0
pulseSpeed: number; // 呼吸节奏 0.5-3.0秒
particleDensity: number; // 粒子密度 0-1
ambientSound: string; // 环境音类型
}
@Component
export struct EmotionResponsiveEnvironment {
@State isARActive: boolean = false;
@State currentEmotion: string = 'calm';
@State emotionConfidence: number = 0;
@State envParams: EnvironmentParameters = {
colorTemperature: 4000,
brightness: 0.6,
pulseSpeed: 2.0,
particleDensity: 0.5,
ambientSound: 'forest'
};
@State trackingStatus: string = '未启动';
private arSession: arEngine.ARSession | null = null;
private faceTracker: arEngine.FaceTracker | null = null;
private envCallback: ((params: EnvironmentParameters) => void) | null = null;
aboutToAppear(): void {
this.initializeAR();
}
aboutToDisappear(): void {
this.releaseAR();
}
setEnvironmentCallback(callback: (params: EnvironmentParameters) => void): void {
this.envCallback = callback;
}
private async initializeAR(): Promise<void> {
try {
this.arSession = arEngine.createARSession({
mode: arEngine.ARMode.FACE,
cameraConfig: {
cameraFacing: camera.CameraFacing.CAMERA_FACING_FRONT
}
});
this.faceTracker = this.arSession.createFaceTracker({
maxFaceCount: 1,
enableExpression: true,
enablePose: true
});
await this.arSession.start();
this.isARActive = true;
this.trackingStatus = '情绪感知中';
AppStorage.setOrCreate('face_ar_active', true);
this.startEmotionTracking();
console.info('Emotion Responsive Environment initialized');
} catch (error) {
console.error('Failed to initialize Face AR:', error);
this.trackingStatus = '初始化失败';
}
}
private startEmotionTracking(): void {
if (!this.arSession || !this.faceTracker) return;
this.arSession.on('frame', (frame: arEngine.ARFrame) => {
const faces = this.faceTracker?.track(frame);
if (faces && faces.length > 0) {
const face = faces[0];
this.analyzeEmotionAndRespond(face);
}
});
}
private analyzeEmotionAndRespond(face: arEngine.Face): void {
const expressions = face.getExpressions();
const pose = face.getPose();
// 提取表情特征
const smile = expressions.get(arEngine.FaceExpressionType.SMILE) || 0;
const frown = expressions.get(arEngine.FaceExpressionType.FROWN) || 0;
const browRaise = expressions.get(arEngine.FaceExpressionType.BROW_RAISE_LEFT) || 0;
const eyeWide = expressions.get(arEngine.FaceExpressionType.EYE_WIDE_LEFT) || 0;
const mouthOpen = expressions.get(arEngine.FaceExpressionType.MOUTH_OPEN) || 0;
// 情绪状态判断
let emotion = 'calm';
let confidence = 0;
if (frown > 0.4 && browRaise > 0.3) {
emotion = 'anxious';
confidence = (frown + browRaise) / 2;
} else if (smile > 0.6 && eyeWide > 0.4) {
emotion = 'joyful';
confidence = (smile + eyeWide) / 2;
} else if (frown > 0.5 && mouthOpen < 0.2) {
emotion = 'sad';
confidence = frown;
} else if (eyeWide < 0.3 && mouthOpen < 0.2 && smile < 0.3) {
emotion = 'tired';
confidence = 1 - (eyeWide + mouthOpen + smile);
} else {
emotion = 'calm';
confidence = 0.5 + (1 - frown - browRaise) / 2;
}
this.currentEmotion = emotion;
this.emotionConfidence = confidence;
// 生成环境参数
const newParams = this.generateEnvironmentParams(emotion, confidence);
this.envParams = newParams;
if (this.envCallback) {
this.envCallback(newParams);
}
AppStorage.setOrCreate('emotion_state', emotion);
AppStorage.setOrCreate('environment_params', newParams);
}
private generateEnvironmentParams(emotion: string, confidence: number): EnvironmentParameters {
const params: Map<string, EnvironmentParameters> = new Map([
['calm', {
colorTemperature: 4000,
brightness: 0.6,
pulseSpeed: 2.0,
particleDensity: 0.5,
ambientSound: 'forest'
}],
['anxious', {
colorTemperature: 6500,
brightness: 0.4,
pulseSpeed: 1.0,
particleDensity: 0.2,
ambientSound: 'rain'
}],
['joyful', {
colorTemperature: 3500,
brightness: 0.8,
pulseSpeed: 1.5,
particleDensity: 0.8,
ambientSound: 'stream'
}],
['tired', {
colorTemperature: 2700,
brightness: 0.3,
pulseSpeed: 3.0,
particleDensity: 0.3,
ambientSound: 'ocean'
}],
['sad', {
colorTemperature: 3000,
brightness: 0.35,
pulseSpeed: 2.5,
particleDensity: 0.4,
ambientSound: 'wind'
}]
]);
return params.get(emotion) || params.get('calm')!;
}
private releaseAR(): void {
if (this.arSession) {
this.arSession.stop();
this.arSession = null;
}
this.faceTracker = null;
this.isARActive = false;
AppStorage.setOrCreate('face_ar_active', false);
this.trackingStatus = '已停止';
}
build() {
Stack() {
// AR预览层
Column() {
if (this.isARActive) {
XComponent({
id: 'emotion_ar_preview',
type: XComponentType.SURFACE,
controller: new XComponentController()
})
.width('100%')
.height('100%')
.opacity(0.15)
}
}
.width('100%')
.height('100%')
// 情绪状态可视化
Column() {
this.buildEmotionStatusPanel()
}
.width('100%')
.height('100%')
// AR状态指示器
this.buildARStatusIndicator()
}
.width('100%')
.height('100%')
}
@Builder
buildEmotionStatusPanel(): void {
Column({ space: 10 }) {
// 当前情绪
Text(this.getEmotionLabel(this.currentEmotion))
.fontSize(20)
.fontColor(this.getEmotionColor(this.currentEmotion))
.fontWeight(FontWeight.Bold)
// 置信度
Progress({
value: this.emotionConfidence * 100,
total: 100,
type: ProgressType.Ring
})
.width(60)
.height(60)
.color(this.getEmotionColor(this.currentEmotion))
// 环境参数
Column({ space: 4 }) {
Text(`色温: ${this.envParams.colorTemperature}K`)
.fontSize(10)
.fontColor('#AAAAAA')
Text(`亮度: ${Math.round(this.envParams.brightness * 100)}%`)
.fontSize(10)
.fontColor('#AAAAAA')
Text(`呼吸: ${this.envParams.pulseSpeed.toFixed(1)}s`)
.fontSize(10)
.fontColor('#AAAAAA')
Text(`环境音: ${this.getSoundLabel(this.envParams.ambientSound)}`)
.fontSize(10)
.fontColor('#AAAAAA')
}
}
.width(160)
.height('auto')
.padding(16)
.backgroundColor('rgba(0,0,0,0.6)')
.borderRadius(16)
.position({ x: '85%', y: '20%' })
.anchor('100% 0%')
.backdropFilter($r('sys.blur.10'))
}
@Builder
buildARStatusIndicator(): void {
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill(this.isARActive ? '#27C93F' : '#95A5A6')
.shadow({
radius: 6,
color: this.isARActive ? '#27C93F' : 'transparent'
})
Text(`情绪感知 · ${this.trackingStatus}`)
.fontSize(11)
.fontColor(this.isARActive ? '#27C93F' : '#95A5A6')
}
.width('auto')
.height(28)
.padding({ left: 10, right: 10 })
.backgroundColor('rgba(0,0,0,0.6)')
.borderRadius(14)
.position({ x: 16, y: 16 })
.backdropFilter($r('sys.blur.10'))
}
private getEmotionLabel(emotion: string): string {
const labels: Map<string, string> = new Map([
['calm', '平静'],
['anxious', '焦虑'],
['joyful', '愉悦'],
['tired', '疲惫'],
['sad', '低落']
]);
return labels.get(emotion) || '平静';
}
private getEmotionColor(emotion: string): string {
const colors: Map<string, string> = new Map([
['calm', '#7ED321'],
['anxious', '#50E3C2'],
['joyful', '#F5A623'],
['tired', '#F8E71C'],
['sad', '#BD10E0']
]);
return colors.get(emotion) || '#7ED321';
}
private getSoundLabel(sound: string): string {
const labels: Map<string, string> = new Map([
['forest', '森林'],
['rain', '雨声'],
['stream', '溪流'],
['ocean', '海浪'],
['wind', '风声']
]);
return labels.get(sound) || '森林';
}
}
5.4 动态冥想粒子场景(MeditationScene.ets)
typescript
// entry/src/main/ets/components/MeditationScene.ets
import { EnvironmentParameters } from './EmotionResponsiveEnvironment';
interface Particle {
x: number;
y: number;
size: number;
speed: number;
opacity: number;
color: string;
}
@Component
export struct MeditationScene {
@Prop envParams: EnvironmentParameters = {
colorTemperature: 4000,
brightness: 0.6,
pulseSpeed: 2.0,
particleDensity: 0.5,
ambientSound: 'forest'
};
@State particles: Particle[] = [];
@State pulsePhase: number = 0;
@State isPlaying: boolean = false;
private animationTimer: number | null = null;
aboutToAppear(): void {
this.generateParticles();
this.startAnimation();
}
aboutToDisappear(): void {
if (this.animationTimer) {
clearInterval(this.animationTimer);
}
}
private generateParticles(): void {
const count = Math.floor(this.envParams.particleDensity * 50);
this.particles = Array.from({ length: count }, () => ({
x: Math.random() * 100,
y: Math.random() * 100,
size: 2 + Math.random() * 6,
speed: 0.2 + Math.random() * 0.8,
opacity: 0.1 + Math.random() * 0.5,
color: this.getTemperatureColor()
}));
}
private startAnimation(): void {
this.animationTimer = setInterval(() => {
this.pulsePhase = (this.pulsePhase + 0.02) % (Math.PI * 2);
this.updateParticles();
}, 50);
}
private updateParticles(): void {
this.particles = this.particles.map(p => ({
...p,
y: (p.y - p.speed + 100) % 100,
opacity: p.opacity * (0.8 + 0.2 * Math.sin(this.pulsePhase))
}));
}
private getTemperatureColor(): string {
// 根据色温返回颜色
const temp = this.envParams.colorTemperature;
if (temp < 3000) return '#FF9500'; // 暖黄
if (temp < 4000) return '#FFCC00'; // 暖白
if (temp < 5000) return '#FFFFFF'; // 正白
return '#B8D4E3'; // 冷白
}
build() {
Stack() {
// 背景层
this.buildEmotionBackground()
// 粒子层
ForEach(this.particles, (particle, index) => {
Circle()
.width(particle.size)
.height(particle.size)
.fill(particle.color)
.opacity(particle.opacity * this.envParams.brightness)
.position({ x: `${particle.x}%`, y: `${particle.y}%` })
.blur(particle.size / 2)
.animation({
duration: 100,
curve: Curve.Linear
})
})
// 呼吸引导圆环
this.buildBreathingGuide()
// 中心焦点
this.buildFocusPoint()
}
.width('100%')
.height('100%')
}
@Builder
buildEmotionBackground(): void {
Column() {
// 基础背景色
Column()
.width('100%')
.height('100%')
.backgroundColor(this.getEmotionBaseColor())
// 渐变光晕
Column()
.width(800)
.height(800)
.backgroundColor(this.getTemperatureColor())
.blur(300)
.opacity(0.15 * this.envParams.brightness)
.position({ x: '50%', y: '50%' })
.anchor('50%')
.animation({
duration: this.envParams.pulseSpeed * 1000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
.scale({ x: 1.5, y: 1.5 })
}
.width('100%')
.height('100%')
}
@Builder
buildBreathingGuide(): void {
Stack() {
// 外环
Circle()
.width(200 + Math.sin(this.pulsePhase) * 40)
.height(200 + Math.sin(this.pulsePhase) * 40)
.fill('transparent')
.border({
width: 2,
color: `rgba(255,255,255,${0.1 + 0.1 * Math.sin(this.pulsePhase)})`
})
.animation({
duration: this.envParams.pulseSpeed * 1000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
// 内环
Circle()
.width(150 + Math.sin(this.pulsePhase) * 30)
.height(150 + Math.sin(this.pulsePhase) * 30)
.fill(`rgba(255,255,255,${0.05 + 0.05 * Math.sin(this.pulsePhase)})`)
.animation({
duration: this.envParams.pulseSpeed * 1000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
// 呼吸提示文字
Text(Math.sin(this.pulsePhase) > 0 ? '吸气' : '呼气')
.fontSize(16)
.fontColor('rgba(255,255,255,0.6)')
.fontWeight(FontWeight.Light)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder
buildFocusPoint(): void {
Column() {
Circle()
.width(8)
.height(8)
.fill('rgba(255,255,255,0.8)')
.shadow({
radius: 20,
color: 'rgba(255,255,255,0.3)'
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.position({ x: 0, y: 0 })
}
private getEmotionBaseColor(): string {
const emotion = AppStorage.get<string>('emotion_state') || 'calm';
const colors: Map<string, string> = new Map([
['calm', '#1a2f1a'],
['anxious', '#1a2f2f'],
['joyful', '#2f2a1a'],
['tired', '#2f2a1a'],
['sad', '#2a1a2f']
]);
return colors.get(emotion) || '#1a2f1a';
}
}
5.5 悬浮场景导航页签(FloatTabNavigation.ets)
typescript
// entry/src/main/ets/components/FloatTabNavigation.ets
import { window } from '@kit.ArkUI';
import { HdsTabs, SystemMaterialEffect } from '@kit.UIDesignKit';
export enum TransparencyLevel {
STRONG = 0.85,
BALANCED = 0.70,
WEAK = 0.55
}
interface SessionTab {
id: string;
name: string;
theme: string;
duration: number;
icon: Resource;
}
@Component
export struct FloatTabNavigation {
@Prop currentIndex: number = 0;
@State navTransparency: number = TransparencyLevel.BALANCED;
@State isExpanded: boolean = false;
@State bottomAvoidHeight: number = 0;
@State tabs: SessionTab[] = [
{ id: '1', name: '晨间冥想', theme: '#7ED321', duration: 10, icon: $r('app.media.ic_sunrise') },
{ id: '2', name: '焦虑舒缓', theme: '#50E3C2', duration: 15, icon: $r('app.media.ic_calm') },
{ id: '3', name: '专注提升', theme: '#F5A623', duration: 25, icon: $r('app.media.ic_focus') },
{ id: '4', name: '睡眠引导', theme: '#9013FE', duration: 30, icon: $r('app.media.ic_moon') },
{ id: '5', name: '情绪释放', theme: '#BD10E0', duration: 20, icon: $r('app.media.ic_release') }
];
aboutToAppear(): void {
this.getBottomAvoidArea();
}
private async getBottomAvoidArea(): Promise<void> {
try {
const mainWindow = await window.getLastWindow();
const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
this.bottomAvoidHeight = avoidArea.bottomRect.height;
} catch (error) {
console.error('Failed to get avoid area:', error);
}
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
this.contentBuilder()
}
.padding({ bottom: this.bottomAvoidHeight + 88 })
Column() {
Stack() {
Column()
.width('100%')
.height('100%')
.backgroundBlurStyle(BlurStyle.REGULAR)
.opacity(this.navTransparency)
.backdropFilter($r('sys.blur.20'))
Column()
.width('100%')
.height('100%')
.linearGradient({
direction: GradientDirection.Top,
colors: [
['rgba(255,255,255,0.15)', 0.0],
['rgba(255,255,255,0.05)', 1.0]
]
})
}
.width('100%')
.height('100%')
.borderRadius(20)
.shadow({
radius: 20,
color: 'rgba(0,0,0,0.2)',
offsetX: 0,
offsetY: -4
})
Row() {
ForEach(this.tabs, (tab: SessionTab, index: number) => {
this.buildSessionTab(tab, index)
}, (tab: SessionTab) => tab.id)
}
.width('100%')
.height(64)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.Start)
if (this.isExpanded) {
this.buildTransparencyPanel()
}
}
.width('96%')
.height(this.isExpanded ? 108 : 64)
.margin({
bottom: this.bottomAvoidHeight + 12,
left: '2%',
right: '2%'
})
.animation({
duration: 300,
curve: Curve.Spring,
iterations: 1
})
.gesture(
LongPressGesture({ duration: 500 })
.onAction(() => {
this.isExpanded = !this.isExpanded;
})
)
}
.width('100%')
.height('100%')
}
@Builder
buildSessionTab(tab: SessionTab, index: number): void {
Row({ space: 6 }) {
Image(tab.icon)
.width(16)
.height(16)
.fillColor(tab.theme)
Text(tab.name)
.fontSize(13)
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.currentIndex === index ? '#FFFFFF' : '#AAAAAA')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width(80)
Text(`${tab.duration}min`)
.fontSize(11)
.fontColor('#666666')
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
if (this.currentIndex === index) {
Button({ type: ButtonType.Circle }) {
Text('×')
.fontSize(14)
.fontColor('#AAAAAA')
}
.width(20)
.height(20)
.backgroundColor('transparent')
.onClick(() => {
this.closeTab(index);
})
}
}
.height(40)
.padding({ left: 12, right: 12 })
.backgroundColor(this.currentIndex === index
? 'rgba(255,255,255,0.15)'
: 'transparent')
.borderRadius(12)
.border({
width: this.currentIndex === index ? 1 : 0,
color: tab.theme
})
.onClick(() => {
this.currentIndex = index;
AppStorage.setOrCreate('current_session', tab.name);
AppStorage.setOrCreate('current_theme', tab.theme);
AppStorage.setOrCreate('session_duration', tab.duration);
})
}
@Builder
buildTransparencyPanel(): void {
Row({ space: 12 }) {
Text('透明度')
.fontSize(12)
.fontColor('#AAAAAA')
Slider({
value: this.navTransparency * 100,
min: 55,
max: 85,
step: 15,
style: SliderStyle.InSet
})
.width(120)
.onChange((value: number) => {
this.navTransparency = value / 100;
})
Text(`${Math.round(this.navTransparency * 100)}%`)
.fontSize(12)
.fontColor('#AAAAAA')
Button('强')
.fontSize(11)
.backgroundColor(this.navTransparency === TransparencyLevel.STRONG
? '#7ED321'
: 'rgba(255,255,255,0.1)')
.onClick(() => { this.navTransparency = TransparencyLevel.STRONG; })
Button('平衡')
.fontSize(11)
.backgroundColor(this.navTransparency === TransparencyLevel.BALANCED
? '#7ED321'
: 'rgba(255,255,255,0.1)')
.onClick(() => { this.navTransparency = TransparencyLevel.BALANCED; })
Button('弱')
.fontSize(11)
.backgroundColor(this.navTransparency === TransparencyLevel.WEAK
? '#7ED321'
: 'rgba(255,255,255,0.1)')
.onClick(() => { this.navTransparency = TransparencyLevel.WEAK; })
}
.width('100%')
.height(44)
.justifyContent(FlexAlign.Center)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius({ topLeft: 12, topRight: 12 })
}
private closeTab(index: number): void {
if (this.tabs.length <= 1) return;
this.tabs.splice(index, 1);
if (this.currentIndex >= index && this.currentIndex > 0) {
this.currentIndex--;
}
}
@BuilderParam contentBuilder: () => void = this.defaultContentBuilder;
@Builder
defaultContentBuilder(): void {
Column() {
Text('冥想区域')
.fontSize(16)
.fontColor('#999999')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
5.6 多窗口光效同步管理器(WindowManager.ets)
typescript
// entry/src/main/ets/utils/WindowManager.ets
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export interface ToolWindowConfig {
name: string;
title: string;
width: number;
height: number;
x?: number;
y?: number;
followMainWindow?: boolean;
themeColor?: string;
}
export class WindowManager {
private static instance: WindowManager;
private mainWindow: window.Window | null = null;
private subWindows: Map<string, window.Window> = new Map();
static getInstance(): WindowManager {
if (!WindowManager.instance) {
WindowManager.instance = new WindowManager();
}
return WindowManager.instance;
}
async initializeMainWindow(windowStage: window.WindowStage): Promise<void> {
this.mainWindow = windowStage.getMainWindowSync();
await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
await this.mainWindow.setWindowTitleBarEnable(false);
await this.mainWindow.setWindowShadowEnabled(true);
await this.mainWindow.setWindowCornerRadius(12);
await this.mainWindow.setWindowBackgroundColor('#00000000');
this.mainWindow.on('windowFocusChange', (isFocused: boolean) => {
AppStorage.setOrCreate('window_focused', isFocused);
if (isFocused) {
this.syncGlobalLightEffect(AppStorage.get<string>('global_theme_color') || '#7ED321');
}
});
console.info('Main window initialized for Flow Space');
}
async createToolWindow(config: ToolWindowConfig): Promise<window.Window | null> {
try {
if (!this.mainWindow) {
throw new Error('Main window not initialized');
}
const subWindow = await this.mainWindow.createSubWindow(config.name);
await subWindow.setWindowSizeType(window.WindowSizeType.FREE);
await subWindow.moveWindowTo({ x: config.x ?? 100, y: config.y ?? 100 });
await subWindow.resize(config.width, config.height);
await subWindow.setWindowBackgroundColor('#00000000');
await subWindow.setWindowShadowEnabled(true);
await subWindow.setWindowCornerRadius(16);
await subWindow.setWindowTopmost(true);
this.subWindows.set(config.name, subWindow);
await subWindow.setUIContent(`pages/${config.name}`);
await subWindow.showWindow();
if (config.followMainWindow) {
this.setupWindowFollow(subWindow, config);
}
this.syncSubWindowLightEffect(subWindow, config.name, config.themeColor);
return subWindow;
} catch (error) {
console.error(`Failed to create tool window:`, (error as BusinessError).message);
return null;
}
}
private setupWindowFollow(subWindow: window.Window, config: ToolWindowConfig): void {
this.mainWindow?.on('windowRectChange', (data: window.RectChangeOptions) => {
if (data.rectChangeReason === window.RectChangeReason.MOVE) {
const mainRect = this.mainWindow?.getWindowProperties().windowRect;
if (mainRect) {
subWindow.moveWindowTo({
x: mainRect.left + (config.x ?? 100),
y: mainRect.top + (config.y ?? 100)
});
}
}
});
}
private syncSubWindowLightEffect(subWindow: window.Window, name: string, themeColor?: string): void {
subWindow.on('windowFocusChange', (isFocused: boolean) => {
AppStorage.setOrCreate(`window_${name}_focused`, isFocused);
if (isFocused && themeColor) {
AppStorage.setOrCreate('global_theme_color', themeColor);
}
});
AppStorage.watch('global_theme_color', (color: string) => {
console.info(`Syncing theme color ${color} to window ${name}`);
});
}
async syncGlobalLightEffect(color: string): Promise<void> {
AppStorage.setOrCreate('global_theme_color', color);
}
async openBreathWindow(): Promise<void> {
await this.createToolWindow({
name: 'BreathWindow',
title: '呼吸引导',
width: 300,
height: 400,
x: 1000,
y: 100,
themeColor: '#50E3C2'
});
}
async openDiaryWindow(): Promise<void> {
await this.createToolWindow({
name: 'DiaryWindow',
title: '情绪日记',
width: 400,
height: 500,
x: 200,
y: 100,
themeColor: '#F5A623'
});
}
async openAmbientWindow(): Promise<void> {
await this.createToolWindow({
name: 'AmbientWindow',
title: '白噪音',
width: 350,
height: 400,
x: 50,
y: 500,
themeColor: '#9013FE'
});
}
async closeToolWindow(name: string): Promise<void> {
const subWindow = this.subWindows.get(name);
if (subWindow) {
await subWindow.destroyWindow();
this.subWindows.delete(name);
}
}
}
5.7 浮动呼吸引导窗口(BreathWindow.ets)
typescript
// entry/src/main/ets/pages/BreathWindow.ets
@Entry
@Component
struct BreathWindow {
@State breathPhase: number = 0; // 0-1 吸气, 1-2 屏息, 2-3 呼气, 3-4 屏息
@State breathProgress: number = 0;
@State isFocused: boolean = false;
@State themeColor: string = '#50E3C2';
@State pulseSpeed: number = 2.0;
private breathTimer: number | null = null;
aboutToAppear(): void {
AppStorage.watch('window_BreathWindow_focused', (focused: boolean) => {
this.isFocused = focused;
});
AppStorage.watch('current_theme', (color: string) => {
this.themeColor = color;
});
AppStorage.watch('environment_params', (params: EnvironmentParameters) => {
this.pulseSpeed = params.pulseSpeed;
this.restartBreathCycle();
});
this.startBreathCycle();
}
aboutToDisappear(): void {
if (this.breathTimer) {
clearInterval(this.breathTimer);
}
}
private startBreathCycle(): void {
const cycleDuration = this.pulseSpeed * 1000 * 4; // 4阶段呼吸
this.breathTimer = setInterval(() => {
this.breathProgress = (this.breathProgress + 0.01) % 4;
this.breathPhase = Math.floor(this.breathProgress);
}, cycleDuration / 400);
}
private restartBreathCycle(): void {
if (this.breathTimer) {
clearInterval(this.breathTimer);
}
this.startBreathCycle();
}
private getBreathText(): string {
const texts = ['吸气', '屏息', '呼气', '屏息'];
return texts[this.breathPhase] || '吸气';
}
private getBreathColor(): string {
const colors = ['#50E3C2', '#F5A623', '#7ED321', '#F5A623'];
return colors[this.breathPhase] || '#50E3C2';
}
private getCircleScale(): number {
const phaseProgress = this.breathProgress % 1;
if (this.breathPhase === 0) return 0.8 + phaseProgress * 0.4; // 吸气放大
if (this.breathPhase === 1) return 1.2; // 屏息保持
if (this.breathPhase === 2) return 1.2 - phaseProgress * 0.4; // 呼气缩小
return 0.8; // 屏息保持
}
build() {
Stack() {
Column() {
Column()
.width(400)
.height(400)
.backgroundColor(this.themeColor)
.blur(150)
.opacity(this.isFocused ? 0.1 : 0.05)
.position({ x: '50%', y: '30%' })
.anchor('50%')
}
.width('100%')
.height('100%')
.backgroundColor('#0f0f1a')
Column() {
Row() {
Text('呼吸引导')
.fontSize(14)
.fontColor(this.themeColor)
.fontWeight(FontWeight.Bold)
Row({ space: 8 }) {
Circle().width(12).height(12).fill('#FF5F56')
Circle().width(12).height(12).fill('#FFBD2E')
Circle().width(12).height(12).fill('#27C93F')
}
}
.width('100%')
.height(36)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(`rgba(${this.hexToRgb(this.themeColor)},0.05)`)
// 呼吸圆环
Stack() {
// 外环
Circle()
.width(180)
.height(180)
.fill('transparent')
.border({
width: 3,
color: `rgba(255,255,255,0.1)`
})
// 呼吸环
Circle()
.width(180 * this.getCircleScale())
.height(180 * this.getCircleScale())
.fill(`rgba(${this.hexToRgb(this.getBreathColor())},0.2)`)
.border({
width: 2,
color: this.getBreathColor()
})
.animation({
duration: 100,
curve: Curve.Linear
})
// 中心文字
Column() {
Text(this.getBreathText())
.fontSize(24)
.fontColor(this.getBreathColor())
.fontWeight(FontWeight.Bold)
Text(`${Math.round(this.pulseSpeed * 4)}秒/周期`)
.fontSize(12)
.fontColor('#AAAAAA')
}
}
.width('100%')
.height(250)
.justifyContent(FlexAlign.Center)
// 阶段指示器
Row({ space: 8 }) {
ForEach([0, 1, 2, 3], (phase) => {
Column()
.width(40)
.height(4)
.backgroundColor(phase === this.breathPhase ? this.getBreathColor() : 'rgba(255,255,255,0.1)')
.borderRadius(2)
.animation({
duration: 300,
curve: Curve.EaseInOut
})
})
}
.width('100%')
.height(20)
.justifyContent(FlexAlign.Center)
// 调节控制
Row({ space: 12 }) {
Text('节奏')
.fontSize(12)
.fontColor('#AAAAAA')
Slider({
value: this.pulseSpeed,
min: 1.0,
max: 4.0,
step: 0.5,
style: SliderStyle.InSet
})
.width(150)
.onChange((value: number) => {
this.pulseSpeed = value;
this.restartBreathCycle();
})
Text(`${this.pulseSpeed.toFixed(1)}s`)
.fontSize(12)
.fontColor('#AAAAAA')
}
.width('100%')
.height(40)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
private hexToRgb(hex: string): string {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ?
`${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}`
: '80,227,194';
}
}
5.8 主页面集成(WellnessPage.ets)
typescript
// entry/src/main/ets/pages/WellnessPage.ets
import { ImmersiveTitleBar, EmotionState, SessionType } from '../components/ImmersiveTitleBar';
import { FloatTabNavigation } from '../components/FloatTabNavigation';
import { MeditationScene } from '../components/MeditationScene';
import { EmotionResponsiveEnvironment } from '../components/EmotionResponsiveEnvironment';
import { WindowManager } from '../utils/WindowManager';
@Entry
@Component
struct WellnessPage {
@State currentSession: number = 0;
@State currentEmotion: EmotionState = EmotionState.CALM;
@State currentTheme: string = '#7ED321';
@State sessionType: SessionType = SessionType.MEDITATION;
@State envParams: EnvironmentParameters = {
colorTemperature: 4000,
brightness: 0.6,
pulseSpeed: 2.0,
particleDensity: 0.5,
ambientSound: 'forest'
};
@State useFaceAR: boolean = true;
@State lightIntensity: number = 0.6;
aboutToAppear(): void {
AppStorage.watch('current_session', (session: string) => {
// 场景切换逻辑
});
AppStorage.watch('emotion_state', (emotion: string) => {
this.currentEmotion = this.getEmotionEnum(emotion);
this.currentTheme = this.getEmotionColor(emotion);
});
AppStorage.watch('environment_params', (params: EnvironmentParameters) => {
this.envParams = params;
});
AppStorage.watch('session_action', (action: string) => {
if (action === 'toggle_play') {
// 播放/暂停
} else if (action === 'set_timer') {
// 设置计时器
}
});
AppStorage.watch('window_action', (action: string) => {
if (action === 'open_breath') {
WindowManager.getInstance().openBreathWindow();
} else if (action === 'open_diary') {
WindowManager.getInstance().openDiaryWindow();
} else if (action === 'open_ambient') {
WindowManager.getInstance().openAmbientWindow();
}
});
}
private getEmotionEnum(emotion: string): EmotionState {
const enums: Map<string, EmotionState> = new Map([
['calm', EmotionState.CALM],
['anxious', EmotionState.ANXIOUS],
['joyful', EmotionState.JOYFUL],
['tired', EmotionState.TIRED],
['sad', EmotionState.SAD]
]);
return enums.get(emotion) || EmotionState.CALM;
}
private getEmotionColor(emotion: string): string {
const colors: Map<string, string> = new Map([
['calm', '#7ED321'],
['anxious', '#50E3C2'],
['joyful', '#F5A623'],
['tired', '#F8E71C'],
['sad', '#BD10E0']
]);
return colors.get(emotion) || '#7ED321';
}
private getSessionType(index: number): SessionType {
const types = [SessionType.MEDITATION, SessionType.BREATHING, SessionType.FOCUS, SessionType.SLEEP, SessionType.MEDITATION];
return types[index] || SessionType.MEDITATION;
}
build() {
Stack() {
this.buildAmbientLightLayer()
Column() {
ImmersiveTitleBar({
currentSession: this.getSessionName(this.currentSession),
sessionType: this.getSessionType(this.currentSession),
emotionState: this.currentEmotion
})
Stack() {
MeditationScene({
envParams: this.envParams
})
.width('100%')
.height('100%')
if (this.useFaceAR) {
EmotionResponsiveEnvironment()
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
}
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
FloatTabNavigation({
currentIndex: this.currentSession,
onTabChange: (index: number) => {
this.currentSession = index;
this.currentTheme = this.getSessionTheme(index);
this.sessionType = this.getSessionType(index);
WindowManager.getInstance().syncGlobalLightEffect(this.currentTheme);
},
contentBuilder: () => {}
})
}
.width('100%')
.height('100%')
.backgroundColor('#0a0a0f')
.expandSafeArea(
[SafeAreaType.SYSTEM],
[SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END]
)
}
private getSessionName(index: number): string {
const names = ['晨间冥想', '焦虑舒缓', '专注提升', '睡眠引导', '情绪释放'];
return names[index] || '晨间冥想';
}
private getSessionTheme(index: number): string {
const themes = ['#7ED321', '#50E3C2', '#F5A623', '#9013FE', '#BD10E0'];
return themes[index] || '#7ED321';
}
@Builder
buildAmbientLightLayer(): void {
Column() {
Column()
.width(600)
.height(600)
.backgroundColor(this.currentTheme)
.blur(180)
.opacity(this.lightIntensity * 0.3)
.position({ x: '50%', y: '25%' })
.anchor('50%')
.animation({
duration: this.envParams.pulseSpeed * 1000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
.scale({ x: 1.4, y: 1.4 })
Column()
.width('100%')
.height(250)
.backgroundColor(this.currentTheme)
.opacity(this.lightIntensity * 0.08)
.blur(120)
.position({ x: 0, y: '75%' })
.linearGradient({
direction: GradientDirection.Top,
colors: [
[this.currentTheme, 0.0],
['transparent', 1.0]
]
})
}
.width('100%')
.height('100%')
.backgroundColor('#050508')
}
}
六、关键技术总结
6.1 沉浸光感实现清单
| 技术点 | API/方法 | 应用场景 |
|---|---|---|
| 系统材质效果 | systemMaterialEffect: SystemMaterialEffect.IMMERSIVE |
HdsNavigation标题栏 |
| 背景模糊 | backgroundBlurStyle(BlurStyle.REGULAR) |
悬浮导航玻璃拟态 |
| 背景滤镜 | backdropFilter($r('sys.blur.20')) |
精细模糊控制 |
| 安全区扩展 | expandSafeArea([SafeAreaType.SYSTEM], [...]) |
全屏沉浸布局 |
| 窗口沉浸 | setWindowLayoutFullScreen(true) |
无边框模式 |
| 光效动画 | animation({ duration, iterations: -1 }) |
呼吸灯/情绪响应 |
| 动态透明度 | backgroundOpacity |
焦点感知降级 |
6.2 Face AR情绪感知实现要点
| 技术点 | API/方法 | 说明 |
|---|---|---|
| AR会话创建 | arEngine.createARSession({ mode: ARMode.FACE }) |
启用人脸模式 |
| 表情跟踪器 | session.createFaceTracker({ enableExpression: true }) |
启用表情捕捉 |
| 多表情融合 | expressions.get(FaceExpressionType.SMILE) |
微笑+睁眼=愉悦 |
| 情绪状态机 | 自定义情绪判断逻辑 | 多特征融合决策 |
| 环境参数映射 | 自定义EnvironmentParameters | 情绪->色温/亮度/节奏 |
| 实时响应 | AppStorage.watch('emotion_state') |
全局状态驱动UI |
6.3 PC端多窗口光效协同
- 主窗口:全屏沉浸,环境光随情绪状态呼吸变化
- 浮动工具窗口:置顶、圆角、阴影,跟随主窗口移动
- 光效同步 :通过
AppStorage全局状态实现跨窗口主题色联动 - 焦点感知:窗口激活时边缘发光增强,失活时自动降低光效强度
七、调试与性能优化
7.1 真机调试建议
- Face AR情绪识别:需要良好光线环境,避免逆光或暗光
- 呼吸节奏同步:验证光效呼吸与引导提示的一致性
- 多窗口情绪同步:验证情绪变化时所有窗口的响应延迟
7.2 性能优化策略
typescript
// 1. Face AR性能优化
private optimizeARPerformance(): void {
if (this.arSession) {
this.arSession.setCameraConfig({
fps: 10,
resolution: { width: 320, height: 240 }
});
}
}
// 2. 粒子系统优化
private optimizeParticles(): void {
// 根据性能动态调整粒子数量
const maxParticles = this.getDevicePerformance() > 0.7 ? 50 : 25;
}
// 3. 窗口创建优化
private async lazyLoadToolWindows(): Promise<void> {
if (!this.breathWindow) {
this.breathWindow = await WindowManager.getInstance().openBreathWindow();
}
}
八、总结与展望
本文基于HarmonyOS 6(API 23)的悬浮导航 、沉浸光感 与Face AR特性,完整实战了一款面向PC端的"心流空间"沉浸式心理疗愈与正念冥想平台。核心创新点总结:
-
情绪响应光效系统:根据Face AR识别的实时情绪状态动态切换环境光色、色温、亮度和呼吸节奏,实现"情绪即环境"的响应式疗愈
-
Face AR情绪感知:基于AR Engine实时捕捉微表情变化,识别平静、焦虑、愉悦、疲惫、悲伤等状态,自动调节疗愈方案
-
悬浮场景导航:底部悬浮页签替代传统菜单,玻璃拟态设计+三档透明度调节,在保持导航可达性的同时最大化冥想区域
-
PC级多窗口疗愈 :主冥想窗口 + 浮动呼吸引导 + 情绪日记 + 白噪音控制的四层架构,通过
WindowManager实现跨窗口光效联动与焦点感知
未来扩展方向:
- 接入分布式软总线,实现跨设备协同疗愈(手机情绪监测、平板呼吸引导、PC主控环境)
- AI疗愈助手:基于长期情绪数据,AI个性化推荐疗愈方案
- VR/AR融合冥想:支持VR头显接入,实现完全沉浸式的自然场景冥想
- 生物反馈融合:接入心率、皮电等传感器,实现多模态情绪识别
转载自:https://blog.csdn.net/u014727709/article/details/138146992
欢迎 👍点赞✍评论⭐收藏,欢迎指正