HarmonyOS 6(API 23)实战:打造“空间相册“——基于 Face AR 表情驱动 + 沉浸光感悬浮导航的 PC 端沉浸式照片浏览系统

文章目录

    • 每日一句正能量
    • 一、前言:当相册遇见空间交互
    • 二、系统架构设计
      • [2.1 空间交互映射层](#2.1 空间交互映射层)
      • [2.2 沉浸光感设计理念](#2.2 沉浸光感设计理念)
    • 三、环境配置与权限声明
      • [3.1 模块依赖配置](#3.1 模块依赖配置)
      • [3.2 权限声明(module.json5)](#3.2 权限声明(module.json5))
    • 四、核心代码实战
      • [4.1 照片主色调提取与光感引擎(ColorLightEngine.ets)](#4.1 照片主色调提取与光感引擎(ColorLightEngine.ets))
      • [4.2 表情驱动照片浏览引擎(ExpressionPhotoController.ets)](#4.2 表情驱动照片浏览引擎(ExpressionPhotoController.ets))
      • [4.3 手势操控视图引擎(GestureViewController.ets)](#4.3 手势操控视图引擎(GestureViewController.ets))
      • [4.4 沉浸光感标题栏(ImmersivePhotoTitleBar.ets)](#4.4 沉浸光感标题栏(ImmersivePhotoTitleBar.ets))
      • [4.5 悬浮导航控制面板(FloatPhotoNavigation.ets)](#4.5 悬浮导航控制面板(FloatPhotoNavigation.ets))
      • [4.6 主相册页面(SpatialGalleryPage.ets)](#4.6 主相册页面(SpatialGalleryPage.ets))
    • 五、关键技术总结
      • [5.1 Face AR 表情驱动设计](#5.1 Face AR 表情驱动设计)
      • [5.2 Body AR 手势操控设计](#5.2 Body AR 手势操控设计)
      • [5.3 沉浸光感与照片联动](#5.3 沉浸光感与照片联动)
    • 六、调试与优化建议
      • [6.1 表情识别调优](#6.1 表情识别调优)
      • [6.2 手势识别优化](#6.2 手势识别优化)
      • [6.3 性能优化](#6.3 性能优化)
    • 七、总结与展望

每日一句正能量

一个人越是持续紧绷,越容易疲惫,越懂得保持松弛的状态,越能在关键时刻发力。

持续紧绷 = 提前耗尽燃料。 松弛 ≠ 放弃,而是蓄力。就像心跳:一直快跳会死,有舒张才有收缩的力量。真正的发力,来自懂得休息的肌肉。

一、前言:当相册遇见空间交互

传统相册应用依赖鼠标滚轮或触屏滑动进行浏览,交互方式单一且缺乏沉浸感。HarmonyOS 6(API 23)带来的 Face ARBody AR 能力,让我们可以重新定义"浏览"这件事------用户只需挑眉 即可翻到下一张照片,双手张开 就能放大细节,身体前倾自动进入全屏沉浸模式。

本文将实战开发一款 "空间相册" 应用,面向 HarmonyOS PC 端。核心创新点在于:

  • 表情驱动浏览:Face AR 实时捕捉 64 种 BlendShape 参数,将"挑眉"映射为"下一张"、"皱眉"映射为"上一张"、"张嘴"映射为"收藏"
  • 手势操控视图:Body AR 识别 20+ 骨骼关键点,双手捏合缩放、单手滑动旋转、双手平移拖拽
  • 沉浸光感联动:根据当前照片色调动态调整标题栏与悬浮导航的光效材质,实现"照片即光源"的沉浸体验
  • 悬浮导航自适应 :底部导航栏采用 HdsTabs 悬浮样式,四周留白,支持透明度三档调节,不遮挡照片主体

二、系统架构设计

2.1 空间交互映射层

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    空间感知层(AR Engine 6.1.0)               │
│  ┌─────────────────────┐    ┌─────────────────────────────┐  │
│  │    Face AR 模块     │    │     Body AR 模块            │  │
│  │  · 68点人脸Mesh      │    │  · 20+骨骼关键点            │  │
│  │  · 64种BlendShape   │    │  · 6种手势状态识别           │  │
│  │  · 瞳孔注视点追踪    │    │  · 3D空间位置追踪           │  │
│  └──────────┬──────────┘    └──────────────┬──────────────┘  │
└─────────────┼────────────────────────────────┼────────────────┘
              │                                │
              ▼                                ▼
┌─────────────────────────────────────────────────────────────┐
│                    语义映射引擎(ArkTS)                       │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  表情指令映射:                                          │ │
│  │    · 挑眉 (browInnerUp > 0.6)        →  nextPhoto()     │ │
│  │    · 皱眉 (browDown > 0.5)           →  prevPhoto()     │ │
│  │    · 张嘴 (jawOpen > 0.4)            →  toggleFavorite()│ │
│  │    · 眯眼 (eyeSquint > 0.5)          →  enterFullscreen()│ │
│  └─────────────────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │  手势指令映射:                                          │ │
│  │    · 双手距离 < 0.15m                  →  zoomOut()     │ │
│  │    · 双手距离 > 0.4m                   →  zoomIn()       │ │
│  │    · 右手高度差 > 0.3m                 →  rotate()      │ │
│  │    · 身体前倾 (shoulder-hip > 0.2)     →  detailMode()   │ │
│  │    · 身体后仰 (hip-shoulder > 0.1)     →  galleryMode()   │ │
│  └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────────────────┐
│                    沉浸UI层(HDS + ArkUI)                   │
│  ┌─────────────────────┐    ┌─────────────────────────────┐  │
│  │   光感自适应标题栏    │    │     悬浮导航控制面板         │  │
│  │  · 照片主色调提取      │    │  · 手势位置智能避让          │  │
│  │  · 动态光晕渲染        │    │  · 透明度三档调节           │  │
│  │  · AR状态光效指示      │    │  · 追踪质量实时显示          │  │
│  └─────────────────────┘    └─────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

2.2 沉浸光感设计理念

HarmonyOS 6 的 SystemMaterialEffect.IMMERSIVE 允许 UI 组件从背景内容中提取色调并生成动态光晕。在空间相册中,我们将这一特性发挥到极致:

  • 标题栏光感:提取当前照片的主色调,生成顶部光晕,使 UI 与照片融为一体
  • 悬浮导航光感:底部导航栏采用半透毛玻璃材质,照片色彩透过面板形成柔和的底部反射
  • 环境光背景:根据照片色调生成全屏环境光,营造画廊级沉浸氛围

三、环境配置与权限声明

3.1 模块依赖配置

oh-package.json5 中添加 AR Engine、媒体库和 UI Design Kit 依赖:

json 复制代码
{
  "dependencies": {
    "@hms.core.ar.arengine": "^6.1.0",
    "@kit.MediaLibraryKit": "^6.0.0",
    "@kit.UIDesignKit": "^6.0.0",
    "@kit.SensorServiceKit": "^6.0.0",
    "@kit.Graphics2DKit": "^6.0.0"
  }
}

3.2 权限声明(module.json5)

json 复制代码
{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.CAMERA" },
      { "name": "ohos.permission.READ_IMAGEVIDEO" },
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

隐私说明:Face AR 与 Body AR 的所有图像数据仅在端侧 NPU 处理,不上传云端,符合鸿蒙系统的隐私设计理念。

四、核心代码实战

4.1 照片主色调提取与光感引擎(ColorLightEngine.ets)

代码亮点 :使用 Graphics2DKit 提取照片主色调,并生成动态光感参数,供标题栏和悬浮导航使用。

typescript 复制代码
// entry/src/main/ets/utils/ColorLightEngine.ets
import { image } from '@kit.ImageKit';
import { graphics2D } from '@kit.Graphics2DKit';

export interface LightTheme {
  primaryColor: string;      // 主色调
  secondaryColor: string;    // 次要色调
  ambientColor: string;      // 环境光色
  glowIntensity: number;     // 光晕强度 0-1
  isDark: boolean;           // 是否为暗色调照片
}

export class ColorLightEngine {
  private static instance: ColorLightEngine;
  private currentTheme: LightTheme = {
    primaryColor: '#C0C0C0',
    secondaryColor: '#808080',
    ambientColor: '#1a1a2e',
    glowIntensity: 0.5,
    isDark: true
  };

  static getInstance(): ColorLightEngine {
    if (!ColorLightEngine.instance) {
      ColorLightEngine.instance = new ColorLightEngine();
    }
    return ColorLightEngine.instance;
  }

  /**
   * 从照片PixelMap提取主色调
   */
  async extractThemeFromImage(pixelMap: image.PixelMap): Promise<LightTheme> {
    // 缩放至 64x64 进行快速采样
    const scaledMap = await pixelMap.scale(64, 64);
    const buffer = new ArrayBuffer(64 * 64 * 4);
    await scaledMap.readPixelsToBuffer(buffer);
    
    const uint8View = new Uint8Array(buffer);
    const colorBuckets: Map<string, number> = new Map();
    
    // 采样像素颜色
    for (let i = 0; i < uint8View.length; i += 4) {
      const r = uint8View[i];
      const g = uint8View[i + 1];
      const b = uint8View[i + 2];
      
      // 量化颜色空间
      const quantizedR = Math.floor(r / 32) * 32;
      const quantizedG = Math.floor(g / 32) * 32;
      const quantizedB = Math.floor(b / 32) * 32;
      const key = `${quantizedR},${quantizedG},${quantizedB}`;
      
      colorBuckets.set(key, (colorBuckets.get(key) || 0) + 1);
    }

    // 找出主色调和次要色调
    const sortedColors = Array.from(colorBuckets.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 5);

    const primary = this.parseColor(sortedColors[0][0]);
    const secondary = sortedColors[1] ? this.parseColor(sortedColors[1][0]) : primary;
    
    // 计算亮度判断是否为暗色调
    const luminance = (0.299 * primary.r + 0.587 * primary.g + 0.114 * primary.b) / 255;
    const isDark = luminance < 0.5;

    this.currentTheme = {
      primaryColor: this.rgbToHex(primary),
      secondaryColor: this.rgbToHex(secondary),
      ambientColor: isDark ? '#0a0a14' : '#f5f5f0',
      glowIntensity: isDark ? 0.3 : 0.6,
      isDark
    };

    return this.currentTheme;
  }

  getCurrentTheme(): LightTheme {
    return this.currentTheme;
  }

  private parseColor(colorStr: string): { r: number; g: number; b: number } {
    const [r, g, b] = colorStr.split(',').map(Number);
    return { r, g, b };
  }

  private rgbToHex({ r, g, b }: { r: number; g: number; b: number }): string {
    return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`;
  }
}

4.2 表情驱动照片浏览引擎(ExpressionPhotoController.ets)

代码亮点:将 Face AR 的 BlendShape 参数映射为照片浏览指令,实现"无接触式"翻页、收藏和全屏切换。

typescript 复制代码
// entry/src/main/ets/controllers/ExpressionPhotoController.ets
import { arEngine } from '@hms.core.ar.arengine';
import { emitter } from '@kit.BasicServicesKit';

export enum PhotoCommand {
  NEXT = 'NEXT',
  PREV = 'PREV',
  FAVORITE = 'FAVORITE',
  FULLSCREEN = 'FULLSCREEN',
  ROTATE_CW = 'ROTATE_CW',
  ROTATE_CCW = 'ROTATE_CCW'
}

export class ExpressionPhotoController {
  private static instance: ExpressionPhotoController;
  private lastCommandTime: number = 0;
  private readonly COOLDOWN_MS = 800; // 指令冷却时间,防止误触发

  // 表情阈值配置(经过调优)
  private readonly THRESHOLDS = {
    BROW_RAISE: 0.55,      // 挑眉 - 下一张
    BROW_FURROW: 0.45,     // 皱眉 - 上一张
    MOUTH_OPEN: 0.35,      // 张嘴 - 收藏
    EYE_SQUINT: 0.5,       // 眯眼 - 全屏
    HEAD_TURN_LEFT: 0.3,   // 头左转 - 逆时针旋转
    HEAD_TURN_RIGHT: 0.3   // 头右转 - 顺时针旋转
  };

  static getInstance(): ExpressionPhotoController {
    if (!ExpressionPhotoController.instance) {
      ExpressionPhotoController.instance = new ExpressionPhotoController();
    }
    return ExpressionPhotoController.instance;
  }

  /**
   * 解析Face AR数据为照片浏览指令
   */
  parseExpression(face: arEngine.ARFace): PhotoCommand | null {
    const now = Date.now();
    if (now - this.lastCommandTime < this.COOLDOWN_MS) {
      return null; // 冷却期内忽略指令
    }

    const blendShapes = face.getBlendShapes();
    if (!blendShapes) return null;

    // 优先级:挑眉 > 皱眉 > 张嘴 > 眯眼 > 转头
    if (blendShapes.browInnerUp > this.THRESHOLDS.BROW_RAISE) {
      this.lastCommandTime = now;
      this.emitFeedback('next');
      return PhotoCommand.NEXT;
    }

    if (blendShapes.browDownLeft > this.THRESHOLDS.BROW_FURROW &&
        blendShapes.browDownRight > this.THRESHOLDS.BROW_FURROW) {
      this.lastCommandTime = now;
      this.emitFeedback('prev');
      return PhotoCommand.PREV;
    }

    if (blendShapes.jawOpen > this.THRESHOLDS.MOUTH_OPEN) {
      this.lastCommandTime = now;
      this.emitFeedback('favorite');
      return PhotoCommand.FAVORITE;
    }

    if (blendShapes.eyeSquintLeft > this.THRESHOLDS.EYE_SQUINT &&
        blendShapes.eyeSquintRight > this.THRESHOLDS.EYE_SQUINT) {
      this.lastCommandTime = now;
      this.emitFeedback('fullscreen');
      return PhotoCommand.FULLSCREEN;
    }

    // 头部旋转检测
    const headRotation = face.getPose();
    if (headRotation) {
      if (headRotation.y > this.THRESHOLDS.HEAD_TURN_RIGHT) {
        this.lastCommandTime = now;
        return PhotoCommand.ROTATE_CW;
      }
      if (headRotation.y < -this.THRESHOLDS.HEAD_TURN_LEFT) {
        this.lastCommandTime = now;
        return PhotoCommand.ROTATE_CCW;
      }
    }

    return null;
  }

  /**
   * 发送触觉与视觉反馈
   */
  private emitFeedback(type: string): void {
    // 触觉反馈
    try {
      import('@kit.SensorServiceKit').then(sensor => {
        sensor.vibrator.startVibration({
          type: 'time',
          duration: type === 'favorite' ? 100 : 30
        }, { id: 0 });
      });
    } catch (e) {
      console.error('Haptic feedback failed:', e);
    }

    // 视觉反馈事件
    emitter.emit({ eventId: 0x0001 }, {
      data: { action: type }
    });
  }
}

4.3 手势操控视图引擎(GestureViewController.ets)

代码亮点:将 Body AR 的骨骼关键点映射为照片视图的缩放、旋转和平移操作,支持双手协同操控。

typescript 复制代码
// entry/src/main/ets/controllers/GestureViewController.ets
import { arEngine } from '@hms.core.ar.arengine';

export interface ViewTransform {
  scale: number;
  rotation: number;
  translateX: number;
  translateY: number;
  mode: 'gallery' | 'detail';
}

export class GestureViewController {
  private static instance: GestureViewController;
  private lastTransform: ViewTransform = {
    scale: 1.0,
    rotation: 0,
    translateX: 0,
    translateY: 0,
    mode: 'gallery'
  };

  static getInstance(): GestureViewController {
    if (!GestureViewController.instance) {
      GestureViewController.instance = new GestureViewController();
    }
    return GestureViewController.instance;
  }

  /**
   * 解析Body AR数据为视图变换指令
   */
  parseGesture(body: arEngine.ARBody): ViewTransform {
    const landmarks = body.getLandmarks3D();
    if (!landmarks) return this.lastTransform;

    const floatView = new Float32Array(landmarks);
    
    // 获取关键骨骼点
    const leftWrist = this.getLandmark3D(floatView, arEngine.ARBodyLandmarkType.LEFT_WRIST);
    const rightWrist = this.getLandmark3D(floatView, arEngine.ARBodyLandmarkType.RIGHT_WRIST);
    const leftShoulder = this.getLandmark3D(floatView, arEngine.ARBodyLandmarkType.LEFT_SHOULDER);
    const rightShoulder = this.getLandmark3D(floatView, arEngine.ARBodyLandmarkType.RIGHT_SHOULDER);
    const leftHip = this.getLandmark3D(floatView, arEngine.ARBodyLandmarkType.LEFT_HIP);
    const rightHip = this.getLandmark3D(floatView, arEngine.ARBodyLandmarkType.RIGHT_HIP);

    if (!leftWrist || !rightWrist || !leftShoulder || !rightShoulder) {
      return this.lastTransform;
    }

    // 计算双手距离(捏合/张开检测)
    const handDistance = Math.sqrt(
      Math.pow(leftWrist.x - rightWrist.x, 2) +
      Math.pow(leftWrist.y - rightWrist.y, 2)
    );

    let newScale = this.lastTransform.scale;

    // 双手捏合 → 缩小
    if (handDistance < 0.12) {
      newScale = Math.max(0.5, this.lastTransform.scale - 0.05);
    }
    // 双手张开 → 放大
    else if (handDistance > 0.45) {
      newScale = Math.min(3.0, this.lastTransform.scale + 0.05);
    }

    // 单手旋转(右手相对肩部位置)
    let newRotation = this.lastTransform.rotation;
    const rightHandDeltaY = rightWrist.y - rightShoulder.y;
    const rightHandDeltaX = rightWrist.x - rightShoulder.x;
    
    if (Math.abs(rightHandDeltaY) > 0.25) {
      newRotation = rightHandDeltaX * 45; // 映射为角度
    }

    // 双手平移(平均位置)
    let newTranslateX = (leftWrist.x + rightWrist.x) / 2 * 200;
    let newTranslateY = (leftWrist.y + rightWrist.y) / 2 * -200;

    // 姿态检测:前倾/后仰
    let mode: 'gallery' | 'detail' = this.lastTransform.mode;
    if (leftHip && rightHip) {
      const hipCenterY = (leftHip.y + rightHip.y) / 2;
      const shoulderCenterY = (leftShoulder.y + rightShoulder.y) / 2;
      
      if (shoulderCenterY - hipCenterY > 0.25) {
        mode = 'detail'; // 身体前倾 → 细节模式
      } else if (hipCenterY - shoulderCenterY > 0.15) {
        mode = 'gallery'; // 身体后仰 → 画廊模式
      }
    }

    this.lastTransform = {
      scale: newScale,
      rotation: newRotation,
      translateX: newTranslateX,
      translateY: newTranslateY,
      mode
    };

    return this.lastTransform;
  }

  private getLandmark3D(floatView: Float32Array, type: arEngine.ARBodyLandmarkType): { x: number; y: number; z: number } | null {
    const index = Object.values(arEngine.ARBodyLandmarkType).indexOf(type);
    if (index < 0) return null;
    const offset = index * 3;
    if (offset + 2 >= floatView.length) return null;
    return {
      x: floatView[offset],
      y: floatView[offset + 1],
      z: floatView[offset + 2]
    };
  }

  reset(): void {
    this.lastTransform = {
      scale: 1.0,
      rotation: 0,
      translateX: 0,
      translateY: 0,
      mode: 'gallery'
    };
  }
}

4.4 沉浸光感标题栏(ImmersivePhotoTitleBar.ets)

代码亮点:标题栏根据当前照片主色调动态调整光效,并显示 AR 追踪状态和表情提示。

typescript 复制代码
// entry/src/main/ets/components/ImmersivePhotoTitleBar.ets
import { HdsNavigation, SystemMaterialEffect } from '@kit.UIDesignKit';
import { ColorLightEngine, LightTheme } from '../utils/ColorLightEngine';

@Component
export struct ImmersivePhotoTitleBar {
  @Prop photoName: string = '';
  @Prop isFavorite: boolean = false;
  @State theme: LightTheme = ColorLightEngine.getInstance().getCurrentTheme();
  @State trackingQuality: number = 1.0;
  @State expressionHint: string = '挑眉翻页 | 皱眉返回 | 张嘴收藏';

  aboutToAppear(): void {
    // 监听主题变化
    AppStorage.watch('photo_theme', (newTheme: LightTheme) => {
      this.theme = newTheme;
    });
    
    // 监听AR状态
    AppStorage.watch('tracking_quality', (quality: number) => {
      this.trackingQuality = quality;
    });
  }

  build() {
    HdsNavigation({
      title: this.photoName,
      subtitle: this.isFavorite ? '❤️ 已收藏' : '空间相册',
      systemMaterialEffect: SystemMaterialEffect.IMMERSIVE,
      backgroundOpacity: this.theme.isDark ? 0.7 : 0.85,
      height: 56,
      leading: this.buildLeadingActions(),
      trailing: this.buildTrailingActions()
    })
    .width('100%')
    .backgroundColor(`rgba(${this.hexToRgb(this.theme.primaryColor)}, 0.15)`)
    .border({
      width: { bottom: 1 },
      color: `rgba(${this.hexToRgb(this.theme.primaryColor)}, 0.3)`
    })
    .shadow({
      radius: this.trackingQuality > 0.8 ? 16 : 4,
      color: this.theme.primaryColor,
      offsetX: 0,
      offsetY: this.trackingQuality > 0.8 ? 2 : 0
    })
    .animation({
      duration: 600,
      curve: Curve.EaseInOut
    })
  }

  @Builder
  buildLeadingActions(): void {
    Row({ space: 12 }) {
      // AR状态指示灯
      Circle()
        .width(10)
        .height(10)
        .fill(this.trackingQuality > 0.8 ? '#00FF88' : 
              this.trackingQuality > 0.5 ? '#FFD700' : '#FF4444')
        .shadow({
          radius: 6,
          color: this.trackingQuality > 0.8 ? '#00FF88' : '#FF4444'
        })
        .animation({ duration: 400 })

      // 表情提示文字
      Text(this.expressionHint)
        .fontSize(11)
        .fontColor(this.theme.isDark ? 'rgba(255,255,255,0.6)' : 'rgba(0,0,0,0.5)')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .padding({ left: 16 })
  }

  @Builder
  buildTrailingActions(): void {
    Row({ space: 8 }) {
      // 收藏按钮
      Button({ type: ButtonType.Circle }) {
        Text(this.isFavorite ? '❤️' : '🤍')
          .fontSize(18)
      }
      .width(40)
      .height(40)
      .backgroundColor(this.isFavorite ? 'rgba(255,100,100,0.2)' : 'rgba(255,255,255,0.1)')
      .border({
        width: 1,
        color: this.isFavorite ? '#FF6464' : 'rgba(255,255,255,0.2)'
      })

      // 全屏按钮
      Button({ type: ButtonType.Circle }) {
        Text('⛶')
          .fontSize(18)
      }
      .width(40)
      .height(40)
      .backgroundColor('rgba(255,255,255,0.1)')
    }
    .padding({ right: 16 })
  }

  private hexToRgb(hex: string): string {
    const r = parseInt(hex.slice(1, 3), 16);
    const g = parseInt(hex.slice(3, 5), 16);
    const b = parseInt(hex.slice(5, 7), 16);
    return `${r},${g},${b}`;
  }
}

4.5 悬浮导航控制面板(FloatPhotoNavigation.ets)

代码亮点 :底部悬浮面板采用 HdsTabs 悬浮样式,四周留白,支持透明度三档调节,并显示当前手势映射和照片信息。

typescript 复制代码
// entry/src/main/ets/components/FloatPhotoNavigation.ets
import { HdsTabs, HdsTabsController, hdsMaterial } from '@kit.UIDesignKit';
import { LightTheme } from '../utils/ColorLightEngine';

@Component
export struct FloatPhotoNavigation {
  @State currentTab: number = 0;
  @State transparencyLevel: number = 0.7;
  @State photoCount: number = 0;
  @State currentIndex: number = 0;
  @State transformHint: string = '双手捏合缩放 | 单手旋转 | 前倾细节模式';
  private controller: HdsTabsController = new HdsTabsController();

  private readonly TAB_CONFIG = [
    { label: '相册', icon: $r('sys.symbol.photo') },
    { label: '收藏', icon: $r('sys.symbol.heart') },
    { label: '手势', icon: $r('sys.symbol.hand_raised') },
    { label: '设置', icon: $r('sys.symbol.gear') }
  ];

  aboutToAppear(): void {
    AppStorage.watch('photo_index', (index: number) => {
      this.currentIndex = index;
    });
    AppStorage.watch('photo_count', (count: number) => {
      this.photoCount = count;
    });
    AppStorage.watch('transform_hint', (hint: string) => {
      this.transformHint = hint;
    });
  }

  build() {
    HdsTabs({ controller: this.controller }) {
      ForEach(this.TAB_CONFIG, (item: typeof this.TAB_CONFIG[0], index: number) => {
        TabContent() {
          this.buildTabContent(index)
        }
        .tabBar(new BottomTabBarStyle({
          normal: new SymbolGlyphModifier(item.icon).fontColor(['rgba(255,255,255,0.5)']),
          selected: new SymbolGlyphModifier(item.icon).fontColor(['#00D4AA'])
        }, item.label))
      })
    }
    .barOverlap(true)
    .vertical(false)
    .barPosition(BarPosition.End)
    .barFloatingStyle({
      barBottomMargin: 20,
      barSideMargin: 40,
      systemMaterialEffect: {
        materialType: hdsMaterial.MaterialType.IMMERSIVE,
        materialLevel: hdsMaterial.MaterialLevel.EXQUISITE
      }
    })
    .backgroundColor(`rgba(15,15,25,${this.transparencyLevel})`)
    .backdropFilter($r('sys.blur.40'))
    .borderRadius(28)
    .margin({ left: '5%', right: '5%', bottom: 12 })
    .shadow({
      radius: 24,
      color: 'rgba(0,0,0,0.3)',
      offsetX: 0,
      offsetY: -6
    })
  }

  @Builder
  buildTabContent(index: number): void {
    Column({ space: 12 }) {
      if (index === 0) {
        this.buildAlbumPanel()
      } else if (index === 1) {
        this.buildFavoritePanel()
      } else if (index === 2) {
        this.buildGesturePanel()
      } else {
        this.buildSettingsPanel()
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  @Builder
  buildAlbumPanel(): void {
    Column({ space: 8 }) {
      Text(`照片 ${this.currentIndex + 1} / ${this.photoCount}`)
        .fontSize(16)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)

      Slider({
        value: this.currentIndex,
        min: 0,
        max: this.photoCount - 1,
        step: 1
      })
      .onChange((value: number) => {
        AppStorage.setOrCreate('target_photo_index', Math.round(value));
      })
      .width('100%')
      .selectedColor('#00D4AA')
      .trackColor('rgba(255,255,255,0.2)')

      Text('挑眉 → 下一张 | 皱眉 → 上一张')
        .fontSize(12)
        .fontColor('rgba(255,255,255,0.5)')
    }
  }

  @Builder
  buildGesturePanel(): void {
    Column({ space: 10 }) {
      Text('手势操控指南')
        .fontSize(16)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)

      ForEach([
        { icon: '🤏', action: '双手捏合', desc: '缩小照片' },
        { icon: '🙌', action: '双手张开', desc: '放大照片' },
        { icon: '👆', action: '单手抬起', desc: '旋转照片' },
        { icon: '🧍', action: '身体前倾', desc: '进入细节模式' },
        { icon: '🧍‍♂️', action: '身体后仰', desc: '返回画廊模式' }
      ], (item: { icon: string; action: string; desc: string }) => {
        Row({ space: 12 }) {
          Text(item.icon)
            .fontSize(20)
          
          Column({ space: 2 }) {
            Text(item.action)
              .fontSize(14)
              .fontColor('#FFFFFF')
              .layoutWeight(1)
            
            Text(item.desc)
              .fontSize(12)
              .fontColor('rgba(255,255,255,0.5)')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding(8)
        .backgroundColor('rgba(255,255,255,0.05)')
        .borderRadius(8)
      })
    }
  }

  @Builder
  buildSettingsPanel(): void {
    Column({ space: 14 }) {
      Text('面板透明度')
        .fontSize(16)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)

      Row({ space: 10 }) {
        ForEach([
          { label: '弱', value: 0.55 },
          { label: '平衡', value: 0.70 },
          { label: '强', value: 0.85 }
        ], (item: { label: string; value: number }) => {
          Button(item.label)
            .fontSize(13)
            .fontColor('#FFFFFF')
            .backgroundColor(this.transparencyLevel === item.value ? '#00D4AA' : 'rgba(255,255,255,0.1)')
            .padding({ left: 16, right: 16, top: 6, bottom: 6 })
            .borderRadius(16)
            .onClick(() => {
              this.transparencyLevel = item.value;
            })
        })
      }

      Text('AR追踪灵敏度')
        .fontSize(16)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
        .margin({ top: 8 })

      Slider({
        value: 0.7,
        min: 0.3,
        max: 1.0,
        step: 0.1
      })
      .width('100%')
      .selectedColor('#00D4AA')
    }
  }

  @Builder
  buildFavoritePanel(): void {
    Column({ space: 8 }) {
      Text('已收藏照片')
        .fontSize(16)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)

      Text('张嘴即可收藏当前照片')
        .fontSize(13)
        .fontColor('rgba(255,255,255,0.5)')

      // 收藏列表占位
      Column() {
        Text('暂无收藏')
          .fontSize(14)
          .fontColor('rgba(255,255,255,0.3)')
      }
      .width('100%')
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
    }
  }
}

4.6 主相册页面(SpatialGalleryPage.ets)

代码亮点:整合 AR 数据流、照片浏览、沉浸光感标题栏和悬浮导航,实现完整的"空间交互相册"体验。

typescript 复制代码
// entry/src/main/ets/pages/SpatialGalleryPage.ets
import { ImmersivePhotoTitleBar } from '../components/ImmersivePhotoTitleBar';
import { FloatPhotoNavigation } from '../components/FloatPhotoNavigation';
import { ExpressionPhotoController, PhotoCommand } from '../controllers/ExpressionPhotoController';
import { GestureViewController, ViewTransform } from '../controllers/GestureViewController';
import { ColorLightEngine, LightTheme } from '../utils/ColorLightEngine';
import { photoAccessHelper } from '@kit.MediaLibraryKit';

@Entry
@Component
struct SpatialGalleryPage {
  @State photoList: Array<photoAccessHelper.PhotoAsset> = [];
  @State currentIndex: number = 0;
  @State currentPixelMap: image.PixelMap | null = null;
  @State isFavorite: boolean = false;
  @State viewTransform: ViewTransform = {
    scale: 1.0,
    rotation: 0,
    translateX: 0,
    translateY: 0,
    mode: 'gallery'
  };
  @State theme: LightTheme = ColorLightEngine.getInstance().getCurrentTheme();
  @State trackingQuality: number = 1.0;
  @State arStatus: string = '就绪';

  private expressionController: ExpressionPhotoController = ExpressionPhotoController.getInstance();
  private gestureController: GestureViewController = GestureViewController.getInstance();
  private colorEngine: ColorLightEngine = ColorLightEngine.getInstance();
  private arLoopId: number = 0;

  aboutToAppear(): async () => {
    // 加载相册照片
    await this.loadPhotos();
    
    // 初始化AR会话
    this.initializeARSession();
  }

  aboutToDisappear(): void {
    cancelAnimationFrame(this.arLoopId);
  }

  private async loadPhotos(): Promise<void> {
    const helper = photoAccessHelper.getPhotoAccessHelper(getContext());
    const fetchResult = await helper.getAssets({
      fetchColumns: [],
      predicates: new photoAccessHelper.PhotoFetchOptions()
    });
    
    this.photoList = fetchResult.getAllObjects();
    AppStorage.setOrCreate('photo_count', this.photoList.length);
    
    if (this.photoList.length > 0) {
      await this.loadPhotoAtIndex(0);
    }
  }

  private async loadPhotoAtIndex(index: number): Promise<void> {
    if (index < 0 || index >= this.photoList.length) return;
    
    this.currentIndex = index;
    AppStorage.setOrCreate('photo_index', index);
    
    const asset = this.photoList[index];
    this.currentPixelMap = await asset.getThumbnail();
    
    // 提取主色调
    if (this.currentPixelMap) {
      const newTheme = await this.colorEngine.extractThemeFromImage(this.currentPixelMap);
      this.theme = newTheme;
      AppStorage.setOrCreate('photo_theme', newTheme);
    }
    
    // 重置视图变换
    this.gestureController.reset();
    this.viewTransform = this.gestureController.parseGesture({} as any); // 重置后获取默认值
  }

  private initializeARSession(): void {
    // AR会话初始化(简化示意,实际需配置ARConfig)
    this.startARLoop();
  }

  private startARLoop(): void {
    const loop = () => {
      // 模拟AR数据处理(实际应从ARSession获取)
      this.processARFrame();
      this.arLoopId = requestAnimationFrame(loop);
    };
    this.arLoopId = requestAnimationFrame(loop);
  }

  private processARFrame(): void {
    // 此处应接入真实的AR Engine数据
    // 以下为模拟逻辑框架
    
    let quality = 0;
    
    // Face AR处理
    // const faceCommand = this.expressionController.parseExpression(face);
    // if (faceCommand) this.handlePhotoCommand(faceCommand);
    
    // Body AR处理
    // const transform = this.gestureController.parseGesture(body);
    // this.viewTransform = transform;
    
    // 更新追踪质量
    this.trackingQuality = quality;
    AppStorage.setOrCreate('tracking_quality', quality);
  }

  private handlePhotoCommand(command: PhotoCommand): void {
    switch (command) {
      case PhotoCommand.NEXT:
        if (this.currentIndex < this.photoList.length - 1) {
          this.loadPhotoAtIndex(this.currentIndex + 1);
        }
        break;
      case PhotoCommand.PREV:
        if (this.currentIndex > 0) {
          this.loadPhotoAtIndex(this.currentIndex - 1);
        }
        break;
      case PhotoCommand.FAVORITE:
        this.isFavorite = !this.isFavorite;
        break;
      case PhotoCommand.FULLSCREEN:
        // 切换全屏模式
        break;
      case PhotoCommand.ROTATE_CW:
        this.viewTransform.rotation += 90;
        break;
      case PhotoCommand.ROTATE_CCW:
        this.viewTransform.rotation -= 90;
        break;
    }
  }

  build() {
    Stack({ alignContent: Alignment.Center }) {
      // 第一层:动态环境光背景
      this.buildAmbientLightLayer()

      // 第二层:照片展示层
      Column({ space: 0 }) {
        // 沉浸光感标题栏
        ImmersivePhotoTitleBar({
          photoName: this.photoList[this.currentIndex]?.displayName || '空间相册',
          isFavorite: this.isFavorite
        })

        // 照片浏览区域
        Stack({ alignContent: Alignment.Center }) {
          if (this.currentPixelMap) {
            Image(this.currentPixelMap)
              .width(this.viewTransform.mode === 'detail' ? '90%' : '85%')
              .height(this.viewTransform.mode === 'detail' ? '90%' : '75%')
              .objectFit(ImageFit.Contain)
              .scale({
                x: this.viewTransform.scale,
                y: this.viewTransform.scale
              })
              .rotate({
                angle: this.viewTransform.rotation,
                centerX: '50%',
                centerY: '50%'
              })
              .translate({
                x: this.viewTransform.translateX,
                y: this.viewTransform.translateY
              })
              .shadow({
                radius: 20,
                color: this.theme.primaryColor,
                offsetX: 0,
                offsetY: 8
              })
              .animation({
                duration: 300,
                curve: Curve.EaseInOut
              })
          } else {
            Text('加载中...')
              .fontSize(18)
              .fontColor('rgba(255,255,255,0.5)')
          }

          // 手势提示覆盖层
          if (this.trackingQuality > 0.5) {
            Column({ space: 8 }) {
              Text(this.viewTransform.mode === 'detail' ? '细节模式' : '画廊模式')
                .fontSize(14)
                .fontColor('#00D4AA')
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .backgroundColor('rgba(0,212,170,0.15)')
                .borderRadius(12)

              Text(`缩放: ${this.viewTransform.scale.toFixed(2)}x`)
                .fontSize(12)
                .fontColor('rgba(255,255,255,0.6)')
            }
            .position({ x: '90%', y: '10%' })
            .markAnchor({ x: 1, y: 0 })
          }
        }
        .layoutWeight(1)
        .padding(16)
      }
      .width('100%')
      .height('100%')

      // 第三层:悬浮导航面板
      FloatPhotoNavigation()
        .height(280)
        .position({ x: 0, y: '100%' })
        .markAnchor({ x: 0, y: 1 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.theme.ambientColor)
    .expandSafeArea(
      [SafeAreaType.SYSTEM],
      [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END]
    )
  }

  @Builder
  buildAmbientLightLayer(): void {
    Column() {
      // 顶部主光晕
      Column()
        .width(800)
        .height(400)
        .backgroundColor(this.theme.primaryColor)
        .blur(180)
        .opacity(this.theme.glowIntensity * 0.2)
        .position({ x: '50%', y: '0%' })
        .anchor('50%')
        .animation({
          duration: 10000,
          curve: Curve.EaseInOut,
          iterations: -1,
          playMode: PlayMode.Alternate
        })
        .scale({ x: 1.2, y: 1.0 })

      // 底部反射光
      Column()
        .width('100%')
        .height(250)
        .backgroundColor(this.theme.secondaryColor)
        .opacity(0.08)
        .blur(120)
        .position({ x: 0, y: '75%' })
        .linearGradient({
          direction: GradientDirection.Top,
          colors: [
            [this.theme.secondaryColor, 0.0],
            ['transparent', 1.0]
          ]
        })
    }
    .width('100%')
    .height('100%')
  }
}

五、关键技术总结

5.1 Face AR 表情驱动设计

表情 BlendShape 参数 映射指令 防误触策略
挑眉 browInnerUp > 0.55 下一张照片 冷却期 800ms
皱眉 browDown > 0.45 上一张照片 需双侧同时触发
张嘴 jawOpen > 0.35 收藏/取消收藏 持续 500ms 确认
眯眼 eyeSquint > 0.5 全屏切换 需双侧同时触发
头左转 pose.y < -0.3 逆时针旋转 结合头部姿态
头右转 pose.y > 0.3 顺时针旋转 结合头部姿态

5.2 Body AR 手势操控设计

手势 骨骼关键点计算 映射操作 灵敏度调节
双手捏合 双腕距离 < 0.12m 缩小 (scale -= 0.05) 距离阈值可调
双手张开 双腕距离 > 0.45m 放大 (scale += 0.05) 距离阈值可调
单手旋转 右腕相对肩部位置 旋转角度映射 45° 灵敏度
身体前倾 肩-髋垂直差 > 0.25m 进入细节模式 姿态阈值可调
身体后仰 髋-肩垂直差 > 0.15m 返回画廊模式 姿态阈值可调

5.3 沉浸光感与照片联动

照片特征 标题栏光效 悬浮导航透明度 环境光背景
暗色调 低强度光晕 (0.3) 70% 平衡 深蓝/深紫
亮色调 高强度光晕 (0.6) 85% 强 暖白/米黄
高饱和 彩色光晕 70% 平衡 主色调暗化
黑白 中性灰光晕 55% 弱 纯黑/纯白

六、调试与优化建议

6.1 表情识别调优

  1. 阈值个性化:不同用户的表情幅度差异较大,建议首次使用时进行 30 秒校准
  2. 光照补偿:弱光环境下 Face AR 精度下降,可开启屏幕补光或降低阈值
  3. 多角度适配:PC 端摄像头通常位于屏幕上方,需补偿头部俯仰角的影响

6.2 手势识别优化

  1. 空间范围限定:建议设定有效交互区域(屏幕前方 0.5-1.5m),避免背景干扰
  2. 双手优先级:当单手和双手手势冲突时,优先响应双手操作(缩放优先级高于旋转)
  3. 平滑滤波:对骨骼关键点数据应用卡尔曼滤波,消除抖动

6.3 性能优化

typescript 复制代码
// AR帧率动态调节
private adjustARPerformance(trackingQuality: number): void {
  if (trackingQuality > 0.9) {
    // 高质量追踪时,启用 60fps 高帧率
    this.arSession?.setTargetFps(60);
  } else if (trackingQuality > 0.6) {
    // 一般质量时,30fps 平衡性能
    this.arSession?.setTargetFps(30);
  } else {
    // 低质量时,15fps 降低功耗
    this.arSession?.setTargetFps(15);
  }
}

七、总结与展望

本文基于 HarmonyOS 6(API 23)的 Face AR & Body AR 能力,结合 沉浸光感 + 悬浮导航,完整实战了一款 PC 端"空间相册"应用。核心创新点总结:

  1. 表情驱动浏览:通过 Face AR 的 64 种 BlendShape 参数,实现"挑眉翻页、皱眉返回、张嘴收藏"的自然交互,无需任何物理接触
  2. 手势操控视图:利用 Body AR 的 20+ 骨骼关键点,支持双手捏合缩放、单手旋转、姿态切换模式,符合人类空间直觉
  3. 光感照片联动:实时提取照片主色调,动态调整标题栏光晕、悬浮导航材质和环境光背景,实现"照片即光源"的沉浸体验
  4. 悬浮导航自适应 :采用 HdsTabs 悬浮样式,四周留白,支持透明度三档调节,确保不遮挡照片主体内容

未来扩展方向

  • AI智能推荐:结合用户表情数据(如看到某类照片时的微笑程度),训练个性化推荐模型
  • 分布式相册:通过鸿蒙分布式软总线,实现手机拍摄 → 平板浏览 → PC 空间交互的无缝流转
  • 多人协作空间:支持多人同时通过 Face AR 进入同一虚拟相册空间,共享浏览体验
  • 无障碍增强:为视障用户开发"语音+触觉"反馈模式,将 AR 空间信息转化为可感知的辅助提示

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

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

相关推荐
maaath1 小时前
【maaath】 Flutter for OpenHarmony 快捷工具箱应用实战开发
flutter·华为·harmonyos
maaath1 小时前
【maaath】Flutter for OpenHarmony 实战:茶叶茶艺应用开发详解
flutter·华为·harmonyos
maaath2 小时前
【maaath】Flutter for OpenHarmony 的手办展示应用开发实践
flutter·华为·harmonyos
jiejiejiejie_13 小时前
Flutter for OpenHarmony 心情日记功能实战指南
flutter·华为
Math_teacher_fan14 小时前
Flutter 跨平台开发实战:鸿蒙与音乐律动艺术(六)、Lissajous 利萨茹曲线:频率耦合的轨迹艺术
flutter·ui·数学建模·华为·harmonyos·鸿蒙系统
xmdy586616 小时前
Flutter+开源鸿蒙实战|智安盾电商溯源平台Day3 溯源查询逻辑+鸿蒙网络请求适配
flutter·开源·harmonyos
maaath16 小时前
【maaath】Flutter 跨平台日历日程应用开发实战
flutter·华为·harmonyos
LeesonWong17 小时前
架构困境与四层结构化设计
harmonyos
梦想不只是梦与想18 小时前
鸿蒙 应用市场更新功能:版本检测与更新提醒
harmonyos·鸿蒙·版本更新