文章目录
-
- 每日一句正能量
- 前言
- 一、前言:电商直播的UI与交互革新需求
- 二、核心特性解析与技术选型
-
- [2.1 沉浸光感在直播场景中的价值](#2.1 沉浸光感在直播场景中的价值)
- [2.2 Face AR在直播中的创新应用](#2.2 Face AR在直播中的创新应用)
- [2.3 Body AR在商品展示中的创新应用](#2.3 Body AR在商品展示中的创新应用)
- 三、环境配置与权限声明
-
- [3.1 模块依赖配置](#3.1 模块依赖配置)
- [3.2 权限声明(module.json5)](#3.2 权限声明(module.json5))
- 四、核心代码实战
-
- [4.1 品类感知光感引擎(CategoryLightEngine.ets)](#4.1 品类感知光感引擎(CategoryLightEngine.ets))
- [4.2 Face AR虚拟试妆引擎(ARMakeupEngine.ets)](#4.2 Face AR虚拟试妆引擎(ARMakeupEngine.ets))
- [4.3 Body AR手势商品操控引擎(ARGestureProductEngine.ets)](#4.3 Body AR手势商品操控引擎(ARGestureProductEngine.ets))
- [4.4 沉浸光感标题栏(ImmersiveLiveTitleBar.ets)](#4.4 沉浸光感标题栏(ImmersiveLiveTitleBar.ets))
- [4.5 悬浮导航控制面板(FloatLiveNavigation.ets)](#4.5 悬浮导航控制面板(FloatLiveNavigation.ets))
- [4.6 3D商品展示视图(Product3DView.ets)](#4.6 3D商品展示视图(Product3DView.ets))
- [4.7 主直播页面(LiveStreamPage.ets)](#4.7 主直播页面(LiveStreamPage.ets))
- 五、关键技术总结
-
- [5.1 Face AR在直播中的适配清单](#5.1 Face AR在直播中的适配清单)
- [5.2 Body AR手势操控最佳实践](#5.2 Body AR手势操控最佳实践)
- [5.3 沉浸光感直播适配要点](#5.3 沉浸光感直播适配要点)
- 六、调试与测试建议
-
- [6.1 AR性能监控](#6.1 AR性能监控)
- [6.2 多窗口测试矩阵](#6.2 多窗口测试矩阵)
- [6.3 常见问题排查](#6.3 常见问题排查)
- 七、总结与展望

每日一句正能量
有时候,最深的相逢,不在人群中,而在一个人安静下来的时刻。
人群中的相逢 多是社交、热闹、外在的互动。安静下来的时刻,你才可能真正"相逢"于自己的内心------那些被忽略的情绪、被压制的梦想、被遗忘的初心。安静中,你可能与一本书、一段音乐、甚至窗外的一片云产生深刻的共鸣。
前言
摘要 :HarmonyOS 6(API 23)带来的悬浮导航、沉浸光感与Face AR & Body AR特性,为电商直播领域提供了全新的交互范式。本文将实战开发一款面向HarmonyOS PC的"灵境直播间"应用,展示如何利用
systemMaterialEffect打造沉浸式直播环境,通过悬浮导航实现多直播间快速切换,基于Face AR实现实时虚拟试妆与表情驱动商品推荐,基于Body AR实现手势操控3D商品模型展示,以及基于多窗口架构构建浮动商品详情、观众互动和实时数据看板的协作直播体验。
一、前言:电商直播的UI与交互革新需求
传统电商直播软件往往采用固定的功能面板和繁杂的侧边栏,在HarmonyOS PC的大屏环境下显得臃肿且缺乏沉浸感。主播需要在OBS、商品后台、互动工具之间频繁切换,操作链路长、视觉疲劳严重。HarmonyOS 6(API 23)引入的悬浮导航(Float Navigation) 、沉浸光感(Immersive Light Effects)与Face AR & Body AR特性,为电商直播带来了"轻盈、沉浸、智能"的设计可能。
本文核心亮点:
- 品类感知光效:根据当前直播商品品类(美妆/数码/服饰/家居)动态切换环境光色与氛围
- 悬浮功能导航:底部悬浮页签替代传统功能栏,支持拖拽排序与透明度调节,最大化直播画面区域
- Face AR虚拟试妆:实时捕捉主播面部微表情,实现口红、眼影、粉底等AR试妆效果,观众所见即所得
- Body AR手势操控:通过手势操控3D商品模型旋转、缩放、拆解,实现"隔空展示"的科幻体验
- 多窗口直播协作:主直播窗口 + 浮动商品详情 + 观众互动面板 + 实时数据看板的光效联动
二、核心特性解析与技术选型
2.1 沉浸光感在直播场景中的价值
HarmonyOS 6的systemMaterialEffect通过模拟物理光照模型,为标题栏和导航组件带来细腻的光晕与反射效果。在直播场景中,这种材质效果能够:
- 增强品类氛围:美妆直播时呈现柔和粉光,数码直播时呈现科技蓝光,服饰直播时呈现时尚暖光
- 提升主播气色:玻璃拟态的半透明层让背景光效柔和过渡,为主播面部补光,提升画面质感
- 增强操作反馈:通过光效强弱区分窗口焦点状态,多窗口协作时视觉层级更清晰
2.2 Face AR在直播中的创新应用
HarmonyOS 6的Face AR能力支持实时精确捕捉人脸微表情(64种BlendShape参数),在直播中可以:
- 实时虚拟试妆:主播无需真实上妆,通过AR实时叠加口红、眼影、腮红等彩妆效果
- 表情驱动推荐:根据主播微笑、惊讶等表情自动弹出对应商品推荐卡片
- 虚拟面具互动:节日直播时自动叠加圣诞帽、兔耳朵等趣味面具,增强互动性
2.3 Body AR在商品展示中的创新应用
HarmonyOS 6的Body AR能力支持20+骨骼关键点追踪,在商品展示中可以:
- 手势操控3D模型:手掌张开放大商品、握拳旋转查看细节、双手开合缩放比例
- 虚拟穿戴展示:服饰类商品自动"穿"在主播身上,实时展示上身效果
- 空间拆解演示:数码产品通过手势触发爆炸图拆解,展示内部构造
三、环境配置与权限声明
3.1 模块依赖配置
在oh-package.json5中添加AR Engine、媒体库和UI Design Kit依赖:
json
{
"dependencies": {
"@hms.core.ar.engine": "^6.1.0",
"@hms.core.ar.arview": "^6.1.0",
"@hms.core.media.library": "^6.0.0",
"@hms.core.arkui.design": "^6.0.0",
"@hms.core.graphics.2d": "^6.0.0"
}
}
3.2 权限声明(module.json5)
json
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:face_ar_camera_permission",
"usedScene": {
"abilities": ["LiveStreamAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_permission"
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:audio_permission"
},
{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "$string:screenshot_permission"
}
]
}
}
隐私说明:Face AR与Body AR的所有图像数据仅在端侧NPU处理,不上传云端,符合鸿蒙系统的隐私设计理念。
四、核心代码实战
4.1 品类感知光感引擎(CategoryLightEngine.ets)
代码亮点:根据直播商品品类动态提取主题色,生成沉浸光感参数,供标题栏、悬浮导航和环境背景使用。
typescript
// engines/CategoryLightEngine.ets
import { ColorUtils } from '@hms.core.graphics.2d';
export enum ProductCategory {
BEAUTY = 'beauty', // 美妆 - 柔粉光
DIGITAL = 'digital', // 数码 - 科技蓝
FASHION = 'fashion', // 服饰 - 时尚暖
HOME = 'home', // 家居 - 温馨黄
FOOD = 'food' // 食品 - 鲜活绿
}
export interface LightTheme {
primaryColor: ResourceColor;
ambientColor: ResourceColor;
glowIntensity: number; // 光晕强度 0-1
materialOpacity: number; // 材质透明度 0-1
pulseSpeed: number; // 呼吸灯速度
}
export class CategoryLightEngine {
private static themes: Map<ProductCategory, LightTheme> = new Map([
[ProductCategory.BEAUTY, {
primaryColor: '#FF6B9D',
ambientColor: 'rgba(255, 107, 157, 0.15)',
glowIntensity: 0.8,
materialOpacity: 0.75,
pulseSpeed: 3000
}],
[ProductCategory.DIGITAL, {
primaryColor: '#00D4FF',
ambientColor: 'rgba(0, 212, 255, 0.12)',
glowIntensity: 0.9,
materialOpacity: 0.65,
pulseSpeed: 2000
}],
[ProductCategory.FASHION, {
primaryColor: '#FF8C42',
ambientColor: 'rgba(255, 140, 66, 0.15)',
glowIntensity: 0.7,
materialOpacity: 0.8,
pulseSpeed: 3500
}],
[ProductCategory.HOME, {
primaryColor: '#FFD93D',
ambientColor: 'rgba(255, 217, 61, 0.12)',
glowIntensity: 0.6,
materialOpacity: 0.85,
pulseSpeed: 4000
}],
[ProductCategory.FOOD, {
primaryColor: '#6BCB77',
ambientColor: 'rgba(107, 203, 119, 0.15)',
glowIntensity: 0.75,
materialOpacity: 0.7,
pulseSpeed: 2800
}]
]);
static getTheme(category: ProductCategory): LightTheme {
return this.themes.get(category) || this.themes.get(ProductCategory.BEAUTY)!;
}
// 根据商品主图动态提取光感色
static async extractThemeFromImage(imageUri: string): Promise<LightTheme> {
const palette = await ColorUtils.extractPalette(imageUri, 5);
const dominantColor = palette[0];
return {
primaryColor: dominantColor,
ambientColor: ColorUtils.alphaBlend(dominantColor, 0.12),
glowIntensity: 0.75,
materialOpacity: 0.7,
pulseSpeed: 3000
};
}
}
4.2 Face AR虚拟试妆引擎(ARMakeupEngine.ets)
代码亮点:利用Face AR的64种BlendShape参数,实时追踪面部关键点,将虚拟妆容精准叠加到主播面部。
typescript
// engines/ARMakeupEngine.ets
import { ARSession, ARFaceTrack, ARBlendShapes, ARFaceMesh } from '@hms.core.ar.engine';
import { MakeupProduct } from '../models/MakeupProduct';
export interface MakeupLayer {
type: 'lipstick' | 'eyeshadow' | 'blush' | 'foundation';
color: ResourceColor;
opacity: number; // 透明度 0-1
blendMode: BlendMode; // 混合模式
}
export class ARMakeupEngine {
private session: ARSession | null = null;
private faceTrack: ARFaceTrack | null = null;
private currentMakeup: Map<string, MakeupLayer> = new Map();
async initialize(): Promise<void> {
this.session = await ARSession.create({
featureTypes: [ARFeatureType.FACE],
cameraConfig: {
facing: CameraFacing.FRONT,
resolution: CameraResolution.HD_1080P
}
});
this.faceTrack = this.session.getFaceTrack();
await this.session.start();
}
// 叠加口红效果
applyLipstick(color: ResourceColor, intensity: number = 0.7): void {
this.currentMakeup.set('lipstick', {
type: 'lipstick',
color: color,
opacity: intensity,
blendMode: BlendMode.MULTIPLY
});
}
// 叠加眼影效果
applyEyeshadow(color: ResourceColor, intensity: number = 0.5): void {
this.currentMakeup.set('eyeshadow', {
type: 'eyeshadow',
color: color,
opacity: intensity,
blendMode: BlendMode.SCREEN
});
}
// 实时渲染妆容到面部Mesh
async renderMakeup(frameData: ARFrame): Promise<ImageBitmap> {
if (!this.faceTrack) return frameData.cameraImage;
const faces = this.faceTrack.getTrackedFaces(frameData);
if (faces.length === 0) return frameData.cameraImage;
const face = faces[0];
const mesh = face.getFaceMesh(); // 4000+顶点高精度Mesh
const blendshapes = face.getBlendShapes(); // 64种表情参数
// 创建妆容画布
const canvas = new OffscreenCanvas(1920, 1080);
const ctx = canvas.getContext('2d')!;
// 绘制原图
ctx.drawImage(frameData.cameraImage, 0, 0);
// 应用口红 - 基于嘴唇BlendShape定位
if (this.currentMakeup.has('lipstick')) {
const lipstick = this.currentMakeup.get('lipstick')!;
const lipPoints = this.extractLipPoints(mesh, blendshapes);
this.drawLipstick(ctx, lipPoints, lipstick);
}
// 应用眼影 - 基于眼部BlendShape定位
if (this.currentMakeup.has('eyeshadow')) {
const eyeshadow = this.currentMakeup.get('eyeshadow')!;
const eyePoints = this.extractEyePoints(mesh, blendshapes);
this.drawEyeshadow(ctx, eyePoints, eyeshadow);
}
// 应用腮红 - 基于脸颊区域
if (this.currentMakeup.has('blush')) {
const blush = this.currentMakeup.get('blush')!;
const cheekPoints = this.extractCheekPoints(mesh);
this.drawBlush(ctx, cheekPoints, blush);
}
return canvas.transferToImageBitmap();
}
private extractLipPoints(mesh: ARFaceMesh, blendshapes: ARBlendShapes): Point[] {
// 利用JAW_OPEN和MOUTH_SMILE等BlendShape参数精确定位嘴唇轮廓
const jawOpen = blendshapes.getValue('JAW_OPEN') || 0;
const mouthSmile = blendshapes.getValue('MOUTH_SMILE_LEFT') || 0;
// 返回嘴唇轮廓顶点索引
return mesh.getRegionVertices(FaceRegion.LIPS, {
jawOpenFactor: jawOpen,
smileFactor: mouthSmile
});
}
private drawLipstick(ctx: CanvasRenderingContext2D, points: Point[], layer: MakeupLayer): void {
if (points.length < 3) return;
ctx.save();
ctx.globalAlpha = layer.opacity;
ctx.globalCompositeOperation = layer.blendMode === BlendMode.MULTIPLY ? 'multiply' : 'source-over';
// 创建嘴唇路径
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.closePath();
// 渐变填充模拟口红光泽
const gradient = ctx.createLinearGradient(
points[0].x, points[0].y,
points[points.length - 1].x, points[points.length - 1].y
);
gradient.addColorStop(0, layer.color as string);
gradient.addColorStop(0.5, ColorUtils.lighten(layer.color, 20));
gradient.addColorStop(1, layer.color as string);
ctx.fillStyle = gradient;
ctx.fill();
// 高光效果
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
}
// 表情驱动自动推荐:检测到微笑时触发商品推荐
detectRecommendationTrigger(blendshapes: ARBlendShapes): boolean {
const smileLeft = blendshapes.getValue('MOUTH_SMILE_LEFT') || 0;
const smileRight = blendshapes.getValue('MOUTH_SMILE_RIGHT') || 0;
const eyeSquint = blendshapes.getValue('EYE_SQUINT_LEFT') || 0;
// 真实微笑检测:嘴角上扬 + 眼部眯起
return (smileLeft > 0.6 && smileRight > 0.6 && eyeSquint > 0.3);
}
release(): void {
this.session?.stop();
this.session?.release();
}
}
4.3 Body AR手势商品操控引擎(ARGestureProductEngine.ets)
代码亮点:将Body AR的20+骨骼关键点映射为3D商品模型的操控指令,实现"隔空展示"的自然交互。
typescript
// engines/ARGestureProductEngine.ets
import { ARSession, ARBodyTrack, ARBodySkeleton } from '@hms.core.ar.engine';
import { Product3DModel } from '../models/Product3DModel';
export enum GestureType {
IDLE = 'idle', // 空闲
SPREAD = 'spread', // 双手张开 - 放大
PINCH = 'pinch', // 双手捏合 - 缩小
ROTATE_LEFT = 'rotate_left', // 左手旋转
ROTATE_RIGHT = 'rotate_right', // 右手旋转
SWIPE_LEFT = 'swipe_left', // 左滑切换
SWIPE_RIGHT = 'swipe_right', // 右滑切换
POINT = 'point' // 单指指向 - 标记热点
}
export interface GestureCommand {
type: GestureType;
confidence: number;
transform: {
scale: number;
rotation: { x: number; y: number; z: number };
translation: { x: number; y: number };
};
}
export class ARGestureProductEngine {
private session: ARSession | null = null;
private bodyTrack: ARBodyTrack | null = null;
private gestureHistory: GestureType[] = [];
private lastGestureTime: number = 0;
async initialize(): Promise<void> {
this.session = await ARSession.create({
featureTypes: [ARFeatureType.BODY],
cameraConfig: {
facing: CameraFacing.FRONT,
resolution: CameraResolution.HD_1080P
}
});
this.bodyTrack = this.session.getBodyTrack();
await this.session.start();
}
// 识别当前手势并生成3D变换指令
recognizeGesture(frameData: ARFrame): GestureCommand | null {
if (!this.bodyTrack) return null;
const bodies = this.bodyTrack.getTrackedBodies(frameData);
if (bodies.length === 0) return null;
const body = bodies[0];
const skeleton = body.getSkeleton();
const keypoints = skeleton.getKeyPoints(); // 20+关键点
// 提取关键骨骼点
const leftWrist = keypoints.find(kp => kp.type === KeyPointType.LEFT_WRIST);
const rightWrist = keypoints.find(kp => kp.type === KeyPointType.RIGHT_WRIST);
const leftElbow = keypoints.find(kp => kp.type === KeyPointType.LEFT_ELBOW);
const rightElbow = keypoints.find(kp => kp.type === KeyPointType.RIGHT_ELBOW);
const nose = keypoints.find(kp => kp.type === KeyPointType.NOSE);
if (!leftWrist || !rightWrist || !nose) return null;
// 计算双手距离和角度
const handDistance = this.calculateDistance(leftWrist, rightWrist);
const handAngle = this.calculateHandAngle(leftWrist, rightWrist, nose);
// 手势状态机判断
let gesture: GestureType = GestureType.IDLE;
let confidence = 0;
// 双手张开检测(距离大且角度水平)
if (handDistance > 300 && Math.abs(handAngle) < 30) {
gesture = GestureType.SPREAD;
confidence = Math.min(handDistance / 500, 1);
}
// 双手捏合检测(距离小)
else if (handDistance < 80) {
gesture = GestureType.PINCH;
confidence = 1 - (handDistance / 80);
}
// 单手旋转检测(一手高一手低)
else if (leftWrist.y < rightWrist.y - 100) {
gesture = GestureType.ROTATE_LEFT;
confidence = Math.min((rightWrist.y - leftWrist.y) / 200, 1);
}
else if (rightWrist.y < leftWrist.y - 100) {
gesture = GestureType.ROTATE_RIGHT;
confidence = Math.min((leftWrist.y - rightWrist.y) / 200, 1);
}
// 防抖处理:同一手势持续500ms以上才确认
const now = Date.now();
this.gestureHistory.push(gesture);
if (this.gestureHistory.length > 5) this.gestureHistory.shift();
const stableGesture = this.getStableGesture();
if (stableGesture !== gesture) return null;
if (now - this.lastGestureTime < 500) return null;
this.lastGestureTime = now;
return {
type: gesture,
confidence: confidence,
transform: this.calculateTransform(gesture, leftWrist, rightWrist, handDistance)
};
}
private calculateTransform(
gesture: GestureType,
leftWrist: KeyPoint,
rightWrist: KeyPoint,
handDistance: number
): GestureCommand['transform'] {
switch (gesture) {
case GestureType.SPREAD:
return {
scale: 1 + (handDistance / 1000),
rotation: { x: 0, y: 0, z: 0 },
translation: { x: 0, y: 0 }
};
case GestureType.PINCH:
return {
scale: Math.max(0.5, 1 - (handDistance / 200)),
rotation: { x: 0, y: 0, z: 0 },
translation: { x: 0, y: 0 }
};
case GestureType.ROTATE_LEFT:
return {
scale: 1,
rotation: { x: 0, y: -15, z: 0 },
translation: { x: 0, y: 0 }
};
case GestureType.ROTATE_RIGHT:
return {
scale: 1,
rotation: { x: 0, y: 15, z: 0 },
translation: { x: 0, y: 0 }
};
default:
return {
scale: 1,
rotation: { x: 0, y: 0, z: 0 },
translation: { x: 0, y: 0 }
};
}
}
private getStableGesture(): GestureType {
if (this.gestureHistory.length < 3) return GestureType.IDLE;
const lastThree = this.gestureHistory.slice(-3);
return lastThree.every(g => g === lastThree[0]) ? lastThree[0] : GestureType.IDLE;
}
private calculateDistance(p1: KeyPoint, p2: KeyPoint): number {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
private calculateHandAngle(left: KeyPoint, right: KeyPoint, nose: KeyPoint): number {
const dx = right.x - left.x;
const dy = right.y - left.y;
return Math.atan2(dy, dx) * (180 / Math.PI);
}
release(): void {
this.session?.stop();
this.session?.release();
}
}
4.4 沉浸光感标题栏(ImmersiveLiveTitleBar.ets)
代码亮点:标题栏根据当前直播品类动态调整光效,并显示AR追踪状态、在线人数和直播时长。
typescript
// components/ImmersiveLiveTitleBar.ets
import { CategoryLightEngine, ProductCategory, LightTheme } from '../engines/CategoryLightEngine';
@Component
export struct ImmersiveLiveTitleBar {
@Prop currentCategory: ProductCategory;
@Prop viewerCount: number;
@Prop liveDuration: number; // 秒
@Prop arTrackingStatus: boolean;
@Prop currentProduct: string;
@State theme: LightTheme = CategoryLightEngine.getTheme(ProductCategory.BEAUTY);
@State pulseAnimation: boolean = false;
aboutToAppear(): void {
this.theme = CategoryLightEngine.getTheme(this.currentCategory);
// 启动呼吸灯动画
setInterval(() => {
this.pulseAnimation = !this.pulseAnimation;
}, this.theme.pulseSpeed / 2);
}
build() {
Row() {
// 左侧:直播状态与品类标识
Row({ space: 12 }) {
// 直播状态指示灯
Stack() {
Circle()
.width(12)
.height(12)
.fill(this.pulseAnimation ? '#FF4444' : '#CC0000')
.shadow({
radius: this.pulseAnimation ? 15 : 5,
color: 'rgba(255, 68, 68, 0.6)',
offsetX: 0,
offsetY: 0
})
.animation({
duration: 1000,
curve: Curve.EaseInOut,
iterations: -1
})
Circle()
.width(8)
.height(8)
.fill('#FFFFFF')
}
Text('LIVE')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
// 品类标签
Text(this.getCategoryLabel())
.fontSize(12)
.fontColor(this.theme.primaryColor)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor(this.theme.ambientColor)
.borderRadius(6)
}
// 中间:当前商品与AR状态
Row({ space: 16 }) {
if (this.currentProduct) {
Text(`📦 ${this.currentProduct}`)
.fontSize(14)
.fontColor('#FFFFFF')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.constraintSize({ maxWidth: 200 })
}
// AR追踪状态
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill(this.arTrackingStatus ? '#00FF88' : '#FFAA00')
Text(this.arTrackingStatus ? 'AR追踪中' : 'AR初始化')
.fontSize(12)
.fontColor(this.arTrackingStatus ? '#00FF88' : '#FFAA00')
}
}
// 右侧:数据看板
Row({ space: 20 }) {
Column({ space: 2 }) {
Text('👥 在线')
.fontSize(10)
.fontColor('#AAAAAA')
Text(this.formatViewerCount())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
Column({ space: 2 }) {
Text('⏱️ 时长')
.fontSize(10)
.fontColor('#AAAAAA')
Text(this.formatDuration())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
}
}
.width('100%')
.height(56)
.padding({ left: 24, right: 24 })
.backgroundColor(this.theme.ambientColor)
.backdropBlur(20)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
.borderRadius({ bottomLeft: 16, bottomRight: 16 })
.shadow({
radius: 20,
color: this.theme.primaryColor,
offsetX: 0,
offsetY: 4
})
.justifyContent(FlexAlign.SpaceBetween)
}
private getCategoryLabel(): string {
const labels: Map<ProductCategory, string> = new Map([
[ProductCategory.BEAUTY, '美妆护肤'],
[ProductCategory.DIGITAL, '数码科技'],
[ProductCategory.FASHION, '潮流服饰'],
[ProductCategory.HOME, '品质家居'],
[ProductCategory.FOOD, '美食生鲜']
]);
return labels.get(this.currentCategory) || '综合直播';
}
private formatViewerCount(): string {
if (this.viewerCount >= 10000) {
return `${(this.viewerCount / 10000).toFixed(1)}w`;
}
return this.viewerCount.toString();
}
private formatDuration(): string {
const hours = Math.floor(this.liveDuration / 3600);
const minutes = Math.floor((this.liveDuration % 3600) / 60);
const seconds = this.liveDuration % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
}
4.5 悬浮导航控制面板(FloatLiveNavigation.ets)
代码亮点 :底部悬浮面板采用HdsTabs悬浮样式,四周留白,支持透明度三档调节,并集成商品切换、AR功能开关和互动工具。
typescript
// components/FloatLiveNavigation.ets
import { HdsTabs, HdsTabBarStyle } from '@hms.core.arkui.design';
import { CategoryLightEngine, ProductCategory } from '../engines/CategoryLightEngine';
interface NavItem {
title: string;
icon: Resource;
category?: ProductCategory;
action: 'switch_category' | 'toggle_ar' | 'show_products' | 'show_interaction' | 'show_data';
}
@Component
export struct FloatLiveNavigation {
@Prop currentCategory: ProductCategory;
@Prop arEnabled: boolean;
@Prop makeupEnabled: boolean;
@Prop gestureEnabled: boolean;
@Prop transparencyLevel: number; // 0.55, 0.70, 0.85
@State selectedIndex: number = 0;
@State theme = CategoryLightEngine.getTheme(ProductCategory.BEAUTY);
private navItems: NavItem[] = [
{ title: '美妆', icon: $r('app.media.icon_beauty'), category: ProductCategory.BEAUTY, action: 'switch_category' },
{ title: '数码', icon: $r('app.media.icon_digital'), category: ProductCategory.DIGITAL, action: 'switch_category' },
{ title: '服饰', icon: $r('app.media.icon_fashion'), category: ProductCategory.FASHION, action: 'switch_category' },
{ title: 'AR试妆', icon: $r('app.media.icon_ar'), action: 'toggle_ar' },
{ title: '商品', icon: $r('app.media.icon_product'), action: 'show_products' },
{ title: '互动', icon: $r('app.media.icon_chat'), action: 'show_interaction' }
];
build() {
Column() {
HdsTabs({
barStyle: HdsTabBarStyle.FLOATING,
index: this.selectedIndex,
onChange: (index: number) => {
this.selectedIndex = index;
this.handleNavChange(this.navItems[index]);
}
}) {
ForEach(this.navItems, (item: NavItem, index: number) => {
TabContent() {
// 内容区域由主页面控制,此处仅作占位
Stack() {}
}
.tabBar(this.buildTabBar(item, index))
})
}
.barBackgroundColor(`rgba(30, 30, 46, ${this.transparencyLevel})`)
.barActiveColor(this.theme.primaryColor)
.barInactiveColor('#888888')
.barHeight(64)
.barMargin({ left: 40, right: 40, bottom: 16 })
.barBorderRadius(32)
.systemMaterialEffect(MaterialStyle.IMMERSIVE)
.backdropBlur(20)
}
.width('100%')
.padding({ bottom: 12 })
}
@Builder
buildTabBar(item: NavItem, index: number): void {
Column({ space: 4 }) {
Stack() {
Image(item.icon)
.width(24)
.height(24)
.fillColor(index === this.selectedIndex ? this.theme.primaryColor : '#888888')
// AR功能开启指示器
if (item.action === 'toggle_ar' && this.arEnabled) {
Circle()
.width(8)
.height(8)
.fill('#00FF88')
.position({ x: 18, y: -4 })
.shadow({ radius: 4, color: 'rgba(0, 255, 136, 0.6)' })
}
}
Text(item.title)
.fontSize(11)
.fontColor(index === this.selectedIndex ? this.theme.primaryColor : '#888888')
}
.width(64)
.height(56)
.justifyContent(FlexAlign.Center)
}
private handleNavChange(item: NavItem): void {
switch (item.action) {
case 'switch_category':
if (item.category) {
this.theme = CategoryLightEngine.getTheme(item.category);
// 通知主页面切换品类
AppStorage.set('switch_category', item.category);
}
break;
case 'toggle_ar':
// 切换AR功能状态
AppStorage.set('toggle_ar_makeup', !this.makeupEnabled);
break;
case 'show_products':
AppStorage.set('show_product_panel', true);
break;
case 'show_interaction':
AppStorage.set('show_interaction_panel', true);
break;
}
}
}
4.6 3D商品展示视图(Product3DView.ets)
代码亮点:集成Body AR手势操控,支持3D商品模型的旋转、缩放、拆解动画,并实时响应手势指令。
typescript
// components/Product3DView.ets
import { ARGestureProductEngine, GestureCommand, GestureType } from '../engines/ARGestureProductEngine';
import { Product3DModel } from '../models/Product3DModel';
@Component
export struct Product3DView {
@Prop product: Product3DModel;
@State gestureEngine: ARGestureProductEngine | null = null;
@State currentGesture: GestureType = GestureType.IDLE;
@State modelTransform: {
scale: number;
rotationX: number;
rotationY: number;
rotationZ: number;
} = { scale: 1, rotationX: 0, rotationY: 0, rotationZ: 0 };
@State isExploded: boolean = false; // 拆解状态
@State gestureHint: string = '双手张开放大,捏合缩小,单手旋转';
aboutToAppear(): void {
this.gestureEngine = new ARGestureProductEngine();
this.gestureEngine.initialize();
// 监听AR手势数据
AppStorage.watch('ar_gesture_command', (cmd: GestureCommand) => {
this.handleGestureCommand(cmd);
});
}
private handleGestureCommand(cmd: GestureCommand): void {
this.currentGesture = cmd.type;
// 平滑过渡动画
animateTo({
duration: 300,
curve: Curve.EaseOut
}, () => {
this.modelTransform.scale = cmd.transform.scale;
this.modelTransform.rotationY += cmd.transform.rotation.y;
});
// 更新手势提示
const hints: Map<GestureType, string> = new Map([
[GestureType.SPREAD, '🔍 放大查看细节'],
[GestureType.PINCH, '🔎 缩小查看整体'],
[GestureType.ROTATE_LEFT, '↩️ 向左旋转'],
[GestureType.ROTATE_RIGHT, '↪️ 向右旋转'],
[GestureType.POINT, '👆 标记热点']
]);
this.gestureHint = hints.get(cmd.type) || '双手张开放大,捏合缩小,单手旋转';
}
build() {
Stack() {
// 3D模型渲染区域
Column() {
// 使用WebGL/Canvas 3D渲染商品模型
Canvas(this.render3DModel)
.width('100%')
.height('100%')
.backgroundColor('transparent')
}
.width('100%')
.height('100%')
// 手势提示层
Column({ space: 8 }) {
Text(this.gestureHint)
.fontSize(14)
.fontColor('rgba(255, 255, 255, 0.8)')
.padding(12)
.backgroundColor('rgba(0, 0, 0, 0.4)')
.borderRadius(20)
.backdropBlur(10)
// 当前手势可视化
if (this.currentGesture !== GestureType.IDLE) {
Text(`当前手势: ${this.currentGesture}`)
.fontSize(12)
.fontColor('#00FF88')
.padding(8)
.backgroundColor('rgba(0, 255, 136, 0.1)')
.borderRadius(12)
}
}
.position({ x: 0, y: 20 })
.width('100%')
.alignItems(HorizontalAlign.Center)
// 拆解控制按钮
if (this.product.supportExplode) {
Button(this.isExploded ? '🔧 组装' : '💥 拆解')
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor(this.isExploded ? '#4A90E2' : '#FF6B6B')
.borderRadius(20)
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.position({ x: 20, y: '90%' })
.onClick(() => {
this.isExploded = !this.isExploded;
// 触发拆解/组装动画
AppStorage.set('explode_model', this.isExploded);
})
}
// 热点标注层
ForEach(this.product.hotspots, (hotspot: ProductHotspot) => {
Button('🔍')
.fontSize(10)
.width(32)
.height(32)
.backgroundColor('rgba(255, 107, 157, 0.8)')
.borderRadius(16)
.position({
x: hotspot.x * 100 + '%',
y: hotspot.y * 100 + '%'
})
.shadow({ radius: 10, color: 'rgba(255, 107, 157, 0.5)' })
.onClick(() => {
AppStorage.set('show_hotspot_detail', hotspot);
})
})
}
.width('100%')
.height('100%')
}
private render3DModel = (context: CanvasRenderingContext2D) => {
const canvas = context.canvas;
const w = canvas.width;
const h = canvas.height;
// 清除画布
context.clearRect(0, 0, w, h);
// 应用当前变换
context.save();
context.translate(w / 2, h / 2);
context.scale(this.modelTransform.scale, this.modelTransform.scale);
context.rotate(this.modelTransform.rotationY * Math.PI / 180);
// 绘制3D模型(简化示意,实际使用WebGL或3D引擎)
this.drawProductModel(context, this.product, this.isExploded);
context.restore();
// 绘制网格地面
this.drawGridFloor(context, w, h);
};
private drawProductModel(ctx: CanvasRenderingContext2D, product: Product3DModel, exploded: boolean): void {
// 根据产品类型绘制不同3D模型
// 实际项目中应使用Three.js或自研3D引擎加载GLTF/GLB模型
ctx.fillStyle = product.baseColor;
if (exploded && product.explodeParts) {
// 拆解状态:绘制分离的部件
product.explodeParts.forEach((part, index) => {
ctx.save();
ctx.translate(part.explodeOffset.x, part.explodeOffset.y);
ctx.fillStyle = part.color;
ctx.fillRect(part.x, part.y, part.width, part.height);
// 部件标签
ctx.fillStyle = '#FFFFFF';
ctx.font = '12px sans-serif';
ctx.fillText(part.name, part.x, part.y - 5);
ctx.restore();
});
} else {
// 正常状态:绘制完整模型
ctx.fillRect(-50, -50, 100, 100);
// 高光效果
const gradient = ctx.createLinearGradient(-50, -50, 50, 50);
gradient.addColorStop(0, 'rgba(255,255,255,0.3)');
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.fillRect(-50, -50, 100, 100);
}
}
private drawGridFloor(ctx: CanvasRenderingContext2D, w: number, h: number): void {
ctx.save();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 1;
const gridSize = 40;
const perspective = 0.6;
for (let i = -w; i < w; i += gridSize) {
ctx.beginPath();
ctx.moveTo(i, h / 2);
ctx.lineTo(i * perspective, h);
ctx.stroke();
}
for (let i = h / 2; i < h; i += gridSize) {
ctx.beginPath();
ctx.moveTo(-w, i);
ctx.lineTo(w, i);
ctx.stroke();
}
ctx.restore();
}
aboutToDisappear(): void {
this.gestureEngine?.release();
}
}
4.7 主直播页面(LiveStreamPage.ets)
代码亮点:整合AR数据流、品类光感、悬浮导航和多窗口管理,实现完整的"灵境直播间"体验。
typescript
// pages/LiveStreamPage.ets
import { ARMakeupEngine } from '../engines/ARMakeupEngine';
import { ARGestureProductEngine } from '../engines/ARGestureProductEngine';
import { CategoryLightEngine, ProductCategory } from '../engines/CategoryLightEngine';
import { ImmersiveLiveTitleBar } from '../components/ImmersiveLiveTitleBar';
import { FloatLiveNavigation } from '../components/FloatLiveNavigation';
import { Product3DView } from '../components/Product3DView';
@Entry
@Component
struct LiveStreamPage {
// AR引擎
private makeupEngine: ARMakeupEngine = new ARMakeupEngine();
private gestureEngine: ARGestureProductEngine = new ARGestureProductEngine();
// 状态管理
@State currentCategory: ProductCategory = ProductCategory.BEAUTY;
@State viewerCount: number = 12580;
@State liveDuration: number = 0;
@State arTrackingStatus: boolean = false;
@State currentProduct: string = '华为FreeBuds Pro 3';
@State makeupEnabled: boolean = true;
@State gestureEnabled: boolean = true;
@State transparencyLevel: number = 0.70;
// 多窗口管理
@State showProductPanel: boolean = false;
@State showInteractionPanel: boolean = false;
@State showDataPanel: boolean = false;
// 直播画面
@State cameraFrame: ImageBitmap | null = null;
@State processedFrame: ImageBitmap | null = null;
aboutToAppear(): void {
// 初始化窗口沉浸模式
this.setupImmersiveWindow();
// 初始化AR引擎
this.initializeAREngines();
// 启动直播计时
setInterval(() => {
this.liveDuration++;
}, 1000);
// 监听AppStorage事件
this.setupEventListeners();
}
private setupImmersiveWindow(): void {
const window = windowStage.getMainWindowSync();
window.setWindowLayoutFullScreen(true);
window.setWindowBackgroundColor('#000000');
}
private async initializeAREngines(): Promise<void> {
try {
await this.makeupEngine.initialize();
await this.gestureEngine.initialize();
this.arTrackingStatus = true;
// 启动AR渲染循环
this.startARLoop();
} catch (err) {
console.error('AR引擎初始化失败:', err);
this.arTrackingStatus = false;
}
}
private async startARLoop(): Promise<void> {
const loop = async () => {
if (!this.arTrackingStatus) return;
try {
// 获取相机帧
const frame = await this.makeupEngine.session?.getCurrentFrame();
if (!frame) {
requestAnimationFrame(loop);
return;
}
// Face AR:虚拟试妆渲染
if (this.makeupEnabled) {
this.processedFrame = await this.makeupEngine.renderMakeup(frame);
} else {
this.processedFrame = frame.cameraImage;
}
// Body AR:手势识别
if (this.gestureEnabled) {
const gestureCmd = this.gestureEngine.recognizeGesture(frame);
if (gestureCmd) {
AppStorage.set('ar_gesture_command', gestureCmd);
}
// 表情驱动推荐检测
const faces = this.makeupEngine.faceTrack?.getTrackedFaces(frame);
if (faces && faces.length > 0) {
const shouldRecommend = this.makeupEngine.detectRecommendationTrigger(
faces[0].getBlendShapes()
);
if (shouldRecommend) {
this.triggerSmartRecommendation();
}
}
}
} catch (err) {
console.error('AR渲染循环错误:', err);
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
private triggerSmartRecommendation(): void {
// 检测到主播真实微笑时,自动弹出当前商品推荐卡片
animateTo({
duration: 500,
curve: Curve.Spring
}, () => {
// 显示推荐动画
AppStorage.set('show_recommendation_card', {
product: this.currentProduct,
trigger: 'smile',
timestamp: Date.now()
});
});
}
private setupEventListeners(): void {
AppStorage.watch('switch_category', (category: ProductCategory) => {
this.currentCategory = category;
});
AppStorage.watch('toggle_ar_makeup', (enabled: boolean) => {
this.makeupEnabled = enabled;
});
AppStorage.watch('show_product_panel', (show: boolean) => {
this.showProductPanel = show;
});
AppStorage.watch('show_interaction_panel', (show: boolean) => {
this.showInteractionPanel = show;
});
}
build() {
Stack() {
// 背景环境光
Column()
.width('100%')
.height('100%')
.backgroundColor(CategoryLightEngine.getTheme(this.currentCategory).ambientColor)
.animation({
duration: 1000,
curve: Curve.EaseInOut
})
// 主直播画面区域
Column() {
// AR处理后的相机画面
if (this.processedFrame) {
Image(this.processedFrame)
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
} else {
// 未初始化时的占位
Column({ space: 16 }) {
LoadingProgress()
.width(48)
.height(48)
.color('#FFFFFF')
Text('正在初始化AR直播引擎...')
.fontSize(14)
.fontColor('#888888')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
// 3D商品展示叠加层(当展示商品时)
if (this.currentProduct && this.gestureEnabled) {
Product3DView({
product: this.getCurrentProductModel()
})
.width('40%')
.height('50%')
.position({ x: '55%', y: '25%' })
.backgroundColor('rgba(0, 0, 0, 0.3)')
.borderRadius(16)
.backdropBlur(10)
}
}
.width('100%')
.height('100%')
// 沉浸光感标题栏
ImmersiveLiveTitleBar({
currentCategory: this.currentCategory,
viewerCount: this.viewerCount,
liveDuration: this.liveDuration,
arTrackingStatus: this.arTrackingStatus,
currentProduct: this.currentProduct
})
.position({ x: 0, y: 0 })
.zIndex(100)
// 浮动商品详情窗口
if (this.showProductPanel) {
FloatProductPanel({
product: this.getCurrentProductModel(),
onClose: () => {
this.showProductPanel = false;
}
})
.position({ x: '60%', y: '15%' })
.width('35%')
.height('70%')
.zIndex(90)
}
// 浮动观众互动窗口
if (this.showInteractionPanel) {
FloatInteractionPanel({
onClose: () => {
this.showInteractionPanel = false;
}
})
.position({ x: '2%', y: '15%' })
.width('30%')
.height('70%')
.zIndex(90)
}
// 底部悬浮导航
FloatLiveNavigation({
currentCategory: this.currentCategory,
arEnabled: this.makeupEnabled || this.gestureEnabled,
makeupEnabled: this.makeupEnabled,
gestureEnabled: this.gestureEnabled,
transparencyLevel: this.transparencyLevel
})
.position({ x: 0, y: '100%' })
.translate({ y: -80 })
.zIndex(100)
}
.width('100%')
.height('100%')
.backgroundColor('#0a0a0f')
}
private getCurrentProductModel(): Product3DModel {
// 返回当前商品的3D模型数据
return {
id: 'freebuds_pro_3',
name: '华为FreeBuds Pro 3',
baseColor: '#C0C0C0',
category: ProductCategory.DIGITAL,
supportExplode: true,
explodeParts: [
{ name: '充电仓', x: -30, y: -30, width: 60, height: 40, color: '#E8E8E8', explodeOffset: { x: -80, y: -60 } },
{ name: '左耳机', x: -25, y: 20, width: 20, height: 35, color: '#D0D0D0', explodeOffset: { x: -50, y: 40 } },
{ name: '右耳机', x: 5, y: 20, width: 20, height: 35, color: '#D0D0D0', explodeOffset: { x: 50, y: 40 } },
{ name: '扬声器单元', x: -10, y: 0, width: 20, height: 20, color: '#333333', explodeOffset: { x: 0, y: -80 } }
],
hotspots: [
{ x: 0.3, y: 0.4, title: '星闪连接', description: '支持星闪技术,抗干扰能力提升2倍' },
{ x: 0.7, y: 0.5, title: '智慧动态降噪', description: '实时识别环境噪音,自动调节降噪深度' }
]
};
}
aboutToDisappear(): void {
this.makeupEngine.release();
this.gestureEngine.release();
}
}
五、关键技术总结
5.1 Face AR在直播中的适配清单
| 适配项 | 说明 | 代码位置 |
|---|---|---|
| 前置摄像头配置 | 直播场景必须使用前置摄像头 | ARMakeupEngine.initialize() |
| 实时性能优化 | 目标帧率30fps,单帧处理<33ms | startARLoop() |
| 妆容精准定位 | 利用BlendShape动态调整Mesh顶点 | extractLipPoints() |
| 隐私合规 | 端侧处理,不上传云端 | module.json5权限声明 |
5.2 Body AR手势操控最佳实践
| 实践项 | 说明 | 代码位置 |
|---|---|---|
| 手势防抖 | 连续3帧一致才确认手势 | getStableGesture() |
| 平滑变换 | 使用animateTo实现300ms过渡 |
handleGestureCommand() |
| 视觉反馈 | 实时显示当前手势和提示 | Product3DView手势提示层 |
| 边界限制 | 缩放比例限制在0.5x-3x | calculateTransform() |
5.3 沉浸光感直播适配要点
- 品类色动态切换 :使用
animation({ duration: 1000 })实现光色平滑过渡,避免突兀跳变 - 透明度智能调节 :直播画面为主体时建议
0.55(弱),展示UI时建议0.85(强) - 多窗口光效联动 :通过
AppStorage同步各子窗口的currentCategory状态,确保光效统一 - 呼吸灯节奏控制 :美妆类
3000ms(柔和)、数码类2000ms(科技)、食品类2800ms(鲜活)
六、调试与测试建议
6.1 AR性能监控
typescript
// 在ARLoop中添加性能监控
const startTime = performance.now();
// ... AR处理逻辑
const processTime = performance.now() - startTime;
if (processTime > 33) {
console.warn(`AR处理帧耗时${processTime.toFixed(1)}ms,存在掉帧风险`);
}
6.2 多窗口测试矩阵
| 测试场景 | 预期结果 |
|---|---|
| 主窗口全屏 + 浮动商品面板 | 标题栏光效同步,面板不遮挡直播画面 |
| 分屏模式(左直播右数据) | 悬浮导航自动适配宽度,功能按钮不重叠 |
| 外接显示器扩展 | 多窗口可拖拽至副屏,光效状态同步 |
| 鼠标悬停导航项 | 显示功能Tooltip,透明度短暂提升 |
6.3 常见问题排查
| 现象 | 原因 | 解决方案 |
|---|---|---|
| Face AR妆容偏移 | 摄像头分辨率与Mesh不匹配 | 统一使用1080P分辨率 |
| Body AR手势误识别 | 背景人物干扰 | 设置ARBodyTrack.setMaxBodyCount(1) |
| 光效切换闪烁 | 动画时长过短 | 调整duration至1000ms以上 |
| 多窗口光效不同步 | AppStorage键名不一致 |
统一使用currentCategory作为同步键 |
七、总结与展望
本文基于HarmonyOS 6(API 23)的悬浮导航 、沉浸光感 与Face AR & Body AR特性,完整实战了一款PC端"灵境直播间"应用。核心创新点总结:
- 品类感知光效系统:根据直播商品品类动态切换主题色,美妆柔粉、数码科技蓝、服饰时尚暖,营造品类专属沉浸氛围
- Face AR虚拟试妆:利用64种BlendShape参数实时追踪面部微表情,实现口红、眼影、腮红的AR叠加,观众所见即所得
- 表情驱动智能推荐:检测到主播真实微笑时自动触发商品推荐,将情感表达转化为商业动作
- Body AR手势操控:通过20+骨骼关键点识别,实现双手张开放大、捏合缩小、单手旋转的隔空3D商品展示
- 悬浮导航自适应 :采用
HdsTabs悬浮样式,四周留白,支持透明度三档调节,最大化直播画面区域 - PC级多窗口协作 :主直播窗口 + 浮动商品详情 + 观众互动面板 + 实时数据看板,通过
WindowManager实现跨窗口光效联动
未来扩展方向:
- AI智能话术:结合主播表情数据,实时生成商品卖点话术推荐
- 分布式直播:通过鸿蒙分布式软总线,实现手机前置摄像头采集 → PC端AR处理 → 智慧屏大屏展示的多设备协同直播
- 虚拟主播模式:基于Face AR的Mesh数据,驱动完全虚拟的3D数字人主播,实现24小时不间断直播
- 观众端AR互动:观众通过Face AR在弹幕中发送自己的AR表情反应,增强双向互动
- 云端多人协作:支持主播、助播、运营多人同时操作不同窗口,通过AR追踪识别各自手势权限
转载自:https://blog.csdn.net/u014727709/article/details/160920489
欢迎 👍点赞✍评论⭐收藏,欢迎指正