HarmonyOS 6健康应用实战:基于悬浮导航与沉浸光感的“光影律动“智能健身系统

文章目录


每日一句正能量

人生就像骑自行车,想要保持平衡,就必须不断前行。停下来就会倒下,犹豫就会败北。即使前路颠簸,也要握紧车把,奋力蹬踏。风吹过脸庞的感觉,只有前进的人才懂。早上好!

一、前言:运动健康的视觉与交互新范式

在智能健身领域,数据可视化与交互体验直接影响用户的运动积极性和训练效果。HarmonyOS 6(API 23)带来的**悬浮导航(Float Navigation)沉浸光感(Immersive Light Effects)**特性,为健康类应用开发提供了革命性的设计工具。

传统健身APP的固定导航栏会遮挡运动数据图表,而复杂的菜单操作在运动中极不便利。HarmonyOS 6的悬浮导航允许在运动过程中通过手势快速切换数据视图,无需精确点击;沉浸光感则能根据实时心率、运动强度动态调整环境氛围,将生理数据转化为视觉反馈,创造"身体即界面"的沉浸式体验。

本文将构建一款名为**"光影律动"**的智能健身应用,展示如何:

  • 运动感知悬浮导航:根据运动状态(静止/热身/燃脂/冲刺)自动调整导航形态
  • 生理数据光效映射:实时心率、卡路里消耗转化为动态光效反馈
  • 多设备数据融合:手表传感器数据驱动手机端光效变化
  • 全屏沉浸训练模式 :利用expandSafeArea实现无边界运动界面

二、健康场景下的特性适配设计

2.1 运动状态与导航的协同

运动阶段 心率区间 导航形态 光效特征 交互优先级
静止/准备 <100bpm 底部展开栏 平静呼吸光 高(可配置)
热身 100-120bpm 侧边迷你轨 温和渐变色
燃脂 120-150bpm 边缘光点 动态脉冲光 低(防误触)
有氧 150-170bpm 全隐藏+手势 快速闪烁警示 极低
冲刺 >170bpm 仅紧急按钮 红色警报光 仅紧急停止
恢复 下降中 逐步浮现 舒缓绿光

2.2 生理数据-光效映射算法

基于运动生理学原理,我们建立了心率变异性(HRV)与光效参数的实时映射模型:

typescript 复制代码
// 心率-光效映射配置
export const HeartRateLightMap = {
  zones: [
    { min: 0, max: 100, color: '#4ECDC4', label: 'rest', pulse: 6000 },      // 静息:青绿慢呼吸
    { min: 100, max: 120, color: '#2ECC71', label: 'warmup', pulse: 4000 },  // 热身:绿色温和
    { min: 120, max: 150, color: '#F5A623', label: 'fatburn', pulse: 2500 }, // 燃脂:橙色活跃
    { min: 150, max: 170, color: '#FF6B6B', label: 'cardio', pulse: 1500 },  // 有氧:红色急促
    { min: 170, max: 220, color: '#FF0000', label: 'peak', pulse: 800 }      // 峰值:深红警示
  ]
};

三、核心组件实战

3.1 运动感知悬浮导航(MotionAwareFloatNav.ets)

代码亮点:这是本应用的核心创新组件。它通过集成运动传感器和心率数据,实现"零点击"导航------在剧烈运动中,导航自动收缩为边缘光点防止误触;在恢复阶段,手势悬停即可展开完整菜单。导航形态与生理状态实时联动,这是传统固定导航无法实现的。

typescript 复制代码
// entry/src/main/ets/components/MotionAwareFloatNav.ets
import { sensor } from '@kit.SensorServiceKit';
import { health } from '@kit.HealthKit';

// 运动状态枚举
export enum MotionState {
  IDLE = 'idle',           // 静止
  WARMUP = 'warmup',       // 热身
  FAT_BURN = 'fat_burn',   // 燃脂
  CARDIO = 'cardio',       // 有氧
  PEAK = 'peak',           // 峰值
  RECOVERY = 'recovery'    // 恢复
}

// 导航模式
export enum FitnessNavMode {
  FULL = 'full',           // 完整展开
  MINI = 'mini',           // 迷你轨道
  DOT = 'dot',             // 边缘光点
  HIDDEN = 'hidden',       // 完全隐藏
  EMERGENCY = 'emergency'  // 仅紧急停止
}

// 导航项
interface FitnessNavItem {
  id: string;
  icon: Resource;
  label: string;
  shortcut: string;         // 手势快捷键
  requiresStable: boolean;  // 是否需要静止状态
}

@Component
export struct MotionAwareFloatNav {
  // 回调
  onItemSelect?: (item: FitnessNavItem) => void;
  onEmergencyStop?: () => void;
  onGesture?: (gesture: string) => void;
  
  @State currentHeartRate: number = 75;
  @State motionState: MotionState = MotionState.IDLE;
  @State navMode: FitnessNavMode = FitnessNavMode.FULL;
  @State activeItemId: string = 'dashboard';
  @State currentLightColor: string = '#4ECDC4';
  @State lightPulseSpeed: number = 6000;
  @State lightIntensity: number = 0.6;
  @State isExpanded: boolean = true;
  @State accelerometerData: {x: number, y: number, z: number} = {x: 0, y: 0, z: 0};
  @State gestureBuffer: string = '';  // 手势识别缓冲区
  
  // 导航数据
  private navItems: FitnessNavItem[] = [
    { id: 'dashboard', icon: $r('app.media.ic_dashboard'), label: '数据面板', shortcut: 'tap', requiresStable: false },
    { id: 'workout', icon: $r('app.media.ic_dumbbell'), label: '训练计划', shortcut: 'swipe_up', requiresStable: true },
    { id: 'history', icon: $r('app.media.ic_chart'), label: '历史记录', shortcut: 'swipe_left', requiresStable: true },
    { id: 'music', icon: $r('app.media.ic_music'), label: '运动音乐', shortcut: 'swipe_right', requiresStable: false },
    { id: 'settings', icon: $r('app.media.ic_settings'), label: '设置', shortcut: 'long_press', requiresStable: true }
  ];

  // 传感器管理
  private heartRateTimer: number = -1;
  private accelTimer: number = -1;
  private readonly EMERGENCY_THRESHOLD: number = 190; // 紧急心率阈值

  aboutToAppear(): void {
    this.initializeSensors();
    this.startHealthMonitoring();
  }

  aboutToDisappear(): void {
    clearInterval(this.heartRateTimer);
    clearInterval(this.accelTimer);
  }

  private async initializeSensors(): Promise<void> {
    try {
      // 注册加速度计(检测运动强度)
      sensor.on(sensor.SensorId.SENSOR_TYPE_ID_ACCELEROMETER, (data) => {
        this.accelerometerData = {
          x: data.x,
          y: data.y,
          z: data.z
        };
        this.detectMotionIntensity();
      });
    } catch (error) {
      console.error('Sensor initialization failed:', error);
    }
  }

  private startHealthMonitoring(): void {
    // 模拟心率数据获取(实际应使用HealthKit)
    this.heartRateTimer = setInterval(() => {
      this.fetchHeartRate();
    }, 2000);
  }

  private async fetchHeartRate(): Promise<void> {
    try {
      // 实际项目中使用HealthKit获取实时心率
      // const heartRate = await health.getHeartRate();
      // 模拟数据:根据运动状态生成合理心率
      const simulatedHR = this.simulateHeartRate();
      this.currentHeartRate = simulatedHR;
      
      // 更新运动状态
      this.updateMotionState(simulatedHR);
      
      // 同步光效
      this.syncLightEffect(simulatedHR);
      
      // 检查紧急情况
      if (simulatedHR > this.EMERGENCY_THRESHOLD) {
        this.triggerEmergencyMode();
      }
    } catch (error) {
      console.error('Heart rate fetch failed:', error);
    }
  }

  private simulateHeartRate(): number {
    // 模拟心率变化(实际项目中替换为真实数据)
    const baseHR = 75;
    const variation = Math.sin(Date.now() / 10000) * 30 + Math.random() * 10;
    return Math.floor(Math.max(60, Math.min(200, baseHR + variation)));
  }

  private detectMotionIntensity(): void {
    // 通过加速度计检测运动强度
    const { x, y, z } = this.accelerometerData;
    const magnitude = Math.sqrt(x*x + y*y + z*z);
    
    // 运动强度辅助判断心率区间
    if (magnitude > 15) {
      // 高强度运动,优先使用心率判断
    }
  }

  private updateMotionState(heartRate: number): void {
    let newState: MotionState;
    
    if (heartRate < 100) {
      newState = MotionState.IDLE;
    } else if (heartRate < 120) {
      newState = MotionState.WARMUP;
    } else if (heartRate < 150) {
      newState = MotionState.FAT_BURN;
    } else if (heartRate < 170) {
      newState = MotionState.CARDIO;
    } else {
      newState = MotionState.PEAK;
    }
    
    // 检测恢复状态(心率下降)
    if (this.motionState === MotionState.CARDIO && heartRate < 140) {
      newState = MotionState.RECOVERY;
    }
    
    if (newState !== this.motionState) {
      this.motionState = newState;
      this.adaptNavToMotion(newState);
    }
  }

  private adaptNavToMotion(state: MotionState): void {
    switch (state) {
      case MotionState.IDLE:
      case MotionState.RECOVERY:
        this.navMode = FitnessNavMode.FULL;
        this.isExpanded = true;
        break;
      case MotionState.WARMUP:
        this.navMode = FitnessNavMode.MINI;
        this.isExpanded = false;
        break;
      case MotionState.FAT_BURN:
        this.navMode = FitnessNavMode.DOT;
        this.isExpanded = false;
        break;
      case MotionState.CARDIO:
        this.navMode = FitnessNavMode.HIDDEN;
        this.isExpanded = false;
        break;
      case MotionState.PEAK:
        this.navMode = FitnessNavMode.EMERGENCY;
        this.isExpanded = false;
        break;
    }
    
    console.info(`Nav adapted to motion state: ${state}, mode: ${this.navMode}`);
  }

  private syncLightEffect(heartRate: number): void {
    // 心率区间映射到光效
    const zones = [
      { max: 100, color: '#4ECDC4', pulse: 6000, intensity: 0.4 },
      { max: 120, color: '#2ECC71', pulse: 4000, intensity: 0.5 },
      { max: 150, color: '#F5A623', pulse: 2500, intensity: 0.7 },
      { max: 170, color: '#FF6B6B', pulse: 1500, intensity: 0.8 },
      { max: 220, color: '#FF0000', pulse: 800, intensity: 1.0 }
    ];
    
    const zone = zones.find(z => heartRate <= z.max) || zones[zones.length - 1];
    
    this.currentLightColor = zone.color;
    this.lightPulseSpeed = zone.pulse;
    this.lightIntensity = zone.intensity;
    
    // 同步到全局
    AppStorage.setOrCreate('fitness_light_color', zone.color);
    AppStorage.setOrCreate('fitness_light_pulse', zone.pulse);
    AppStorage.setOrCreate('fitness_light_intensity', zone.intensity);
    AppStorage.setOrCreate('current_heart_rate', heartRate);
  }

  private triggerEmergencyMode(): void {
    // 触发紧急模式
    this.navMode = FitnessNavMode.EMERGENCY;
    AppStorage.setOrCreate('emergency_mode', true);
    
    // 震动警示
    try {
      import('@kit.SensorServiceKit').then(sensor => {
        sensor.vibrator.startVibration({
          type: 'time',
          duration: 1000
        }, { id: 0 });
      });
    } catch (error) {
      console.error('Emergency vibration failed:', error);
    }
  }

  // 手势识别
  private onPanGesture(event: GestureEvent): void {
    const { offsetX, offsetY } = event;
    const threshold = 50;
    
    if (Math.abs(offsetX) > Math.abs(offsetY)) {
      // 水平手势
      if (offsetX > threshold) {
        this.gestureBuffer = 'swipe_right';
        this.handleGesture('next');
      } else if (offsetX < -threshold) {
        this.gestureBuffer = 'swipe_left';
        this.handleGesture('prev');
      }
    } else {
      // 垂直手势
      if (offsetY < -threshold) {
        this.gestureBuffer = 'swipe_up';
        this.handleGesture('expand');
      } else if (offsetY > threshold) {
        this.gestureBuffer = 'swipe_down';
        this.handleGesture('collapse');
      }
    }
  }

  private handleGesture(gesture: string): void {
    this.onGesture?.(gesture);
    
    switch (gesture) {
      case 'expand':
        if (this.navMode !== FitnessNavMode.EMERGENCY) {
          this.navMode = FitnessNavMode.FULL;
          this.isExpanded = true;
        }
        break;
      case 'collapse':
        this.adaptNavToMotion(this.motionState);
        break;
      case 'next':
        this.cycleNavItem(1);
        break;
      case 'prev':
        this.cycleNavItem(-1);
        break;
    }
  }

  private cycleNavItem(direction: number): void {
    const currentIndex = this.navItems.findIndex(i => i.id === this.activeItemId);
    const newIndex = (currentIndex + direction + this.navItems.length) % this.navItems.length;
    this.activeItemId = this.navItems[newIndex].id;
    this.onItemSelect?.(this.navItems[newIndex]);
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      // 内容层(由外部传入)
      this.contentBuilder()

      // 运动感知导航层
      if (this.navMode === FitnessNavMode.FULL) {
        this.buildFullNav()
      } else if (this.navMode === FitnessNavMode.MINI) {
        this.buildMiniNav()
      } else if (this.navMode === FitnessNavMode.DOT) {
        this.buildDotNav()
      } else if (this.navMode === FitnessNavMode.HIDDEN) {
        this.buildHiddenNav()
      } else if (this.navMode === FitnessNavMode.EMERGENCY) {
        this.buildEmergencyNav()
      }

      // 心率光效反馈层
      this.buildHeartRateGlow()
    }
    .width('100%')
    .height('100%')
    .gesture(
      PanGesture()
        .onActionUpdate((event: GestureEvent) => {
          this.onPanGesture(event);
        })
    )
  }

  @Builder
  contentBuilder(): void {
    Column() {
      Text('运动内容区域')
        .fontSize(16)
        .fontColor('rgba(255,255,255,0.3)')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  // 完整导航(静止/恢复状态)
  @Builder
  buildFullNav(): void {
    Column() {
      // 心率指示条
      this.buildHeartRateIndicator()

      // 导航容器
      Column() {
        Row({ space: 8 }) {
          ForEach(this.navItems, (item: FitnessNavItem) => {
            this.buildNavItem(item)
          })
        }
        .width('100%')
        .height(72)
        .padding({ left: 16, right: 16 })
        .justifyContent(FlexAlign.SpaceAround)
      }
      .width('94%')
      .height(80)
      .backgroundColor('rgba(30, 30, 40, 0.8)')
      .backdropFilter($r('sys.blur.25'))
      .borderRadius(24)
      .shadow({
        radius: 20,
        color: 'rgba(0, 0, 0, 0.3)',
        offsetX: 0,
        offsetY: -4
      })
    }
    .width('100%')
    .height(120)
    .padding({ bottom: 12 })
    .animation({
      duration: 300,
      curve: Curve.EaseInOut
    })
  }

  // 迷你导航(热身状态)
  @Builder
  buildMiniNav(): void {
    Row() {
      Column() {
        // 垂直进度指示
        ForEach(this.navItems, (item: FitnessNavItem, index: number) => {
          Column() {
            Column()
              .width(6)
              .height(6)
              .backgroundColor(item.id === this.activeItemId ? 
                this.currentLightColor : 'rgba(255,255,255,0.2)')
              .borderRadius(3)
              .shadow({
                radius: item.id === this.activeItemId ? 6 : 0,
                color: this.currentLightColor,
                offsetX: 0,
                offsetY: 0
              })
          }
          .width(24)
          .height(32)
          .onClick(() => {
            this.activeItemId = item.id;
            this.onItemSelect?.(item);
          })
        })
      }
      .width(32)
      .height(200)
      .backgroundColor('rgba(30, 30, 40, 0.6)')
      .backdropFilter($r('sys.blur.15'))
      .borderRadius({ topRight: 16, bottomRight: 16 })
      .position({ x: 0, y: '50%' })
      .anchor('50%')
    }
    .width(32)
    .height('100%')
  }

  // 边缘光点(燃脂状态)
  @Builder
  buildDotNav(): void {
    Column() {
      // 呼吸光点
      Column() {
        Column()
          .width(12)
          .height(12)
          .backgroundColor(this.currentLightColor)
          .borderRadius(6)
          .opacity(0.6 + Math.sin(Date.now() / this.lightPulseSpeed * Math.PI * 2) * 0.4)
          .shadow({
            radius: 8,
            color: this.currentLightColor,
            offsetX: 0,
            offsetY: 0
          })
      }
      .width(32)
      .height(32)
      .backgroundColor('rgba(30, 30, 40, 0.4)')
      .backdropFilter($r('sys.blur.10'))
      .borderRadius(16)
      .onClick(() => {
        // 点击展开迷你菜单
        this.navMode = FitnessNavMode.MINI;
      })
    }
    .width(32)
    .height(32)
    .position({ x: '100%', y: '50%' })
    .anchor('100%')
    .margin({ right: 8 })
  }

  // 隐藏导航(有氧状态)- 仅边缘热区
  @Builder
  buildHiddenNav(): void {
    Column()
      .width(20)
      .height('100%')
      .position({ x: '100%' })
      .anchor('100%')
      .onClick(() => {
        this.navMode = FitnessNavMode.DOT;
      })
  }

  // 紧急导航(峰值状态)
  @Builder
  buildEmergencyNav(): void {
    Column() {
      // 紧急停止按钮(大且明显)
      Button() {
        Column({ space: 4 }) {
          Image($r('app.media.ic_stop'))
            .width(32)
            .height(32)
            .fillColor('#FFFFFF')
          
          Text('紧急停止')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
        }
      }
      .type(ButtonType.Circle)
      .width(80)
      .height(80)
      .backgroundColor('#FF0000')
      .shadow({
        radius: 20,
        color: '#FF0000',
        offsetX: 0,
        offsetY: 0
      })
      .onClick(() => {
        this.onEmergencyStop?.();
      })
      .animation({
        duration: 500,
        curve: Curve.EaseInOut,
        iterations: -1,
        playMode: PlayMode.Alternate
      })
      .scale({ x: 1.05, y: 1.05 })

      // 心率警告
      Text(`${this.currentHeartRate} BPM`)
        .fontSize(18)
        .fontColor('#FF0000')
        .fontWeight(FontWeight.Bold)
        .margin({ top: 12 })
        .shadow({
          radius: 10,
          color: '#FF0000',
          offsetX: 0,
          offsetY: 0
        })
    }
    .width('100%')
    .height(200)
    .position({ x: 0, y: '100%' })
    .anchor('100%')
    .margin({ bottom: 40 })
  }

  // 心率指示条
  @Builder
  buildHeartRateIndicator(): void {
    Row() {
      // 心率图标
      Image($r('app.media.ic_heart'))
        .width(16)
        .height(16)
        .fillColor(this.currentLightColor)
        .margin({ right: 8 })

      // 心率值
      Text(`${this.currentHeartRate}`)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.currentLightColor)
        .margin({ right: 4 })

      Text('BPM')
        .fontSize(10)
        .fontColor('rgba(255,255,255,0.5)')

      // 心率区间条
      Stack() {
        Column()
          .width('100%')
          .height(4)
          .backgroundColor('rgba(255,255,255,0.1)')
          .borderRadius(2)

        Column()
          .width(`${Math.min(100, (this.currentHeartRate / 200) * 100)}%`)
          .height(4)
          .backgroundColor(this.currentLightColor)
          .borderRadius(2)
          .animation({ duration: 500 })
      }
      .width(80)
      .height(4)
      .margin({ left: 12 })
    }
    .width('100%')
    .height(32)
    .padding({ left: 20, right: 20 })
    .justifyContent(FlexAlign.Center)
  }

  // 导航项构建
  @Builder
  buildNavItem(item: FitnessNavItem): void {
    Column() {
      Stack() {
        // 选中背景
        if (this.activeItemId === item.id) {
          Column()
            .width(48)
            .height(48)
            .backgroundColor(this.currentLightColor)
            .opacity(0.2)
            .blur(10)
            .borderRadius(24)
        }

        Image(this.activeItemId === item.id ? item.activeIcon : item.icon)
          .width(24)
          .height(24)
          .fillColor(this.activeItemId === item.id ? this.currentLightColor : '#B0B0B0')
      }
      .width(48)
      .height(48)

      Text(item.label)
        .fontSize(10)
        .fontColor(this.activeItemId === item.id ? '#FFFFFF' : 'rgba(255,255,255,0.6)')
        .margin({ top: 2 })
    }
    .opacity(item.requiresStable && this.motionState !== MotionState.IDLE ? 0.4 : 1.0)
    .onClick(() => {
      if (!item.requiresStable || this.motionState === MotionState.IDLE) {
        this.activeItemId = item.id;
        this.onItemSelect?.(item);
      }
    })
  }

  // 心率光效反馈层
  @Builder
  buildHeartRateGlow(): void {
    Column() {
      // 底部心率光晕
      Column()
        .width('80%')
        .height(100)
        .backgroundColor(this.currentLightColor)
        .blur(80)
        .opacity(this.lightIntensity * 0.2)
        .position({ x: '50%', y: '100%' })
        .anchor('50%')
        .animation({
          duration: this.lightPulseSpeed,
          curve: Curve.EaseInOut,
          iterations: -1,
          playMode: PlayMode.Alternate
        })
        .scale({ x: 1.1, y: 1.1 })

      // 侧边心率脉冲(高强度时)
      if (this.motionState === MotionState.CARDIO || this.motionState === MotionState.PEAK) {
        Column()
          .width(40)
          .height('60%')
          .backgroundColor(this.currentLightColor)
          .blur(60)
          .opacity(this.lightIntensity * 0.15)
          .position({ x: 0, y: '20%' })
          .animation({
            duration: this.lightPulseSpeed,
            curve: Curve.EaseInOut,
            iterations: -1,
            playMode: PlayMode.Alternate
          })
      }
    }
    .width('100%')
    .height('100%')
    .pointerEvents(PointerEvents.None)
  }
}

3.2 实时数据光效可视化(DataLightVisualization.ets)

代码亮点:将抽象的运动数据(心率、配速、卡路里)转化为直观的动态光效。例如,心率变异性(HRV)通过光效脉冲的不规则性可视化,卡路里消耗累积通过光效亮度渐变呈现。这是HarmonyOS 6沉浸光感在健康数据领域的创新应用。

typescript 复制代码
// entry/src/main/ets/components/DataLightVisualization.ets
import { health } from '@kit.HealthKit';

// 运动数据接口
interface FitnessData {
  heartRate: number;        // 心率
  heartRateVariability: number; // 心率变异性
  calories: number;         // 卡路里
  distance: number;         // 距离
  pace: number;             // 配速
  steps: number;            // 步数
  duration: number;         // 运动时长
}

// 光效粒子
interface LightParticle {
  x: number;
  y: number;
  vx: number;
  vy: number;
  life: number;
  maxLife: number;
  color: string;
  size: number;
  type: 'heart' | 'calorie' | 'step';
}

@Component
export struct DataLightVisualization {
  @Prop fitnessData: FitnessData;
  @State particles: LightParticle[] = [];
  @State calorieGlow: number = 0;
  @State stepRipple: number = 0;
  @State hrvIrregularity: number = 0;
  
  private particleTimer: number = -1;
  private readonly MAX_PARTICLES: number = 50;

  aboutToAppear(): void {
    this.startParticleSystem();
  }

  aboutToDisappear(): void {
    clearInterval(this.particleTimer);
  }

  private startParticleSystem(): void {
    this.particleTimer = setInterval(() => {
      this.updateParticles();
      this.emitDataParticles();
    }, 100);
  }

  private emitDataParticles(): void {
    // 根据运动数据发射光效粒子
    
    // 心率粒子(每次心跳)
    if (Math.random() < this.fitnessData.heartRate / 60 / 10) {
      this.particles.push({
        x: 0.5 + (Math.random() - 0.5) * 0.2,
        y: 0.8,
        vx: (Math.random() - 0.5) * 0.01,
        vy: -0.02 - Math.random() * 0.02,
        life: 1.0,
        maxLife: 2.0,
        color: this.getHeartRateColor(this.fitnessData.heartRate),
        size: 3 + Math.random() * 4,
        type: 'heart'
      });
    }

    // 卡路里粒子(累积时)
    if (this.fitnessData.calories > 0 && Math.random() < 0.1) {
      this.calorieGlow = Math.min(1, this.calorieGlow + 0.01);
      this.particles.push({
        x: Math.random(),
        y: 1.0,
        vx: 0,
        vy: -0.01,
        life: 1.0,
        maxLife: 3.0,
        color: '#F5A623',
        size: 2 + Math.random() * 3,
        type: 'calorie'
      });
    }

    // 步数涟漪
    if (this.fitnessData.steps > 0 && this.fitnessData.steps % 100 === 0) {
      this.stepRipple = 1.0;
    }

    // 限制粒子数量
    if (this.particles.length > this.MAX_PARTICLES) {
      this.particles = this.particles.slice(-this.MAX_PARTICLES);
    }
  }

  private updateParticles(): void {
    this.particles = this.particles.filter(p => {
      p.x += p.vx;
      p.y += p.vy;
      p.life -= 0.05;
      p.vy *= 0.98; // 阻力
      return p.life > 0;
    });

    // 衰减累积效果
    this.calorieGlow *= 0.99;
    this.stepRipple *= 0.95;
    
    // 计算HRV不规则性
    this.hrvIrregularity = Math.min(1, this.fitnessData.heartRateVariability / 50);
  }

  private getHeartRateColor(hr: number): string {
    if (hr < 100) return '#4ECDC4';
    if (hr < 120) return '#2ECC71';
    if (hr < 150) return '#F5A623';
    if (hr < 170) return '#FF6B6B';
    return '#FF0000';
  }

  build() {
    Stack() {
      // 背景层
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('#0a0a0f')

      // 卡路里累积光晕
      Column()
        .width('60%')
        .height('40%')
        .backgroundColor('#F5A623')
        .blur(100)
        .opacity(this.calorieGlow * 0.3)
        .position({ x: '50%', y: '80%' })
        .anchor('50%')

      // 步数涟漪
      if (this.stepRipple > 0.1) {
        Column()
          .width(`${this.stepRipple * 100}%`)
          .height(`${this.stepRipple * 100}%`)
          .backgroundColor('transparent')
          .border({
            width: 2,
            color: `rgba(78, 205, 196, ${this.stepRipple * 0.5})`,
            style: BorderStyle.Solid
          })
          .borderRadius('50%')
          .position({ x: '50%', y: '50%' })
          .anchor('50%')
          .animation({ duration: 100 })
      }

      // HRV不规则性可视化(光效抖动)
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor(this.getHeartRateColor(this.fitnessData.heartRate))
        .opacity(this.hrvIrregularity * 0.05)
        .blur(50)
        .position({
          x: (Math.random() - 0.5) * this.hrvIrregularity * 20,
          y: (Math.random() - 0.5) * this.hrvIrregularity * 20
        })
        .animation({ duration: 50 })

      // 粒子层
      ForEach(this.particles, (particle: LightParticle, index: number) => {
        Column()
          .width(particle.size * particle.life)
          .height(particle.size * particle.life)
          .backgroundColor(particle.color)
          .blur(particle.size)
          .opacity(particle.life * 0.6)
          .borderRadius('50%')
          .position({
            x: `${particle.x * 100}%`,
            y: `${particle.y * 100}%`
          })
          .animation({ duration: 100 })
      })

      // 数据覆盖层
      this.buildDataOverlay()
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildDataOverlay(): void {
    Column({ space: 16 }) {
      // 心率大数字
      Row({ space: 8 }) {
        Text(`${this.fitnessData.heartRate}`)
          .fontSize(72)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.getHeartRateColor(this.fitnessData.heartRate))
          .fontVariant(FontVariant.TabNums)
          .shadow({
            radius: 20,
            color: this.getHeartRateColor(this.fitnessData.heartRate),
            offsetX: 0,
            offsetY: 0
          })

        Column() {
          Text('BPM')
            .fontSize(16)
            .fontColor('rgba(255,255,255,0.5)')
          
          // 心率区间指示
          Text(this.getHeartRateZone(this.fitnessData.heartRate))
            .fontSize(12)
            .fontColor(this.getHeartRateColor(this.fitnessData.heartRate))
        }
        .alignItems(HorizontalAlign.Start)
      }

      // 其他数据网格
      Row({ space: 16 }) {
        this.buildDataCard('卡路里', `${Math.floor(this.fitnessData.calories)}`, 'kcal', '#F5A623')
        this.buildDataCard('距离', `${(this.fitnessData.distance / 1000).toFixed(2)}`, 'km', '#4ECDC4')
        this.buildDataCard('配速', `${this.fitnessData.pace.toFixed(1)}`, '/km', '#9B59B6')
      }

      // 运动时长
      Text(this.formatDuration(this.fitnessData.duration))
        .fontSize(24)
        .fontColor('rgba(255,255,255,0.8)')
        .fontVariant(FontVariant.TabNums)
    }
    .width('100%')
    .height('100%')
    .padding(40)
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  buildDataCard(label: string, value: string, unit: string, color: string): void {
    Column({ space: 4 }) {
      Text(label)
        .fontSize(12)
        .fontColor('rgba(255,255,255,0.5)')
      
      Row({ space: 4 }) {
        Text(value)
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor(color)
          .fontVariant(FontVariant.TabNums)
        
        Text(unit)
          .fontSize(12)
          .fontColor('rgba(255,255,255,0.4)')
          .margin({ top: 8 })
      }
    }
    .width(100)
    .height(80)
    .backgroundColor('rgba(255,255,255,0.03)')
    .backdropFilter($r('sys.blur.10'))
    .borderRadius(16)
    .border({
      width: 1,
      color: `${color}33`
    })
  }

  private getHeartRateZone(hr: number): string {
    if (hr < 100) return '静息';
    if (hr < 120) return '热身';
    if (hr < 150) return '燃脂';
    if (hr < 170) return '有氧';
    return '峰值';
  }

  private formatDuration(seconds: number): string {
    const hrs = Math.floor(seconds / 3600);
    const mins = Math.floor((seconds % 3600) / 60);
    const secs = seconds % 60;
    return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }
}

3.3 训练计划悬浮卡片(WorkoutFloatCard.ets)

代码亮点:在运动过程中,训练计划以悬浮卡片形式呈现,支持语音控制和手势交互。卡片采用玻璃拟态设计,根据当前训练阶段的光效主题动态变化边框颜色,实现"训练即视觉"的沉浸体验。

typescript 复制代码
// entry/src/main/ets/components/WorkoutFloatCard.ets

// 训练阶段
interface WorkoutPhase {
  id: string;
  name: string;
  duration: number;         // 秒
  targetHeartRate: number;  // 目标心率
  description: string;
  color: string;
}

@Component
export struct WorkoutFloatCard {
  @Prop phases: WorkoutPhase[];
  @Prop currentPhaseIndex: number = 0;
  @Prop currentPhaseTime: number = 0;  // 当前阶段已进行时间
  @State isExpanded: boolean = false;
  @State currentLightColor: string = '#4ECDC4';
  
  private readonly phaseProgress: number = 0;

  aboutToAppear(): void {
    // 监听光效变化
    AppStorage.watch('fitness_light_color', (color: string) => {
      this.currentLightColor = color;
    });
  }

  private getPhaseProgress(): number {
    if (this.phases.length === 0) return 0;
    const phase = this.phases[this.currentPhaseIndex];
    return Math.min(1, this.currentPhaseTime / phase.duration);
  }

  private getTotalProgress(): number {
    if (this.phases.length === 0) return 0;
    let totalDuration = 0;
    let completedDuration = 0;
    
    this.phases.forEach((phase, index) => {
      totalDuration += phase.duration;
      if (index < this.currentPhaseIndex) {
        completedDuration += phase.duration;
      }
    });
    
    completedDuration += this.currentPhaseTime;
    return completedDuration / totalDuration;
  }

  build() {
    Stack() {
      // 悬浮卡片
      Column({ space: 12 }) {
        // 顶部拖拽条
        Column()
          .width(40)
          .height(4)
          .backgroundColor('rgba(255,255,255,0.3)')
          .borderRadius(2)
          .margin({ top: 8 })

        // 阶段信息(始终显示)
        Row({ space: 12 }) {
          // 当前阶段指示
          Stack() {
            Column()
              .width(48)
              .height(48)
              .backgroundColor(this.currentLightColor)
              .opacity(0.2)
              .blur(10)
              .borderRadius(24)

            Text(`${this.currentPhaseIndex + 1}`)
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor(this.currentLightColor)
          }
          .width(48)
          .height(48)

          Column({ space: 4 }) {
            Text(this.phases[this.currentPhaseIndex]?.name ?? '准备')
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .fontColor('#FFFFFF')

            Text(`${this.formatTime(this.currentPhaseTime)} / ${this.formatTime(this.phases[this.currentPhaseIndex]?.duration ?? 0)}`)
              .fontSize(12)
              .fontColor('rgba(255,255,255,0.5)')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          // 展开/收起按钮
          Button() {
            Image(this.isExpanded ? $r('app.media.ic_collapse') : $r('app.media.ic_expand'))
              .width(20)
              .height(20)
              .fillColor('rgba(255,255,255,0.6)')
          }
          .type(ButtonType.Circle)
          .backgroundColor('transparent')
          .width(36)
          .height(36)
          .onClick(() => {
            this.isExpanded = !this.isExpanded;
          })
        }
        .width('100%')
        .padding({ left: 16, right: 16 })

        // 阶段进度条
        Stack() {
          Column()
            .width('100%')
            .height(4)
            .backgroundColor('rgba(255,255,255,0.1)')
            .borderRadius(2)

          Column()
            .width(`${this.getPhaseProgress() * 100}%`)
            .height(4)
            .backgroundColor(this.currentLightColor)
            .borderRadius(2)
            .shadow({
              radius: 4,
              color: this.currentLightColor,
              offsetX: 0,
              offsetY: 0
            })
            .animation({ duration: 500 })
        }
        .width('100%')
        .height(4)
        .padding({ left: 16, right: 16 })

        // 展开后的详细内容
        if (this.isExpanded) {
          this.buildExpandedContent()
        }

        // 总进度指示
        Row({ space: 8 }) {
          Text('总进度')
            .fontSize(11)
            .fontColor('rgba(255,255,255,0.4)')

          Stack() {
            Column()
              .width('100%')
              .height(2)
              .backgroundColor('rgba(255,255,255,0.1)')
              .borderRadius(1)

            Column()
              .width(`${this.getTotalProgress() * 100}%`)
              .height(2)
              .backgroundColor('rgba(255,255,255,0.4)')
              .borderRadius(1)
          }
          .width(60)
          .height(2)

          Text(`${Math.floor(this.getTotalProgress() * 100)}%`)
            .fontSize(11)
            .fontColor('rgba(255,255,255,0.4)')
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 12 })
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .width('92%')
      .backgroundColor('rgba(30, 30, 40, 0.8)')
      .backdropFilter($r('sys.blur.25'))
      .borderRadius(20)
      .border({
        width: 1,
        color: `${this.currentLightColor}33`
      })
      .shadow({
        radius: 20,
        color: 'rgba(0, 0, 0, 0.3)',
        offsetX: 0,
        offsetY: 4
      })
      .animation({
        duration: 300,
        curve: Curve.EaseInOut
      })
    }
    .width('100%')
    .height('auto')
    .position({ x: 0, y: 100 })
  }

  @Builder
  buildExpandedContent(): void {
    Column({ space: 12 }) {
      // 阶段列表
      List({ space: 8 }) {
        ForEach(this.phases, (phase: WorkoutPhase, index: number) => {
          ListItem() {
            Row({ space: 12 }) {
              // 阶段状态指示
              Column()
                .width(8)
                .height(8)
                .backgroundColor(
                  index < this.currentPhaseIndex ? '#2ECC71' :
                  index === this.currentPhaseIndex ? this.currentLightColor :
                  'rgba(255,255,255,0.2)'
                )
                .borderRadius(4)

              Column({ space: 2 }) {
                Text(phase.name)
                  .fontSize(14)
                  .fontColor(index === this.currentPhaseIndex ? '#FFFFFF' : 'rgba(255,255,255,0.6)')
                  .fontWeight(index === this.currentPhaseIndex ? FontWeight.Medium : FontWeight.Regular)

                Text(`${this.formatTime(phase.duration)} · 目标心率 ${phase.targetHeartRate} BPM`)
                  .fontSize(11)
                  .fontColor('rgba(255,255,255,0.4)')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              if (index < this.currentPhaseIndex) {
                Image($r('app.media.ic_check'))
                  .width(16)
                  .height(16)
                  .fillColor('#2ECC71')
              }
            }
            .width('100%')
            .padding(12)
            .backgroundColor(index === this.currentPhaseIndex ? 
              `${this.currentLightColor}15` : 'transparent')
            .borderRadius(12)
          }
        })
      }
      .width('100%')
      .height(200)
      .padding({ left: 12, right: 12 })

      // 当前阶段描述
      Text(this.phases[this.currentPhaseIndex]?.description ?? '')
        .fontSize(13)
        .fontColor('rgba(255,255,255,0.6)')
        .width('100%')
        .padding({ left: 16, right: 16 })
        .lineHeight(20)

      // 快捷操作
      Row({ space: 12 }) {
        Button('跳过此阶段')
          .width('48%')
          .height(40)
          .backgroundColor('rgba(255,255,255,0.05)')
          .fontColor('rgba(255,255,255,0.6)')
          .fontSize(13)
          .borderRadius(10)

        Button('延长30秒')
          .width('48%')
          .height(40)
          .backgroundColor(`${this.currentLightColor}30`)
          .fontColor(this.currentLightColor)
          .fontSize(13)
          .borderRadius(10)
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .animation({
      duration: 250,
      curve: Curve.EaseInOut
    })
  }

  private formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  }
}

四、主页面集成

4.1 运动主页面(WorkoutPage.ets)

typescript 复制代码
// entry/src/main/ets/pages/WorkoutPage.ets
import { MotionAwareFloatNav, MotionState } from '../components/MotionAwareFloatNav';
import { DataLightVisualization } from '../components/DataLightVisualization';
import { WorkoutFloatCard } from '../components/WorkoutFloatCard';

@Entry
@Component
struct WorkoutPage {
  @State currentTab: string = 'dashboard';
  @State isWorkoutActive: boolean = false;
  @State workoutStartTime: number = 0;
  @State currentPhaseIndex: number = 0;
  @State currentPhaseTime: number = 0;
  
  // 模拟运动数据
  @State fitnessData: {
    heartRate: number;
    heartRateVariability: number;
    calories: number;
    distance: number;
    pace: number;
    steps: number;
    duration: number;
  } = {
    heartRate: 75,
    heartRateVariability: 30,
    calories: 0,
    distance: 0,
    pace: 0,
    steps: 0,
    duration: 0
  };

  // 训练计划
  private workoutPlan = [
    {
      id: 'warmup',
      name: '热身',
      duration: 300,
      targetHeartRate: 110,
      description: '轻松慢跑,激活身体',
      color: '#2ECC71'
    },
    {
      id: 'sprint1',
      name: '冲刺训练 1',
      duration: 180,
      targetHeartRate: 160,
      description: '全力冲刺,保持高心率',
      color: '#FF6B6B'
    },
    {
      id: 'recovery1',
      name: '主动恢复',
      duration: 120,
      targetHeartRate: 130,
      description: '慢跑恢复,调整呼吸',
      color: '#4ECDC4'
    },
    {
      id: 'sprint2',
      name: '冲刺训练 2',
      duration: 180,
      targetHeartRate: 165,
      description: '再次冲刺,突破极限',
      color: '#FF6B6B'
    },
    {
      id: 'cooldown',
      name: '冷身',
      duration: 300,
      targetHeartRate: 100,
      description: '慢走放松,拉伸肌肉',
      color: '#9B59B6'
    }
  ];

  private dataTimer: number = -1;

  aboutToAppear(): void {
    this.startDataSimulation();
  }

  aboutToDisappear(): void {
    clearInterval(this.dataTimer);
  }

  private startDataSimulation(): void {
    this.dataTimer = setInterval(() => {
      if (this.isWorkoutActive) {
        // 更新运动数据
        this.fitnessData.duration++;
        this.fitnessData.calories += 0.15;
        this.fitnessData.distance += 2.5;
        this.fitnessData.steps += 2;
        this.fitnessData.pace = 6 + Math.random() * 2;
        this.fitnessData.heartRate = 75 + Math.sin(Date.now() / 10000) * 40 + Math.random() * 10;
        this.fitnessData.heartRateVariability = 20 + Math.random() * 30;
        
        // 更新阶段时间
        this.currentPhaseTime++;
        const currentPhase = this.workoutPlan[this.currentPhaseIndex];
        if (currentPhase && this.currentPhaseTime >= currentPhase.duration) {
          this.currentPhaseIndex++;
          this.currentPhaseTime = 0;
        }
      }
    }, 1000);
  }

  build() {
    Stack() {
      // 数据光效可视化层
      DataLightVisualization({
        fitnessData: this.fitnessData
      })

      // 训练计划卡片(运动中显示)
      if (this.isWorkoutActive) {
        WorkoutFloatCard({
          phases: this.workoutPlan,
          currentPhaseIndex: this.currentPhaseIndex,
          currentPhaseTime: this.currentPhaseTime
        })
      }

      // 主内容区
      Column() {
        if (this.currentTab === 'dashboard') {
          this.buildDashboard()
        } else if (this.currentTab === 'workout') {
          this.buildWorkoutView()
        } else if (this.currentTab === 'history') {
          this.buildHistoryView()
        }
      }
      .width('100%')
      .height('100%')
      .padding({ top: 40, bottom: 120 })

      // 运动感知悬浮导航
      MotionAwareFloatNav({
        onItemSelect: (item) => {
          this.currentTab = item.id;
        },
        onEmergencyStop: () => {
          this.isWorkoutActive = false;
        },
        onGesture: (gesture) => {
          console.info(`Gesture: ${gesture}`);
        }
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a0f')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }

  @Builder
  buildDashboard(): void {
    Column({ space: 24 }) {
      // 今日概览
      Text('今日运动')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')

      // 快速开始按钮
      Button() {
        Column({ space: 8 }) {
          Image($r('app.media.ic_play'))
            .width(40)
            .height(40)
            .fillColor('#FFFFFF')
          
          Text(this.isWorkoutActive ? '运动中' : '开始训练')
            .fontSize(18)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Medium)
        }
      }
      .width(200)
      .height(200)
      .backgroundColor(this.isWorkoutActive ? '#FF6B6B' : '#4ECDC4')
      .borderRadius(100)
      .shadow({
        radius: 30,
        color: this.isWorkoutActive ? '#FF6B6B' : '#4ECDC4',
        offsetX: 0,
        offsetY: 10
      })
      .onClick(() => {
        this.isWorkoutActive = !this.isWorkoutActive;
        if (this.isWorkoutActive) {
          this.workoutStartTime = Date.now();
        }
      })

      // 本周统计
      Row({ space: 16 }) {
        this.buildStatCard('本周运动', '3次', '#4ECDC4')
        this.buildStatCard('消耗热量', '1,240', '#F5A623')
        this.buildStatCard('运动时长', '4.5h', '#9B59B6')
      }
    }
    .width('100%')
    .height('100%')
    .padding(24)
  }

  @Builder
  buildWorkoutView(): void {
    Column({ space: 20 }) {
      Text('训练计划')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')

      List({ space: 12 }) {
        ForEach(this.workoutPlan, (phase, index) => {
          ListItem() {
            Row({ space: 12 }) {
              Column()
                .width(48)
                .height(48)
                .backgroundColor(`${phase.color}20`)
                .borderRadius(12)
                .border({
                  width: 2,
                  color: phase.color
                })

              Column({ space: 4 }) {
                Text(phase.name)
                  .fontSize(16)
                  .fontColor('#FFFFFF')
                  .fontWeight(FontWeight.Medium)

                Text(`${this.formatTime(phase.duration)} · ${phase.description}`)
                  .fontSize(13)
                  .fontColor('rgba(255,255,255,0.5)')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .width('100%')
            .padding(16)
            .backgroundColor('rgba(255,255,255,0.03)')
            .borderRadius(16)
          }
        })
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .padding(24)
  }

  @Builder
  buildHistoryView(): void {
    Column() {
      Text('历史记录')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
    }
    .width('100%')
    .height('100%')
    .padding(24)
  }

  @Builder
  buildStatCard(label: string, value: string, color: string): void {
    Column({ space: 8 }) {
      Text(value)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
        .fontVariant(FontVariant.TabNums)

      Text(label)
        .fontSize(12)
        .fontColor('rgba(255,255,255,0.5)')
    }
    .width(100)
    .height(100)
    .backgroundColor('rgba(255,255,255,0.03)')
    .backdropFilter($r('sys.blur.10'))
    .borderRadius(16)
    .justifyContent(FlexAlign.Center)
  }

  private formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  }
}

五、关键技术总结

5.1 运动状态-导航-光效协同矩阵

运动状态 心率区间 导航模式 光效特征 交互策略
静止 <100 完整展开 青绿慢呼吸 全功能可用
热身 100-120 侧边迷你 绿色温和脉冲 基础功能
燃脂 120-150 边缘光点 橙色活跃闪烁 手势优先
有氧 150-170 全隐藏 红色急促警示 仅手势
峰值 >170 仅紧急停止 深红警报 紧急优先
恢复 下降中 逐步浮现 舒缓渐变 逐步恢复

5.2 生理数据可视化映射

复制代码
心率 -> 光效颜色 + 脉冲速度
  └─ 静息(青) -> 热身(绿) -> 燃脂(橙) -> 有氧(红) -> 峰值(深红)

HRV -> 光效不规则性
  └─ 高HRV = 光效平滑规律
  └─ 低HRV = 光效抖动不规则

卡路里 -> 底部光晕累积亮度
  └─ 持续累积 = 金色光晕逐渐增强

步数 -> 涟漪扩散动画
  └─ 每100步 = 一次青色涟漪

5.3 性能与功耗优化

typescript 复制代码
// 光效性能优化
class LightEffectOptimizer {
  // 根据电池状态调整光效质量
  static adjustQuality(batteryLevel: number): void {
    if (batteryLevel < 20) {
      // 低电量:降低粒子数量,停止呼吸动画
      AppStorage.setOrCreate('light_quality', 'low');
      AppStorage.setOrCreate('particle_count', 20);
    } else if (batteryLevel < 50) {
      AppStorage.setOrCreate('light_quality', 'medium');
      AppStorage.setOrCreate('particle_count', 35);
    } else {
      AppStorage.setOrCreate('light_quality', 'high');
      AppStorage.setOrCreate('particle_count', 50);
    }
  }
  
  // 运动时降低UI刷新率以节省电量
  static setWorkoutRefreshRate(isWorkout: boolean): void {
    const rate = isWorkout ? 30 : 60; // 运动时30fps,静止时60fps
    // 调用系统API设置帧率
  }
}

六、调试与适配建议

  1. 传感器校准:不同设备的加速度计灵敏度不同,需在真机上校准运动检测阈值
  2. 心率设备兼容:测试多款HarmonyOS手表的心率数据同步延迟,优化光效同步
  3. 户外可视性:强光环境下增强光效对比度,或自动切换为高对比度模式
  4. 防水场景:游泳等水下运动时,导航应切换为物理按键模式,关闭触控

七、结语

HarmonyOS 6的悬浮导航与沉浸光感特性,为运动健康应用开发带来了从"数据展示"到"感官体验"的范式转变。通过"光影律动"的实战案例,我们展示了如何:

  • 构建运动感知的自适应导航,根据心率区间自动调整交互形态,防止运动中误触
  • 实现生理数据到光效的实时映射,将抽象数字转化为直观的视觉反馈
  • 利用玻璃拟态和动态光效创造沉浸式训练环境,提升运动专注度
  • 通过多设备传感器融合,打造"身体即界面"的自然交互体验

这些技术不仅提升了健身应用的专业性,更让科技真正服务于人的健康生活。期待HarmonyOS生态中的健康开发者们能够利用这些新特性,创造出更多激励用户、守护健康的优秀应用。


转载自:https://blog.csdn.net/u014727709/article/details/160311451

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
酣大智2 小时前
Win11 24H2 eNSP中AR报错40,解决方法
网络·华为
ICT系统集成阿祥2 小时前
黄金秘籍解决华为防火墙最困难的故障
网络·华为·php
酣大智4 小时前
eNSP中AR报错40,重新安装
网络·华为
weitingfu4 小时前
AI 游戏,为什么更适合鸿蒙?
人工智能·游戏·华为·ai·harmonyos
木斯佳4 小时前
鸿蒙开发入门指南:鸿蒙canvas实操——快速掌握自定义图表组件
harmonyos·自定义图表
光锥智能5 小时前
华为MateBook 14 鸿蒙版发布,体验全面升维
华为·harmonyos
UnicornDev5 小时前
【HarmonyOS 6】练习记录页面 UI 设计
ui·华为·harmonyos·arkts·鸿蒙
浮芷.5 小时前
生命科学数据视界防御:基于鸿蒙Flutter陀螺仪云台与三维体积光栅的视轴锁定架构
flutter·华为·架构·开源·harmonyos·鸿蒙
浮芷.6 小时前
微观搜打撤:基于鸿蒙flutter的内存快照算法的局内外状态隔离与高阶背包系统设计
算法·flutter·华为·开源·harmonyos·鸿蒙