文章目录
-
- 每日一句正能量
- 前言
- 一、教师端的数字化困境与AR破局
-
- [1.1 在线教学的核心痛点](#1.1 在线教学的核心痛点)
- [1.2 "智能互动课堂"系统架构](#1.2 "智能互动课堂"系统架构)
- 二、环境配置与系统初始化
-
- [2.1 模块依赖配置](#2.1 模块依赖配置)
- [2.2 教师端窗口配置(TeacherAbility.ets)](#2.2 教师端窗口配置(TeacherAbility.ets))
- 三、核心组件实战
-
- [3.1 学生专注度实时分析引擎(FocusAnalyzer.ets)](#3.1 学生专注度实时分析引擎(FocusAnalyzer.ets))
- [3.2 教师手势控制面板(TeacherGesturePanel.ets)](#3.2 教师手势控制面板(TeacherGesturePanel.ets))
- [3.3 学生专注度可视化面板(StudentFocusPanel.ets)](#3.3 学生专注度可视化面板(StudentFocusPanel.ets))
- [3.4 主课堂页面:沉浸光效与教学场景融合](#3.4 主课堂页面:沉浸光效与教学场景融合)
- 四、关键技术总结
-
- [4.1 Face AR 专注度分析体系](#4.1 Face AR 专注度分析体系)
- [4.2 Body AR 教师手势指令映射](#4.2 Body AR 教师手势指令映射)
- [4.3 沉浸光效与教学状态联动](#4.3 沉浸光效与教学状态联动)
- 五、调试与部署建议
-
- [5.1 调试要点](#5.1 调试要点)
- [5.2 部署场景](#5.2 部署场景)
- 六、总结与展望

每日一句正能量
身处低谷,怎么走都是向上,没有谁生来自带盔甲,但你可以让自己无坚不摧。
坚韧并非天赋,而是每个人都可以通过历练和心智锻造获得的能力。
前言
摘要:在线教育与混合式教学已成为教育新常态,但教师端长期面临"看不见学生、摸不清状态"的痛点。HarmonyOS 6(API 23)的 Face AR 与 Body AR 能力为教师提供了"数字化的第三只眼"------通过实时分析学生的面部表情和肢体动作,自动评估课堂专注度;通过手势识别实现无接触式课堂互动操控。本文将实战开发一款面向 HarmonyOS PC 的教师授课辅助系统,展示悬浮导航与沉浸光感如何适配教学场景,打造"懂学生、懂教学"的智能课堂。
一、教师端的数字化困境与AR破局
1.1 在线教学的核心痛点
| 痛点场景 | 传统解决方案 | 问题 |
|---|---|---|
| 学生走神无法察觉 | 学生主动开麦报告 | 学生羞于开口,教师被动等待 |
| 课堂互动形式单一 | 文字聊天区提问 | 打字慢、参与度低、反馈延迟 |
| 教学节奏凭感觉 | 教师经验判断 | 新手教师难以把握,优质课难复制 |
| 课后复盘无数据 | 回放录像人工分析 | 耗时耗力,难以规模化 |
HarmonyOS 6(API 23)的 AR Engine 6.1.0 提供了 Face AR (64种BlendShape表情参数、注视点追踪)和 Body AR(20+骨骼关键点、手势识别)两大核心能力 ,为教师端带来了"实时学情感知"的全新维度。
1.2 "智能互动课堂"系统架构
┌─────────────────────────────────────────────────────────────┐
│ 教师端 PC 大屏(27英寸) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 课件展示区(PPT/视频/白板) │ │
│ │ · 手势翻页:左手上一页/右手下一页 │ │
│ │ · 表情标注:学生疑问自动标记在课件对应位置 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↑ │
│ AR学生状态浮层(悬浮导航形态) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 专注度仪表盘 | 情绪热力图 | 互动手势指示 | 沉浸光效反馈 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 学生A │ │ 学生B │ │ 学生C │
│ 手机端 │ │ 平板端 │ │ PC端 │
│ Face AR │ │ Face AR │ │ Face AR │
│ 专注度 │ │ 专注度 │ │ 专注度 │
└─────────┘ └─────────┘ └─────────┘
二、环境配置与系统初始化
2.1 模块依赖配置
json
// oh-package.json5
{
"dependencies": {
"@hms.core.ar.arengine": "^6.1.0",
"@kit.ArkUI": "^6.1.0",
"@kit.AbilityKit": "^6.1.0",
"@kit.DistributedServiceKit": "^6.1.0",
"@kit.SensorServiceKit": "^6.1.0"
}
}
2.2 教师端窗口配置(TeacherAbility.ets)
代码亮点:教师端需要同时展示课件和AR学生状态,采用左右分栏布局。左侧为主课件区(占70%),右侧为AR学情面板(占30%)。悬浮导航根据教学阶段动态切换形态------讲课时最小化为边缘光点,互动时展开为完整工具栏 。
typescript
// entry/src/main/ets/ability/TeacherAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
/**
* 学生设备信息
*/
interface StudentDevice {
deviceId: string;
deviceName: string;
studentName: string;
faceAREnabled: boolean;
bodyAREnabled: boolean;
lastActive: number;
}
export default class TeacherAbility extends UIAbility {
private studentDevices: StudentDevice[] = [];
private mainWindow: window.Window | null = null;
onWindowStageCreate(windowStage: window.WindowStage): void {
this.setupTeacherWindow(windowStage);
this.discoverStudentDevices();
}
/**
* 配置教师端窗口:左右分栏布局
* 左侧70%课件区 + 右侧30% AR学情面板
*/
private async setupTeacherWindow(windowStage: window.WindowStage): Promise<void> {
try {
this.mainWindow = windowStage.getMainWindowSync();
// PC端自由窗口模式
await this.mainWindow.setWindowSizeType(window.WindowSizeType.FREE);
await this.mainWindow.setWindowMode(window.WindowMode.FULLSCREEN);
await this.mainWindow.setWindowTitleBarEnable(false);
await this.mainWindow.setWindowBackgroundColor('#00000000');
await this.mainWindow.setWindowLayoutFullScreen(true);
// 禁用系统手势,防止教学过程中误触
await this.mainWindow.setWindowGestureDisabled(true);
AppStorage.setOrCreate('teacher_window', this.mainWindow);
windowStage.loadContent('pages/TeacherClassroomPage', (err) => {
if (err.code) {
console.error('Failed to load teacher page:', JSON.stringify(err));
}
});
} catch (error) {
console.error('Teacher window setup failed:', error);
}
}
/**
* 发现学生设备:通过分布式软总线扫描同账号下的学生端
*/
private async discoverStudentDevices(): Promise<void> {
try {
const discoverListener: distributedDeviceManager.DeviceManagerCallback = {
onDeviceOnline: (device) => this.handleStudentOnline(device),
onDeviceOffline: (device) => this.handleStudentOffline(device),
onDeviceChanged: (device) => this.handleStudentChanged(device)
};
await distributedDeviceManager.on('deviceStateChange', discoverListener);
const trustedDevices = await distributedDeviceManager.getTrustedDeviceListSync();
console.info(`Found ${trustedDevices.length} trusted devices`);
} catch (error) {
console.error('Device discovery failed:', error);
}
}
private handleStudentOnline(device: distributedDeviceManager.DeviceBasicInfo): void {
const student: StudentDevice = {
deviceId: device.networkId,
deviceName: device.deviceName,
studentName: `学生${this.studentDevices.length + 1}`,
faceAREnabled: false,
bodyAREnabled: false,
lastActive: Date.now()
};
this.studentDevices.push(student);
AppStorage.setOrCreate('student_devices', this.studentDevices);
console.info(`Student online: ${device.deviceName}`);
}
private handleStudentOffline(device: distributedDeviceManager.DeviceBasicInfo): void {
this.studentDevices = this.studentDevices.filter(s => s.deviceId !== device.networkId);
AppStorage.setOrCreate('student_devices', this.studentDevices);
}
private handleStudentChanged(device: distributedDeviceManager.DeviceBasicInfo): void {
console.info(`Student device changed: ${device.deviceName}`);
}
onWindowStageDestroy(): void {
distributedDeviceManager.off('deviceStateChange');
}
}
三、核心组件实战
3.1 学生专注度实时分析引擎(FocusAnalyzer.ets)
代码亮点:基于 Face AR 的 BlendShape 参数,实时计算每位学生的专注度分数(0-100)。综合眨眼频率(疲劳指标)、注视点稳定性(注意力指标)、头部姿态(参与度指标)三个维度,生成课堂整体的专注度热力图 。
typescript
// utils/FocusAnalyzer.ets
import { arEngine } from '@hms.core.ar.arengine';
/**
* 专注度分析结果
*/
export interface FocusAnalysis {
studentId: string;
focusScore: number; // 0-100 综合专注度
attentionLevel: number; // 0-100 注意力水平
fatigueLevel: number; // 0-100 疲劳程度
engagementLevel: number; // 0-100 参与程度
emotionState: string; // 当前情绪状态
gazeHeatmap: number[][]; // 注视点热力图
lastUpdate: number; // 最后更新时间戳
}
/**
* 课堂整体状态
*/
export interface ClassroomState {
averageFocus: number; // 平均专注度
focusDistribution: { // 专注度分布
high: number; // >80
medium: number; // 50-80
low: number; // <50
};
alertStudents: string[]; // 需要关注的学生ID
trend: 'rising' | 'stable' | 'falling'; // 整体趋势
}
export class FocusAnalyzer {
private static instance: FocusAnalyzer;
private studentData: Map<string, FocusAnalysis> = new Map();
private historyWindow: number = 60000; // 60秒历史窗口
static getInstance(): FocusAnalyzer {
if (!FocusAnalyzer.instance) {
FocusAnalyzer.instance = new FocusAnalyzer();
}
return FocusAnalyzer.instance;
}
/**
* 分析学生Face AR数据,计算专注度
*/
analyzeStudentFaceAR(
studentId: string,
faceAnchor: arEngine.ARFaceAnchor,
timestamp: number
): FocusAnalysis {
const face = faceAnchor.getFace();
if (!face) return this.getDefaultAnalysis(studentId);
const blendShapes = face.getBlendShapes();
const data = new Float32Array(blendShapes.getData());
const types = blendShapes.getTypes();
const expressions = new Map<string, number>();
types.forEach((type, i) => expressions.set(type.toString(), data[i]));
// 1. 注意力评估:注视点稳定性
const attention = this.calculateAttention(expressions, face.getLandmark());
// 2. 疲劳评估:眨眼频率和头部姿态
const fatigue = this.calculateFatigue(expressions);
// 3. 参与度评估:表情活跃度和头部转动
const engagement = this.calculateEngagement(expressions);
// 4. 情绪状态识别
const emotion = this.detectEmotion(expressions);
// 5. 综合专注度(加权平均)
const focusScore = Math.round(
attention * 0.4 + // 注意力占40%
(100 - fatigue) * 0.3 + // 非疲劳度占30%
engagement * 0.3 // 参与度占30%
);
const analysis: FocusAnalysis = {
studentId,
focusScore,
attentionLevel: attention,
fatigueLevel: fatigue,
engagementLevel: engagement,
emotionState: emotion,
gazeHeatmap: this.generateGazeHeatmap(face.getLandmark()),
lastUpdate: timestamp
};
this.studentData.set(studentId, analysis);
return analysis;
}
/**
* 计算注意力水平:基于注视点稳定性
* 注视点越稳定(在屏幕中心区域),注意力越高
*/
private calculateAttention(
expressions: Map<string, number>,
landmark: arEngine.ARLandmark
): number {
const vertices2D = landmark.getVertices2D();
const floatView = new Float32Array(vertices2D);
// 提取瞳孔中心(简化:取眼部关键点的平均)
let gazeX = 0, gazeY = 0;
const eyeIndices = [37, 38, 40, 41]; // 瞳孔周围关键点索引
eyeIndices.forEach(idx => {
gazeX += floatView[idx * 2];
gazeY += floatView[idx * 2 + 1];
});
gazeX /= eyeIndices.length;
gazeY /= eyeIndices.length;
// 归一化到0-1范围(假设摄像头分辨率为1920x1080)
const normalizedX = gazeX / 1920;
const normalizedY = gazeY / 1080;
// 屏幕中心区域(0.3-0.7)为最佳注意力区域
const centerDistance = Math.sqrt(
Math.pow(normalizedX - 0.5, 2) +
Math.pow(normalizedY - 0.5, 2)
);
// 距离中心越近,注意力越高
let attention = Math.max(0, 100 - centerDistance * 200);
// 眼睛睁大程度修正
const eyeOpen = expressions.get('EYE_WIDE_LEFT') || 0;
attention *= (0.5 + eyeOpen * 0.5); // 眼睛睁大→注意力加成
return Math.round(Math.min(100, attention));
}
/**
* 计算疲劳程度:基于眨眼频率和打哈欠
*/
private calculateFatigue(expressions: Map<string, number>): number {
const eyeBlink = expressions.get('EYE_BLINK_LEFT') || 0;
const jawOpen = expressions.get('JAW_OPEN') || 0;
const browDown = expressions.get('BROW_DOWN_LEFT') || 0;
// 频繁眨眼(>0.8)= 疲劳
const blinkFatigue = eyeBlink > 0.8 ? (eyeBlink - 0.8) * 500 : 0;
// 打哈欠(嘴张大+眉毛下垂)
const yawnFatigue = (jawOpen > 0.6 && browDown > 0.3) ? 50 : 0;
return Math.round(Math.min(100, blinkFatigue + yawnFatigue));
}
/**
* 计算参与度:基于表情活跃度和头部转动
*/
private calculateEngagement(expressions: Map<string, number>): number {
const smile = expressions.get('MOUTH_SMILE_LEFT') || 0;
const browUp = expressions.get('EYE_BROW_UP_LEFT') || 0;
const surprise = expressions.get('MOUTH_OPEN') || 0;
// 表情越丰富,参与度越高
const expressionVariety = smile + browUp + surprise;
// 归一化到0-100
return Math.round(Math.min(100, expressionVariety * 100));
}
/**
* 情绪状态识别
*/
private detectEmotion(expressions: Map<string, number>): string {
const smile = expressions.get('MOUTH_SMILE_LEFT') || 0;
const browUp = expressions.get('EYE_BROW_UP_LEFT') || 0;
const jawOpen = expressions.get('JAW_OPEN') || 0;
const browDown = expressions.get('BROW_DOWN_LEFT') || 0;
if (smile > 0.6) return '愉悦';
if (browUp > 0.7 && jawOpen > 0.3) return '惊讶';
if (browDown > 0.6) return '困惑';
if (jawOpen > 0.5) return '疲惫';
return '平静';
}
/**
* 生成注视点热力图(10x10网格)
*/
private generateGazeHeatmap(landmark: arEngine.ARLandmark): number[][] {
const heatmap: number[][] = Array(10).fill(0).map(() => Array(10).fill(0));
const vertices2D = landmark.getVertices2D();
const floatView = new Float32Array(vertices2D);
// 简化为单点热力
const gazeX = floatView[37 * 2] / 1920; // 归一化
const gazeY = floatView[37 * 2 + 1] / 1080;
const gridX = Math.floor(gazeX * 10);
const gridY = Math.floor(gazeY * 10);
if (gridX >= 0 && gridX < 10 && gridY >= 0 && gridY < 10) {
heatmap[gridY][gridX] = 1.0;
// 扩散到周围
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const nx = gridX + dx, ny = gridY + dy;
if (nx >= 0 && nx < 10 && ny >= 0 && ny < 10) {
heatmap[ny][nx] = Math.max(heatmap[ny][nx], 0.5);
}
}
}
}
return heatmap;
}
/**
* 获取课堂整体状态
*/
getClassroomState(): ClassroomState {
const analyses = Array.from(this.studentData.values());
if (analyses.length === 0) {
return {
averageFocus: 0,
focusDistribution: { high: 0, medium: 0, low: 0 },
alertStudents: [],
trend: 'stable'
};
}
const scores = analyses.map(a => a.focusScore);
const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
const high = scores.filter(s => s > 80).length;
const medium = scores.filter(s => s >= 50 && s <= 80).length;
const low = scores.filter(s => s < 50).length;
const alertStudents = analyses
.filter(a => a.focusScore < 50 || a.fatigueLevel > 70)
.map(a => a.studentId);
// 计算趋势(与30秒前比较)
const trend = this.calculateTrend(avg);
return {
averageFocus: Math.round(avg),
focusDistribution: { high, medium, low },
alertStudents,
trend
};
}
private calculateTrend(currentAvg: number): 'rising' | 'stable' | 'falling' {
// 简化实现,实际应比较历史数据
return 'stable';
}
private getDefaultAnalysis(studentId: string): FocusAnalysis {
return {
studentId,
focusScore: 50,
attentionLevel: 50,
fatigueLevel: 0,
engagementLevel: 50,
emotionState: '未知',
gazeHeatmap: Array(10).fill(0).map(() => Array(10).fill(0)),
lastUpdate: Date.now()
};
}
getStudentAnalysis(studentId: string): FocusAnalysis | undefined {
return this.studentData.get(studentId);
}
getAllAnalyses(): FocusAnalysis[] {
return Array.from(this.studentData.values());
}
}
3.2 教师手势控制面板(TeacherGesturePanel.ets)
代码亮点:教师通过 Body AR 手势实现无接触式课堂操控------左手抬起上一页、右手抬起下一页、双手张开展开互动菜单、捏合缩放课件。悬浮导航根据手势状态动态变形,讲课时隐藏为边缘光点,需要时展开为完整工具栏 。
typescript
// components/TeacherGesturePanel.ets
import { window } from '@kit.ArkUI';
/**
* 教师手势指令
*/
export enum TeacherGesture {
IDLE = 'idle',
LEFT_HAND_UP = 'left_hand_up', // 左手抬起:上一页
RIGHT_HAND_UP = 'right_hand_up', // 右手抬起:下一页
BOTH_HANDS_UP = 'both_hands_up', // 双手抬起:展开菜单
PINCH = 'pinch', // 捏合:缩放课件
SPREAD = 'spread', // 张开:全屏展示
POINT_LEFT = 'point_left', // 左手指示:激光笔
POINT_RIGHT = 'point_right' // 右手指示:激光笔
}
/**
* 课件控制指令
*/
export interface SlideCommand {
type: 'prev' | 'next' | 'zoom_in' | 'zoom_out' | 'fullscreen' | 'laser';
param?: number;
timestamp: number;
}
@Component
export struct TeacherGesturePanel {
@State currentGesture: TeacherGesture = TeacherGesture.IDLE;
@State gestureConfidence: number = 0;
@State navExpanded: boolean = false;
@State slideIndex: number = 0;
@State totalSlides: number = 20;
@State zoomLevel: number = 1.0;
@State laserPosition: { x: number; y: number } | null = null;
@State currentSubject: string = 'mathematics';
@State classroomFocus: number = 75;
@State alertCount: number = 0;
// 学科光效配置
private subjectLights: Record<string, { primary: string; ambient: string }> = {
mathematics: { primary: '#4A90E2', ambient: '#1E3A5F' },
literature: { primary: '#F5A623', ambient: '#5C3D1E' },
science: { primary: '#2ECC71', ambient: '#1E4D2B' },
history: { primary: '#8B4513', ambient: '#3D2817' },
art: { primary: '#9B59B6', ambient: '#4A235A' }
};
aboutToAppear(): void {
this.setupGestureListening();
this.setupClassroomListening();
}
private setupGestureListening(): void {
AppStorage.watch('body_gesture', (gesture: { type: string; confidence: number; hands: { left: boolean; right: boolean } }) => {
if (!gesture) return;
this.gestureConfidence = gesture.confidence;
const mappedGesture = this.mapToTeacherGesture(gesture);
if (mappedGesture !== this.currentGesture) {
this.currentGesture = mappedGesture;
this.executeGestureCommand(mappedGesture);
}
});
AppStorage.watch('body_posture', (posture: { type: string; angle: number }) => {
// 身体前倾=激光笔模式
if (posture?.type === 'lean_forward') {
this.currentGesture = TeacherGesture.POINT_RIGHT;
}
});
}
private setupClassroomListening(): void {
AppStorage.watch('classroom_state', (state: { averageFocus: number; alertStudents: string[] }) => {
if (state) {
this.classroomFocus = state.averageFocus;
this.alertCount = state.alertStudents.length;
}
});
AppStorage.watch('current_subject', (subject: string) => {
if (subject) this.currentSubject = subject;
});
}
/**
* 将Body AR手势映射为教师指令
*/
private mapToTeacherGesture(gesture: { type: string; hands: { left: boolean; right: boolean } }): TeacherGesture {
const { type, hands } = gesture;
if (type === 'both_hands_up' || (hands.left && hands.right)) {
return TeacherGesture.BOTH_HANDS_UP;
}
if (hands.left && !hands.right) {
return TeacherGesture.LEFT_HAND_UP;
}
if (hands.right && !hands.left) {
return TeacherGesture.RIGHT_HAND_UP;
}
if (type === 'pinch') {
return TeacherGesture.PINCH;
}
if (type === 'spread') {
return TeacherGesture.SPREAD;
}
return TeacherGesture.IDLE;
}
/**
* 执行手势对应的课件控制指令
*/
private executeGestureCommand(gesture: TeacherGesture): void {
const now = Date.now();
switch (gesture) {
case TeacherGesture.LEFT_HAND_UP:
if (this.slideIndex > 0) {
this.slideIndex--;
this.broadcastSlideCommand({ type: 'prev', timestamp: now });
this.triggerFeedback('上一页');
}
break;
case TeacherGesture.RIGHT_HAND_UP:
if (this.slideIndex < this.totalSlides - 1) {
this.slideIndex++;
this.broadcastSlideCommand({ type: 'next', timestamp: now });
this.triggerFeedback('下一页');
}
break;
case TeacherGesture.BOTH_HANDS_UP:
this.navExpanded = !this.navExpanded;
this.triggerFeedback(this.navExpanded ? '展开菜单' : '收起菜单');
break;
case TeacherGesture.PINCH:
this.zoomLevel = Math.max(0.5, this.zoomLevel - 0.1);
this.broadcastSlideCommand({ type: 'zoom_out', param: this.zoomLevel, timestamp: now });
break;
case TeacherGesture.SPREAD:
this.zoomLevel = Math.min(2.0, this.zoomLevel + 0.1);
this.broadcastSlideCommand({ type: 'zoom_in', param: this.zoomLevel, timestamp: now });
break;
case TeacherGesture.POINT_RIGHT:
// 激光笔模式:跟随手指位置
this.broadcastSlideCommand({ type: 'laser', timestamp: now });
break;
}
}
private broadcastSlideCommand(command: SlideCommand): void {
AppStorage.setOrCreate('slide_command', command);
// 微震动反馈
try {
import('@kit.SensorServiceKit').then(sensor => {
sensor.vibrator.startVibration({ type: 'time', duration: 20 }, { id: 0 });
});
} catch (error) {
console.error('Haptic feedback failed:', error);
}
}
private triggerFeedback(message: string): void {
AppStorage.setOrCreate('gesture_feedback', { message, timestamp: Date.now() });
}
build() {
const lightConfig = this.subjectLights[this.currentSubject] || this.subjectLights.mathematics;
Stack({ alignContent: Alignment.Bottom }) {
// 内容占位
Column() {}
.width('100%')
.height('100%')
// 手势反馈浮层
if (this.currentGesture !== TeacherGesture.IDLE) {
this.buildGestureFeedback()
}
// 悬浮导航面板(可展开/收起)
Column() {
Stack() {
// 学科光效背景
Column()
.width('100%')
.height('100%')
.backgroundColor(lightConfig.primary)
.opacity(0.15)
.blur(60)
Column()
.width('100%')
.height('100%')
.backgroundBlurStyle(BlurStyle.COMPONENT_THICK)
.opacity(0.9)
Column()
.width('100%')
.height('100%')
.linearGradient({
direction: GradientDirection.Top,
colors: [
['rgba(255,255,255,0.15)', 0.0],
['rgba(255,255,255,0.05)', 0.5],
['transparent', 1.0]
]
})
}
.width('100%')
.height('100%')
.borderRadius(24)
.shadow({
radius: 20,
color: lightConfig.primary + '40',
offsetX: 0,
offsetY: -4
})
// 面板内容
Column({ space: 12 }) {
// 顶部:课堂状态概览
this.buildClassroomStatus(lightConfig)
// 中部:课件控制(展开时显示)
if (this.navExpanded) {
this.buildSlideControls(lightConfig)
}
// 底部:手势状态指示
this.buildGestureIndicator()
}
.width('100%')
.padding(16)
}
.width(this.navExpanded ? '60%' : '40%')
.height(this.navExpanded ? 200 : 100)
.margin({ bottom: 24, left: '20%', right: '20%' })
.animation({
duration: 300,
curve: Curve.Spring
})
// 点击展开/收起
.onClick(() => {
this.navExpanded = !this.navExpanded;
})
}
.width('100%')
.height('100%')
}
@Builder
buildClassroomStatus(lightConfig: { primary: string }): void {
Row({ space: 16 }) {
// 专注度仪表盘
Row({ space: 8 }) {
Stack() {
// 背景圆环
Column()
.width(48)
.height(48)
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(24)
// 进度圆弧(简化)
Column()
.width(48)
.height(48)
.backgroundColor(
this.classroomFocus > 80 ? '#00FF88' :
this.classroomFocus > 50 ? '#FFD700' : '#FF4444'
)
.opacity(0.3)
.borderRadius(24)
Text(`${this.classroomFocus}`)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width(48)
.height(48)
Column() {
Text('课堂专注度')
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
Text(this.classroomFocus > 80 ? '良好' : this.classroomFocus > 50 ? '一般' : '需关注')
.fontSize(10)
.fontColor(
this.classroomFocus > 80 ? '#00FF88' :
this.classroomFocus > 50 ? '#FFD700' : '#FF4444'
)
}
}
// 需关注学生数
if (this.alertCount > 0) {
Row({ space: 6 }) {
Column()
.width(8)
.height(8)
.backgroundColor('#FF4444')
.borderRadius(4)
.animation({
duration: 1000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
Text(`${this.alertCount}人需关注`)
.fontSize(12)
.fontColor('#FF4444')
}
.backgroundColor('rgba(255,68,68,0.1)')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(8)
}
// 当前页码
Text(`${this.slideIndex + 1}/${this.totalSlides}`)
.fontSize(14)
.fontColor('#FFFFFF')
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
}
@Builder
buildSlideControls(lightConfig: { primary: string }): void {
Row({ space: 12 }) {
// 上一页
Button() {
Image($r('app.media.ic_prev'))
.width(20)
.height(20)
.fillColor('#FFFFFF')
}
.type(ButtonType.Circle)
.backgroundColor('rgba(255,255,255,0.1)')
.width(44)
.height(44)
.onClick(() => {
if (this.slideIndex > 0) {
this.slideIndex--;
this.broadcastSlideCommand({ type: 'prev', timestamp: Date.now() });
}
})
// 页码指示器
Slider({
value: this.slideIndex,
min: 0,
max: this.totalSlides - 1,
step: 1
})
.width(200)
.selectedColor(lightConfig.primary)
.onChange((value: number) => {
this.slideIndex = value;
this.broadcastSlideCommand({ type: 'next', param: value, timestamp: Date.now() });
})
// 下一页
Button() {
Image($r('app.media.ic_next'))
.width(20)
.height(20)
.fillColor('#FFFFFF')
}
.type(ButtonType.Circle)
.backgroundColor('rgba(255,255,255,0.1)')
.width(44)
.height(44)
.onClick(() => {
if (this.slideIndex < this.totalSlides - 1) {
this.slideIndex++;
this.broadcastSlideCommand({ type: 'next', timestamp: Date.now() });
}
})
// 缩放控制
Row({ space: 8 }) {
Button('−')
.type(ButtonType.Circle)
.fontSize(16)
.backgroundColor('rgba(255,255,255,0.1)')
.width(36)
.height(36)
.onClick(() => {
this.zoomLevel = Math.max(0.5, this.zoomLevel - 0.1);
this.broadcastSlideCommand({ type: 'zoom_out', param: this.zoomLevel, timestamp: Date.now() });
})
Text(`${Math.round(this.zoomLevel * 100)}%`)
.fontSize(12)
.fontColor('#FFFFFF')
.width(40)
.textAlign(TextAlign.Center)
Button('+')
.type(ButtonType.Circle)
.fontSize(16)
.backgroundColor('rgba(255,255,255,0.1)')
.width(36)
.height(36)
.onClick(() => {
this.zoomLevel = Math.min(2.0, this.zoomLevel + 0.1);
this.broadcastSlideCommand({ type: 'zoom_in', param: this.zoomLevel, timestamp: Date.now() });
})
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 8, bottom: 8 })
}
@Builder
buildGestureIndicator(): void {
Row({ space: 12 }) {
// 手势图标
Text(this.getGestureEmoji(this.currentGesture))
.fontSize(24)
// 手势名称
Text(this.getGestureLabel(this.currentGesture))
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
// 置信度条
if (this.currentGesture !== TeacherGesture.IDLE) {
Row() {
Column()
.width(`${this.gestureConfidence * 100}%`)
.height(4)
.backgroundColor('#00FF88')
.borderRadius(2)
}
.width(60)
.height(4)
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(2)
}
// 操作提示
Text(this.getGestureAction(this.currentGesture))
.fontSize(11)
.fontColor('#00FF88')
.layoutWeight(1)
.textAlign(TextAlign.End)
}
.width('100%')
.height(32)
.padding({ left: 12, right: 12 })
.backgroundColor('rgba(0,0,0,0.2)')
.borderRadius(8)
}
@Builder
buildGestureFeedback(): void {
Column() {
Text(this.getGestureAction(this.currentGesture))
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.shadow({ radius: 10, color: '#000000' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.position({ x: 0, y: 0 })
.animation({
duration: 200,
curve: Curve.EaseOut
})
}
private getGestureEmoji(gesture: TeacherGesture): string {
const emojis: Record<TeacherGesture, string> = {
[TeacherGesture.IDLE]: '✋',
[TeacherGesture.LEFT_HAND_UP]: '👈',
[TeacherGesture.RIGHT_HAND_UP]: '👉',
[TeacherGesture.BOTH_HANDS_UP]: '🙌',
[TeacherGesture.PINCH]: '🤏',
[TeacherGesture.SPREAD]: '✋',
[TeacherGesture.POINT_LEFT]: '☝️',
[TeacherGesture.POINT_RIGHT]: '☝️'
};
return emojis[gesture] || '✋';
}
private getGestureLabel(gesture: TeacherGesture): string {
const labels: Record<TeacherGesture, string> = {
[TeacherGesture.IDLE]: '等待手势',
[TeacherGesture.LEFT_HAND_UP]: '左手抬起',
[TeacherGesture.RIGHT_HAND_UP]: '右手抬起',
[TeacherGesture.BOTH_HANDS_UP]: '双手抬起',
[TeacherGesture.PINCH]: '捏合',
[TeacherGesture.SPREAD]: '张开',
[TeacherGesture.POINT_LEFT]: '左手指示',
[TeacherGesture.POINT_RIGHT]: '右手指示'
};
return labels[gesture] || '等待';
}
private getGestureAction(gesture: TeacherGesture): string {
const actions: Record<TeacherGesture, string> = {
[TeacherGesture.IDLE]: '',
[TeacherGesture.LEFT_HAND_UP]: '上一页',
[TeacherGesture.RIGHT_HAND_UP]: '下一页',
[TeacherGesture.BOTH_HANDS_UP]: this.navExpanded ? '收起菜单' : '展开菜单',
[TeacherGesture.PINCH]: '缩小',
[TeacherGesture.SPREAD]: '放大',
[TeacherGesture.POINT_LEFT]: '激光笔',
[TeacherGesture.POINT_RIGHT]: '激光笔'
};
return actions[gesture] || '';
}
}
3.3 学生专注度可视化面板(StudentFocusPanel.ets)
代码亮点:右侧AR学情面板实时展示每位学生的专注度分数、情绪状态、注视点热力图。沉浸光效根据课堂整体专注度动态调整------专注度高时显示镇静蓝,出现走神时切换为警示橙提醒教师 。
typescript
// components/StudentFocusPanel.ets
import { FocusAnalysis, FocusAnalyzer } from '../utils/FocusAnalyzer';
@Component
export struct StudentFocusPanel {
@State studentAnalyses: FocusAnalysis[] = [];
@State classroomState: { averageFocus: number; alertStudents: string[] } = { averageFocus: 75, alertStudents: [] };
@State selectedStudent: string = '';
@State panelWidth: number = 360;
private analyzer: FocusAnalyzer = FocusAnalyzer.getInstance();
private updateTimer: number = 0;
aboutToAppear(): void {
this.startDataUpdate();
}
aboutToDisappear(): void {
clearInterval(this.updateTimer);
}
private startDataUpdate(): void {
this.updateTimer = setInterval(() => {
this.studentAnalyses = this.analyzer.getAllAnalyses();
this.classroomState = this.analyzer.getClassroomState();
AppStorage.setOrCreate('classroom_state', this.classroomState);
}, 1000);
}
build() {
Column({ space: 0 }) {
// 面板标题
this.buildPanelHeader()
// 课堂整体状态
this.buildClassroomOverview()
// 学生列表
List({ space: 8 }) {
ForEach(this.studentAnalyses, (analysis: FocusAnalysis) => {
ListItem() {
this.buildStudentCard(analysis)
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 12, right: 12 })
// 选中学生详情
if (this.selectedStudent) {
this.buildStudentDetail()
}
}
.width(this.panelWidth)
.height('100%')
.backgroundColor('rgba(20, 20, 30, 0.95)')
.borderRadius({ topLeft: 16, bottomLeft: 16 })
.shadow({ radius: 16, color: 'rgba(0,0,0,0.3)', offsetX: -4, offsetY: 0 })
}
@Builder
buildPanelHeader(): void {
Row({ space: 8 }) {
Image($r('app.media.ic_students'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
Text('学生状态')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.layoutWeight(1)
Text(`${this.studentAnalyses.length}人在线`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.5)')
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor('rgba(255,255,255,0.05)')
}
@Builder
buildClassroomOverview(): void {
Column({ space: 12 }) {
// 平均专注度大数字
Row({ space: 16 }) {
Column({ space: 4 }) {
Text(`${this.classroomState.averageFocus}`)
.fontSize(48)
.fontWeight(FontWeight.Bold)
.fontColor(
this.classroomState.averageFocus > 80 ? '#00FF88' :
this.classroomState.averageFocus > 50 ? '#FFD700' : '#FF4444'
)
Text('平均专注度')
.fontSize(12)
.fontColor('rgba(255,255,255,0.5)')
}
.layoutWeight(1)
// 专注度分布饼图(简化)
Column() {
Row({ space: 4 }) {
Column()
.width(12)
.height(12)
.backgroundColor('#00FF88')
.borderRadius(6)
Text('高')
.fontSize(10)
.fontColor('#FFFFFF')
}
Row({ space: 4 }) {
Column()
.width(12)
.height(12)
.backgroundColor('#FFD700')
.borderRadius(6)
Text('中')
.fontSize(10)
.fontColor('#FFFFFF')
}
Row({ space: 4 }) {
Column()
.width(12)
.height(12)
.backgroundColor('#FF4444')
.borderRadius(6)
Text('低')
.fontSize(10)
.fontColor('#FFFFFF')
}
}
}
.width('100%')
.padding(16)
// 趋势指示
if (this.classroomState.alertStudents.length > 0) {
Row({ space: 8 }) {
Image($r('app.media.ic_alert'))
.width(16)
.height(16)
.fillColor('#FF4444')
Text(`${this.classroomState.alertStudents.length}位学生需要关注`)
.fontSize(13)
.fontColor('#FF4444')
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.backgroundColor('rgba(255,68,68,0.1)')
.borderRadius(8)
}
}
.width('100%')
.padding({ bottom: 12 })
.backgroundColor('rgba(255,255,255,0.02)')
}
@Builder
buildStudentCard(analysis: FocusAnalysis): void {
Column({ space: 8 }) {
Row({ space: 12 }) {
// 学生头像(带专注度色环)
Stack() {
Column()
.width(44)
.height(44)
.backgroundColor(
analysis.focusScore > 80 ? '#00FF8820' :
analysis.focusScore > 50 ? '#FFD70020' : '#FF444420'
)
.borderRadius(22)
.borderWidth(2)
.borderColor(
analysis.focusScore > 80 ? '#00FF88' :
analysis.focusScore > 50 ? '#FFD700' : '#FF4444'
)
Text(analysis.studentId.slice(-2))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width(44)
.height(44)
// 学生信息
Column({ space: 4 }) {
Text(analysis.studentId)
.fontSize(14)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Medium)
Row({ space: 8 }) {
Text(`专注 ${analysis.focusScore}`)
.fontSize(11)
.fontColor(
analysis.focusScore > 80 ? '#00FF88' :
analysis.focusScore > 50 ? '#FFD700' : '#FF4444'
)
Text(analysis.emotionState)
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
.backgroundColor('rgba(255,255,255,0.08)')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
// 展开按钮
Button() {
Image($r('app.media.ic_expand'))
.width(16)
.height(16)
.fillColor('rgba(255,255,255,0.5)')
}
.type(ButtonType.Circle)
.backgroundColor('transparent')
.width(32)
.height(32)
.onClick(() => {
this.selectedStudent = this.selectedStudent === analysis.studentId ? '' : analysis.studentId;
})
}
.width('100%')
// 注视点热力图(简化)
if (this.selectedStudent === analysis.studentId) {
Column({ space: 8 }) {
Text('注视点分布')
.fontSize(12)
.fontColor('rgba(255,255,255,0.5)')
Grid() {
ForEach(analysis.gazeHeatmap.flat(), (value: number, index: number) => {
GridItem() {
Column()
.width('100%')
.height('100%')
.backgroundColor(`rgba(255,255,0,${value * 0.8})`)
}
})
}
.width('100%')
.height(100)
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.columnsGap(1)
.rowsGap(1)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(8)
// 详细指标
Row({ space: 16 }) {
this.buildMetricItem('注意力', analysis.attentionLevel, '#4A90E2')
this.buildMetricItem('疲劳', analysis.fatigueLevel, '#FF4444')
this.buildMetricItem('参与', analysis.engagementLevel, '#00FF88')
}
.width('100%')
.padding({ top: 8 })
}
.width('100%')
.padding({ top: 8 })
}
}
.width('100%')
.padding(12)
.backgroundColor('rgba(255,255,255,0.03)')
.borderRadius(12)
.onClick(() => {
this.selectedStudent = this.selectedStudent === analysis.studentId ? '' : analysis.studentId;
})
}
@Builder
buildMetricItem(label: string, value: number, color: string): void {
Column({ space: 4 }) {
Text(`${value}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(color)
Text(label)
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.layoutWeight(1)
}
@Builder
buildStudentDetail(): void {
Column() {
// 选中学生详情(简化)
}
.width('100%')
.height(200)
.backgroundColor('rgba(255,255,255,0.05)')
}
}
3.4 主课堂页面:沉浸光效与教学场景融合
typescript
// pages/TeacherClassroomPage.ets
import { TeacherGesturePanel } from '../components/TeacherGesturePanel';
import { StudentFocusPanel } from '../components/StudentFocusPanel';
import { FocusAnalyzer } from '../utils/FocusAnalyzer';
@Entry
@Component
struct TeacherClassroomPage {
@State currentSubject: string = 'mathematics';
@State classroomFocus: number = 75;
@State slideCommand: { type: string; param?: number } | null = null;
@State ambientColor: string = '#1E3A5F';
private analyzer: FocusAnalyzer = FocusAnalyzer.getInstance();
aboutToAppear(): void {
AppStorage.watch('classroom_state', (state: { averageFocus: number }) => {
if (state) {
this.classroomFocus = state.averageFocus;
this.updateAmbientColor();
}
});
AppStorage.watch('current_subject', (subject: string) => {
if (subject) this.currentSubject = subject;
});
AppStorage.watch('slide_command', (cmd: { type: string; param?: number }) => {
this.slideCommand = cmd;
});
}
private updateAmbientColor(): void {
// 根据课堂专注度调整环境光色
if (this.classroomFocus > 80) {
this.ambientColor = '#1E4D2B'; // 绿色:专注良好
} else if (this.classroomFocus > 50) {
this.ambientColor = '#5C3D1E'; // 橙色:需要关注
} else {
this.ambientColor = '#4A1E1E'; // 红色:严重走神
}
}
build() {
Row({ space: 0 }) {
// 左侧:课件展示区(70%)
Stack() {
// 动态环境光背景
this.buildAmbientBackground()
// 课件内容(简化)
Column() {
Text('课件展示区')
.fontSize(24)
.fontColor('#FFFFFF40')
if (this.slideCommand) {
Text(`指令: ${this.slideCommand.type}`)
.fontSize(14)
.fontColor('#00FF88')
.margin({ top: 12 })
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
// 教师手势面板(悬浮)
TeacherGesturePanel()
}
.layoutWeight(7)
// 右侧:AR学情面板(30%)
StudentFocusPanel()
}
.width('100%')
.height('100%')
.backgroundColor('#0a0a12')
.expandSafeArea(
[SafeAreaType.SYSTEM],
[SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM, SafeAreaEdge.START, SafeAreaEdge.END]
)
}
@Builder
buildAmbientBackground(): void {
Column() {
// 基础环境光
Column()
.width('100%')
.height('100%')
.backgroundColor(this.ambientColor)
.opacity(0.15)
.blur(100)
// 专注度脉冲光效
Column()
.width('100%')
.height('100%')
.backgroundColor(
this.classroomFocus > 80 ? '#00FF88' :
this.classroomFocus > 50 ? '#FFD700' : '#FF4444'
)
.opacity(0.05)
.blur(80)
.animation({
duration: 3000,
curve: Curve.EaseInOut,
iterations: -1,
playMode: PlayMode.Alternate
})
// 学科主题色微光
Column()
.width(600)
.height(600)
.backgroundColor(
this.currentSubject === 'mathematics' ? '#4A90E2' :
this.currentSubject === 'literature' ? '#F5A623' :
this.currentSubject === 'science' ? '#2ECC71' : '#FFFFFF'
)
.blur(200)
.opacity(0.08)
.position({ x: '50%', y: '50%' })
.anchor('50%')
}
.width('100%')
.height('100%')
}
}
四、关键技术总结
4.1 Face AR 专注度分析体系
| 维度 | AR参数 | 计算方法 | 权重 |
|---|---|---|---|
| 注意力 | 注视点稳定性、眼睛睁大程度 | 瞳孔中心距屏幕中心距离 | 40% |
| 疲劳度 | 眨眼频率、打哈欠 | 频繁眨眼(>0.8)+嘴张大+眉毛下垂 | 30% |
| 参与度 | 表情丰富度 | 微笑+挑眉+惊讶的综合活跃度 | 30% |
4.2 Body AR 教师手势指令映射
| 手势 | 识别条件 | 教学功能 | 视觉反馈 |
|---|---|---|---|
| 左手抬起 | 左腕高于左肩 | 上一页 | 左箭头光效 |
| 右手抬起 | 右腕高于右肩 | 下一页 | 右箭头光效 |
| 双手抬起 | 双腕同时高于肩 | 展开/收起菜单 | 菜单动画 |
| 捏合 | 双手距离<80px | 缩小课件 | 缩放动画 |
| 张开 | 双手距离>200px | 放大课件 | 缩放动画 |
| 身体前倾 | 鼻-肩距>60px | 激光笔模式 | 光标跟随 |
4.3 沉浸光效与教学状态联动
| 课堂状态 | 环境光色 | 光效强度 | 触发条件 |
|---|---|---|---|
| 专注良好 | 镇静绿 #1E4D2B | 0.15 | 平均专注度>80 |
| 需要关注 | 警示橙 #5C3D1E | 0.2 | 平均专注度50-80 |
| 严重走神 | 警示红 #4A1E1E | 0.25 | 平均专注度<50 |
| 学科主题 | 学科色 | 0.08 | 切换学科时 |
五、调试与部署建议
5.1 调试要点
- 多设备协同测试:确保教师端PC与学生端手机/平板的分布式连接稳定
- 光线条件优化:学生端需避免逆光,确保Face AR追踪精度
- 手势识别校准:不同教师的肢体习惯不同,需提供个性化阈值调节
- 隐私合规:所有面部数据端侧处理,不上传云端,符合教育数据安全规范
5.2 部署场景
| 场景 | 设备配置 | 功能重点 |
|---|---|---|
| 大班直播课 | 教师PC + 学生手机 | 专注度统计、自动提醒 |
| 小班互动课 | 教师PC + 学生平板 | 个体关注、手势互动 |
| 双师课堂 | 主讲PC + 助教PC | 学情同步、协作授课 |
| 智慧教室 | 教师PC + 教室大屏 | 全班可视化、沉浸式光效 |
六、总结与展望
本文基于 HarmonyOS 6(API 23)的 Face AR 、Body AR 、悬浮导航 与沉浸光感四大特性,完整实战了一款面向教师的"智能互动课堂"授课系统。核心创新点:
- 实时学情感知:通过Face AR分析学生注视点稳定性、疲劳度和参与度,生成课堂专注度热力图,让教师"看见"学生的理解状态
- 无接触式操控:通过Body AR手势实现课件翻页、缩放、激光笔等功能,教师无需回到讲台即可控制课堂
- 情境感知导航:悬浮导航根据教学阶段(讲课/互动/复习)自动切换形态,沉浸光效根据课堂专注度动态变色,打造"懂教学"的智能界面
- 分布式课堂协同:通过分布式软总线连接教师端与学生端,实现跨设备的AR数据实时同步
未来扩展方向:
- AI教学助手:基于课堂专注度数据,AI自动推荐教学节奏调整策略
- 情绪预警系统:检测学生困惑/焦虑情绪,自动推送辅助解释内容
- 课后学情报告:生成每位学生的专注度曲线、情绪变化趋势,辅助个性化教学
- VR课堂融合:将AR数据同步至VR头显,打造全息沉浸式课堂
转载自:https://blog.csdn.net/u014727709/article/details/148799952
欢迎 👍点赞✍评论⭐收藏,欢迎指正