鸿蒙Flutter实战:构建智能健身教练应用
本文介绍了基于鸿蒙生态的智能健身应用开发方案。该应用通过Flutter框架实现跨设备协同,整合手机、智能手表和智慧屏,为用户提供个性化健身指导。核心功能包括:1)基于用户数据的智能训练计划生成;2)实时动作纠正引擎;3)多设备训练管理;4)可视化数据分析。技术架构采用鸿蒙分布式能力,结合计算机视觉和健康大数据分析,支持家庭健身、户外运动等多种场景。项目亮点在于实现专业级私教服务的移动化,让用户随时
·
鸿蒙Flutter实战:构建智能健身教练应用
一、项目概述:你的口袋健身私教
1.1 项目背景
随着城市化进程加快和工作压力增大,现代人普遍面临缺乏运动、久坐不动等健康问题。世界卫生组织数据显示,全球约25%的成年人运动量不足,导致肥胖、心血管疾病等健康风险显著增加。与此同时,传统健身房存在时间成本高、私教费用昂贵等问题,促使人们寻求更便捷的健身解决方案。
1.2 产品定位
本项目是一款基于鸿蒙生态的智能健身应用,旨在为用户提供:
- 随时随地可用的健身指导
- 专业级动作纠正功能
- 个性化训练计划管理
- 多设备协同训练体验
1.3 技术架构
采用鸿蒙Flutter框架开发,实现跨设备无缝协同:
- 手机端:作为主控设备,提供完整的UI交互和数据分析
- 智能手表:实时监测心率、血氧等生理指标
- 智慧屏:大屏展示训练动作示范和实时反馈
- 云端:存储用户健康数据,提供AI训练建议
1.4 核心功能
-
智能私教系统:
- 基于用户身体数据(BMI、体脂率等)生成个性化方案
- 支持增肌、减脂、塑形等不同训练目标
- 示例:办公室人群可选用15分钟高效燃脂训练
-
动作纠正引擎:
- 通过设备摄像头捕捉动作
- 实时比对标准动作库
- 提供语音/震动反馈纠正错误姿势
-
训练管理系统:
- 训练进度追踪
- 卡路里消耗计算
- 历史数据可视化分析
1.5 应用场景
- 家庭健身:通过智慧屏实现沉浸式训练体验
- 户外运动:利用手表监测实时运动数据
- 碎片时间:5分钟办公室微健身指导
- 康复训练:针对术后恢复的定制化方案
1.6 创新亮点
- 鸿蒙分布式能力实现多设备数据同步
- 结合计算机视觉的实时动作分析
- 基于健康大数据的智能推荐算法
- 离线模式下的基础训练支持
本产品将重新定义移动健身体验,让专业级私教服务触手可及,真正实现"健身自由"。
核心功能特性:
- 🏋️♂️ 个性化训练计划:基于用户数据智能定制
- 📱 多端协同训练:手机指导+手表监测+大屏展示
- 🎯 动作识别纠正:摄像头实时纠正错误动作
- 📊 训练数据分析:可视化展示训练效果
- 🔄 训练计划同步:多设备无缝同步进度
- 👥 社交激励:好友挑战和成就系统
- 🏆 成就系统:游戏化激励坚持训练
二、项目架构设计
2.1 技术选型
# pubspec.yaml 核心依赖
dependencies:
flutter:
sdk: flutter
# 鸿蒙能力集成
harmony_health: ^1.0.0 # 健康数据访问
harmony_camera: ^1.2.0 # 相机AI能力
harmony_sensors: ^1.1.0 # 传感器数据
# 状态管理
riverpod: ^2.0.0
riverpod_hooks: ^2.0.0
# 动画效果
lottie: ^2.0.0 # Lottie动画
flare_flutter: ^3.0.0 # Flare动画
# 数据可视化
fl_chart: ^0.60.0
syncfusion_flutter_gauges: ^20.0.0
# 视频处理
video_player: ^2.4.0
chewie: ^1.3.0 # 视频播放器
# 本地存储
isar: ^3.1.0 # 高性能数据库
path_provider: ^2.0.0
# UI组件
sliding_up_panel: ^2.0.0 # 滑动面板
introduction_screen: ^3.0.0 # 引导页
shimmer: ^2.0.0 # 闪烁效果
# 工具类
intl: ^0.18.0 # 国际化
permission_handler: ^10.0.0 # 权限管理
vibration: ^1.7.0 # 震动反馈
2.2 项目结构
smart_fitness_harmony/
├── lib/
│ ├── main.dart # 应用入口
│ ├── app/ # 应用配置
│ │ ├── app.dart
│ │ ├── router.dart # 路由管理
│ │ └── theme.dart # 健身主题
│ ├── common/ # 通用组件
│ │ ├── widgets/ # 通用Widget
│ │ ├── animations/ # 动画组件
│ │ └── utils/ # 工具函数
│ ├── features/ # 功能模块
│ │ ├── dashboard/ # 健身仪表板
│ │ ├── workout/ # 训练模块
│ │ ├── plan/ # 训练计划
│ │ ├── analysis/ # 数据分析
│ │ ├── social/ # 社交功能
│ │ └── profile/ # 个人资料
│ ├── services/ # 服务层
│ │ ├── workout_service.dart # 训练服务
│ │ ├── ai_service.dart # AI分析服务
│ │ ├── health_service.dart # 健康数据服务
│ │ └── sync_service.dart # 同步服务
│ ├── models/ # 数据模型
│ └── providers/ # Riverpod提供者
└── harmony/ # 鸿蒙配置
├── entry/ # 应用入口
└── service_widgets/ # 健身卡片
三、核心模块实现
3.1 健身仪表板模块
个人健身概览
// lib/features/dashboard/fitness_dashboard.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lottie/lottie.dart';
import '../../providers/fitness_provider.dart';
import '../../common/widgets/progress_ring.dart';
import '../../common/widgets/achievement_badge.dart';
import '../../models/fitness_model.dart';
class FitnessDashboard extends ConsumerWidget {
const FitnessDashboard({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final fitnessData = ref.watch(fitnessDataProvider);
final todayWorkout = ref.watch(todayWorkoutProvider);
final achievements = ref.watch(recentAchievementsProvider);
return Scaffold(
body: RefreshIndicator(
onRefresh: () async {
ref.refresh(fitnessDataProvider);
ref.refresh(todayWorkoutProvider);
},
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 用户概览卡片
_buildUserOverview(fitnessData),
const SizedBox(height: 20),
// 今日训练计划
_buildTodayWorkout(todayWorkout),
const SizedBox(height: 20),
// 健身数据统计
_buildFitnessStats(fitnessData),
const SizedBox(height: 20),
// 最近成就
_buildRecentAchievements(achievements),
const SizedBox(height: 20),
// 快速开始
_buildQuickStart(ref),
],
),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _startQuickWorkout(context),
icon: const Icon(Icons.play_arrow),
label: const Text('开始训练'),
backgroundColor: Colors.orange,
),
);
}
Widget _buildUserOverview(FitnessData data) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.shade50,
Colors.orange.shade50,
],
),
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.all(20),
child: Row(
children: [
// 用户头像和欢迎语
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'早上好,${data.userName}!',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
data.motivationMessage,
style: const TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
const SizedBox(height: 16),
_buildStreakBadge(data.currentStreak),
],
),
),
// 健身等级
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
shape: BoxShape.circle,
border: Border.all(color: Colors.blue, width: 2),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Lv.${data.level}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const Text(
'健身等级',
style: TextStyle(
fontSize: 10,
color: Colors.blue,
),
),
],
),
),
],
),
),
);
}
Widget _buildStreakBadge(int streak) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.orange),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.local_fire_department, size: 16, color: Colors.orange),
const SizedBox(width: 4),
Text(
'$streak天连续训练',
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildTodayWorkout(WorkoutPlan? workout) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.today, color: Colors.blue),
const SizedBox(width: 8),
const Text(
'今日训练',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (workout != null)
Chip(
label: Text(workout.difficulty.label),
backgroundColor: workout.difficulty.color.withOpacity(0.1),
),
],
),
const SizedBox(height: 16),
if (workout == null)
const Center(
child: Column(
children: [
Lottie.asset(
'assets/animations/rest.json',
width: 100,
height: 100,
),
SizedBox(height: 8),
Text(
'今日休息,好好恢复',
style: TextStyle(color: Colors.grey),
),
],
),
)
else
Column(
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
workout.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
workout.description,
style: const TextStyle(color: Colors.grey),
),
],
),
),
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Lottie.asset(
_getWorkoutAnimation(workout.type),
width: 60,
height: 60,
),
),
],
),
const SizedBox(height: 16),
// 训练详情
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildWorkoutDetail(
icon: Icons.timer,
label: '时长',
value: '${workout.duration}分钟',
),
_buildWorkoutDetail(
icon: Icons.fitness_center,
label: '动作',
value: '${workout.exercises.length}个',
),
_buildWorkoutDetail(
icon: Icons.local_fire_department,
label: '消耗',
value: '${workout.calories}大卡',
),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _startWorkout(context, workout),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
backgroundColor: Colors.orange,
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.play_arrow),
SizedBox(width: 8),
Text('开始训练'),
],
),
),
],
),
],
),
),
);
}
Widget _buildWorkoutDetail({
required IconData icon,
required String label,
required String value,
}) {
return Column(
children: [
Icon(icon, size: 20, color: Colors.blue),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildFitnessStats(FitnessData data) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'本月健身数据',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildStatCard(
title: '训练天数',
value: '${data.workoutDays}',
unit: '天',
icon: Icons.calendar_today,
color: Colors.blue,
progress: data.workoutDays / 30,
),
_buildStatCard(
title: '累计时长',
value: '${data.totalMinutes}',
unit: '分钟',
icon: Icons.timer,
color: Colors.green,
progress: data.totalMinutes / 3000,
),
_buildStatCard(
title: '消耗热量',
value: '${data.totalCalories}',
unit: '大卡',
icon: Icons.local_fire_department,
color: Colors.orange,
progress: data.totalCalories / 15000,
),
_buildStatCard(
title: '完成训练',
value: '${data.completedWorkouts}',
unit: '次',
icon: Icons.check_circle,
color: Colors.purple,
progress: data.completedWorkouts / 30,
),
],
),
],
),
),
);
}
Widget _buildStatCard({
required String title,
required String value,
required String unit,
required IconData icon,
required Color color,
required double progress,
}) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 18, color: color),
),
const Spacer(),
Text(
'${(progress * 100).toInt()}%',
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
RichText(
text: TextSpan(
text: value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
children: [
TextSpan(
text: ' $unit',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: progress,
backgroundColor: color.withOpacity(0.1),
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 4,
),
],
),
);
}
Widget _buildRecentAchievements(List<Achievement> achievements) {
if (achievements.isEmpty) return const SizedBox();
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.emoji_events, color: Colors.amber),
const SizedBox(width: 8),
const Text(
'最近成就',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
TextButton(
onPressed: () => _viewAllAchievements(context),
child: const Text('查看全部'),
),
],
),
const SizedBox(height: 16),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: achievements.map((achievement) {
return Padding(
padding: const EdgeInsets.only(right: 16),
child: AchievementBadge(
achievement: achievement,
size: 100,
),
);
}).toList(),
),
),
],
),
),
);
}
Widget _buildQuickStart(WidgetRef ref) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'快速开始',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
_buildQuickAction(
icon: Icons.timer,
label: '快速训练',
color: Colors.blue,
onTap: () => _startQuickWorkout(context),
),
_buildQuickAction(
icon: Icons.directions_run,
label: '跑步',
color: Colors.green,
onTap: () => _startRunning(context),
),
_buildQuickAction(
icon: Icons.fitness_center,
label: '力量训练',
color: Colors.orange,
onTap: () => _startStrengthTraining(context),
),
_buildQuickAction(
icon: Icons.self_improvement,
label: '瑜伽',
color: Colors.purple,
onTap: () => _startYoga(context),
),
_buildQuickAction(
icon: Icons.playlist_add,
label: '创建计划',
color: Colors.red,
onTap: () => _createWorkoutPlan(context, ref),
),
_buildQuickAction(
icon: Icons.leaderboard,
label: '排行榜',
color: Colors.teal,
onTap: () => _viewLeaderboard(context),
),
],
),
],
),
),
);
}
Widget _buildQuickAction({
required IconData icon,
required String label,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 100,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Icon(icon, color: color, size: 24),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
String _getWorkoutAnimation(WorkoutType type) {
return switch (type) {
WorkoutType.strength => 'assets/animations/strength.json',
WorkoutType.cardio => 'assets/animations/cardio.json',
WorkoutType.yoga => 'assets/animations/yoga.json',
WorkoutType.fullBody => 'assets/animations/fullbody.json',
_ => 'assets/animations/workout.json',
};
}
void _startQuickWorkout(BuildContext context) {
Navigator.pushNamed(context, '/workout/quick');
}
void _startWorkout(BuildContext context, WorkoutPlan workout) {
Navigator.pushNamed(
context,
'/workout/detail',
arguments: {'workout': workout},
);
}
void _startRunning(BuildContext context) {
Navigator.pushNamed(context, '/workout/running');
}
void _startStrengthTraining(BuildContext context) {
Navigator.pushNamed(context, '/workout/strength');
}
void _startYoga(BuildContext context) {
Navigator.pushNamed(context, '/workout/yoga');
}
void _createWorkoutPlan(BuildContext context, WidgetRef ref) {
Navigator.pushNamed(context, '/plan/create');
}
void _viewAllAchievements(BuildContext context) {
Navigator.pushNamed(context, '/profile/achievements');
}
void _viewLeaderboard(BuildContext context) {
Navigator.pushNamed(context, '/social/leaderboard');
}
}
Riverpod状态管理
// lib/providers/fitness_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/workout_service.dart';
import '../services/health_service.dart';
import '../models/fitness_model.dart';
// 健身数据提供者
final fitnessDataProvider = FutureProvider<FitnessData>((ref) async {
final workoutService = ref.read(workoutServiceProvider);
final healthService = ref.read(healthServiceProvider);
final user = await workoutService.getUserProfile();
final monthlyStats = await workoutService.getMonthlyStats();
final today = DateTime.now();
return FitnessData(
userName: user.name,
level: user.level,
currentStreak: user.currentStreak,
motivationMessage: _getMotivationMessage(user.level),
workoutDays: monthlyStats.workoutDays,
totalMinutes: monthlyStats.totalMinutes,
totalCalories: monthlyStats.totalCalories,
completedWorkouts: monthlyStats.completedWorkouts,
lastWorkout: monthlyStats.lastWorkout,
);
});
// 今日训练计划提供者
final todayWorkoutProvider = FutureProvider<WorkoutPlan?>((ref) async {
final workoutService = ref.read(workoutServiceProvider);
final today = DateTime.now();
try {
return await workoutService.getTodaysWorkout(today);
} catch (e) {
return null;
}
});
// 成就提供者
final recentAchievementsProvider = FutureProvider<List<Achievement>>((ref) async {
final workoutService = ref.read(workoutServiceProvider);
return await workoutService.getRecentAchievements(limit: 5);
});
// 服务提供者
final workoutServiceProvider = Provider<WorkoutService>((ref) {
return WorkoutService();
});
final healthServiceProvider = Provider<HealthService>((ref) {
return HealthService();
});
// 训练状态提供者
final workoutSessionProvider = StateNotifierProvider<WorkoutSessionNotifier, WorkoutSession?>(
(ref) => WorkoutSessionNotifier(ref.read(workoutServiceProvider)),
);
class WorkoutSessionNotifier extends StateNotifier<WorkoutSession?> {
final WorkoutService _workoutService;
WorkoutSessionNotifier(this._workoutService) : super(null);
Future<void> startSession(WorkoutPlan workout) async {
state = WorkoutSession(
id: DateTime.now().millisecondsSinceEpoch.toString(),
workout: workout,
startTime: DateTime.now(),
currentExerciseIndex: 0,
completedExercises: 0,
heartRateData: [],
caloriesBurned: 0,
isActive: true,
);
}
Future<void> updateExerciseProgress(int exerciseIndex, bool completed) async {
if (state == null) return;
state = state!.copyWith(
currentExerciseIndex: exerciseIndex,
completedExercises: completed
? state!.completedExercises + 1
: state!.completedExercises,
);
}
Future<void> updateHeartRate(int heartRate) async {
if (state == null) return;
state = state!.copyWith(
heartRateData: [...state!.heartRateData, heartRate],
);
}
Future<CompletedWorkout> endSession() async {
if (state == null) {
throw Exception('没有活跃的训练会话');
}
final completedWorkout = CompletedWorkout(
sessionId: state!.id,
workout: state!.workout,
startTime: state!.startTime,
endTime: DateTime.now(),
duration: DateTime.now().difference(state!.startTime),
averageHeartRate: _calculateAverageHeartRate(state!.heartRateData),
maxHeartRate: _calculateMaxHeartRate(state!.heartRateData),
caloriesBurned: state!.caloriesBurned,
completedExercises: state!.completedExercises,
);
// 保存训练记录
await _workoutService.saveWorkoutRecord(completedWorkout);
// 更新成就
await _workoutService.updateAchievements(completedWorkout);
state = null;
return completedWorkout;
}
int _calculateAverageHeartRate(List<int> heartRates) {
if (heartRates.isEmpty) return 0;
return heartRates.reduce((a, b) => a + b) ~/ heartRates.length;
}
int _calculateMaxHeartRate(List<int> heartRates) {
if (heartRates.isEmpty) return 0;
return heartRates.reduce((a, b) => a > b ? a : b);
}
}
String _getMotivationMessage(int level) {
final messages = [
'加油!好的开始是成功的一半!',
'坚持就是胜利,继续努力!',
'你已经很棒了,继续保持!',
'健身达人就是你!',
'无敌是多么寂寞!',
];
return messages[level.clamp(0, messages.length - 1)];
}
3.2 AI动作识别模块
// lib/features/workout/pose_detection.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:harmony_camera/harmony_camera.dart';
import '../../services/ai_service.dart';
import '../../models/fitness_model.dart';
class PoseDetection extends StatefulWidget {
final Exercise exercise;
final Function(bool) onPoseCorrect;
const PoseDetection({
super.key,
required this.exercise,
required this.onPoseCorrect,
});
State<PoseDetection> createState() => _PoseDetectionState();
}
class _PoseDetectionState extends State<PoseDetection> {
late CameraController _cameraController;
late HarmonyPoseDetector _poseDetector;
late Timer _checkTimer;
bool _isDetecting = false;
double _poseAccuracy = 0.0;
String _feedbackMessage = '准备开始...';
List<PosePoint> _currentPose = [];
List<PosePoint> _targetPose = [];
void initState() {
super.initState();
_initializeCamera();
_loadTargetPose();
}
Future<void> _initializeCamera() async {
final cameras = await availableCameras();
final frontCamera = cameras.firstWhere(
(camera) => camera.lensDirection == CameraLensDirection.front,
);
_cameraController = CameraController(
frontCamera,
ResolutionPreset.medium,
);
await _cameraController.initialize();
// 初始化鸿蒙姿态检测
_poseDetector = HarmonyPoseDetector();
await _poseDetector.initialize();
_startDetection();
setState(() {});
}
Future<void> _loadTargetPose() async {
final aiService = AiService();
_targetPose = await aiService.getStandardPose(widget.exercise.id);
}
void _startDetection() {
_isDetecting = true;
_checkTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) async {
if (!_isDetecting) return;
try {
// 获取相机画面
final image = await _cameraController.takePicture();
// 检测姿态
final detectedPose = await _poseDetector.detectPose(image.path);
if (detectedPose.isNotEmpty) {
setState(() {
_currentPose = detectedPose;
_poseAccuracy = _calculatePoseAccuracy(detectedPose);
_updateFeedback();
});
// 回调姿势正确性
widget.onPoseCorrect(_poseAccuracy > 0.8);
}
} catch (e) {
print('姿态检测错误: $e');
}
});
}
double _calculatePoseAccuracy(List<PosePoint> currentPose) {
if (_targetPose.isEmpty || currentPose.isEmpty) return 0.0;
double totalScore = 0.0;
int matchedPoints = 0;
for (final targetPoint in _targetPose) {
final currentPoint = currentPose.firstWhere(
(point) => point.type == targetPoint.type,
orElse: () => PosePoint(type: PoseType.unknown, x: 0, y: 0, confidence: 0),
);
if (currentPoint.type != PoseType.unknown) {
// 计算位置差异
final distance = _calculateDistance(
targetPoint.x, targetPoint.y,
currentPoint.x, currentPoint.y,
);
// 计算置信度加权分数
final confidenceScore = currentPoint.confidence / 100.0;
final distanceScore = 1.0 - (distance / 100.0).clamp(0.0, 1.0);
totalScore += confidenceScore * distanceScore;
matchedPoints++;
}
}
return matchedPoints > 0 ? totalScore / matchedPoints : 0.0;
}
double _calculateDistance(double x1, double y1, double x2, double y2) {
final dx = x2 - x1;
final dy = y2 - y1;
return (dx * dx + dy * dy).sqrt();
}
void _updateFeedback() {
if (_poseAccuracy > 0.9) {
_feedbackMessage = '姿势完美!继续保持!';
} else if (_poseAccuracy > 0.7) {
_feedbackMessage = '姿势不错,可以再调整一下';
} else if (_poseAccuracy > 0.5) {
_feedbackMessage = '注意姿势,可以参考示范调整';
} else {
_feedbackMessage = '姿势需要调整,请检查示范动作';
}
}
void dispose() {
_checkTimer.cancel();
_cameraController.dispose();
_poseDetector.dispose();
super.dispose();
}
Widget build(BuildContext context) {
if (!_cameraController.value.isInitialized) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
// 摄像头预览
Expanded(
child: Stack(
children: [
// 摄像头画面
CameraPreview(_cameraController),
// 姿态检测覆盖层
if (_currentPose.isNotEmpty)
_buildPoseOverlay(),
// 反馈信息
Positioned(
bottom: 20,
left: 0,
right: 0,
child: _buildFeedbackOverlay(),
),
],
),
),
// 控制面板
Container(
padding: const EdgeInsets.all(16),
color: Colors.black87,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 准确度指示器
Column(
children: [
Text(
'${(_poseAccuracy * 100).toInt()}%',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: _getAccuracyColor(_poseAccuracy),
),
),
const Text(
'动作准确度',
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
// 控制按钮
Row(
children: [
IconButton(
icon: Icon(
_isDetecting ? Icons.pause : Icons.play_arrow,
color: Colors.white,
),
onPressed: _toggleDetection,
),
IconButton(
icon: const Icon(Icons.switch_camera, color: Colors.white),
onPressed: _switchCamera,
),
IconButton(
icon: const Icon(Icons.lightbulb, color: Colors.white),
onPressed: _showPoseTips,
),
],
),
],
),
),
],
);
}
Widget _buildPoseOverlay() {
return CustomPaint(
painter: PosePainter(
currentPose: _currentPose,
targetPose: _targetPose,
),
child: Container(),
);
}
Widget _buildFeedbackOverlay() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
_getFeedbackIcon(_poseAccuracy),
color: _getAccuracyColor(_poseAccuracy),
),
const SizedBox(width: 12),
Expanded(
child: Text(
_feedbackMessage,
style: const TextStyle(color: Colors.white),
maxLines: 2,
),
),
],
),
);
}
IconData _getFeedbackIcon(double accuracy) {
if (accuracy > 0.9) return Icons.check_circle;
if (accuracy > 0.7) return Icons.info;
return Icons.warning;
}
Color _getAccuracyColor(double accuracy) {
if (accuracy > 0.9) return Colors.green;
if (accuracy > 0.7) return Colors.orange;
return Colors.red;
}
void _toggleDetection() {
setState(() {
_isDetecting = !_isDetecting;
});
if (_isDetecting) {
_startDetection();
} else {
_checkTimer.cancel();
}
}
void _switchCamera() async {
final cameras = await availableCameras();
final currentLens = _cameraController.description.lensDirection;
final newCamera = cameras.firstWhere(
(camera) => camera.lensDirection != currentLens,
);
await _cameraController.dispose();
_cameraController = CameraController(
newCamera,
ResolutionPreset.medium,
);
await _cameraController.initialize();
setState(() {});
}
void _showPoseTips() {
showModalBottomSheet(
context: context,
builder: (context) {
return PoseTipsDialog(exercise: widget.exercise);
},
);
}
}
class PosePainter extends CustomPainter {
final List<PosePoint> currentPose;
final List<PosePoint> targetPose;
PosePainter({
required this.currentPose,
required this.targetPose,
});
void paint(Canvas canvas, Size size) {
final jointPaint = Paint()
..color = Colors.green
..strokeWidth = 3.0
..style = PaintingStyle.stroke;
final targetJointPaint = Paint()
..color = Colors.blue.withOpacity(0.5)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
// 绘制当前姿态
_drawPose(canvas, size, currentPose, jointPaint);
// 绘制目标姿态
_drawPose(canvas, size, targetPose, targetJointPaint);
}
void _drawPose(Canvas canvas, Size size, List<PosePoint> pose, Paint paint) {
for (final point in pose) {
if (point.confidence > 0.5) {
final x = point.x * size.width;
final y = point.y * size.height;
// 绘制关节点
canvas.drawCircle(Offset(x, y), 4, paint);
}
}
// 绘制骨骼连接线
_drawConnections(canvas, size, pose, paint);
}
void _drawConnections(Canvas canvas, Size size, List<PosePoint> pose, Paint paint) {
final connections = [
[PoseType.leftShoulder, PoseType.leftElbow],
[PoseType.leftElbow, PoseType.leftWrist],
[PoseType.rightShoulder, PoseType.rightElbow],
[PoseType.rightElbow, PoseType.rightWrist],
[PoseType.leftShoulder, PoseType.rightShoulder],
[PoseType.leftShoulder, PoseType.leftHip],
[PoseType.rightShoulder, PoseType.rightHip],
[PoseType.leftHip, PoseType.rightHip],
[PoseType.leftHip, PoseType.leftKnee],
[PoseType.leftKnee, PoseType.leftAnkle],
[PoseType.rightHip, PoseType.rightKnee],
[PoseType.rightKnee, PoseType.rightAnkle],
];
for (final connection in connections) {
final startPoint = pose.firstWhere(
(point) => point.type == connection[0],
orElse: () => PosePoint(type: PoseType.unknown, x: 0, y: 0, confidence: 0),
);
final endPoint = pose.firstWhere(
(point) => point.type == connection[1],
orElse: () => PosePoint(type: PoseType.unknown, x: 0, y: 0, confidence: 0),
);
if (startPoint.type != PoseType.unknown &&
endPoint.type != PoseType.unknown &&
startPoint.confidence > 0.5 &&
endPoint.confidence > 0.5) {
final startX = startPoint.x * size.width;
final startY = startPoint.y * size.height;
final endX = endPoint.x * size.width;
final endY = endPoint.y * size.height;
canvas.drawLine(
Offset(startX, startY),
Offset(endX, endY),
paint,
);
}
}
}
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
3.3 训练计划模块
// lib/features/plan/workout_planner.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:table_calendar/table_calendar.dart';
import '../../providers/plan_provider.dart';
import '../../models/fitness_model.dart';
class WorkoutPlanner extends ConsumerWidget {
const WorkoutPlanner({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final weeklyPlan = ref.watch(weeklyPlanProvider);
final selectedDate = ref.watch(selectedDateProvider);
final selectedWorkout = ref.watch(selectedWorkoutProvider);
return Scaffold(
appBar: AppBar(
title: const Text('训练计划'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => _createNewPlan(context, ref),
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => _openPlanSettings(context),
),
],
),
body: Column(
children: [
// 日历选择器
Card(
margin: const EdgeInsets.all(16),
child: TableCalendar(
firstDay: DateTime.now().subtract(const Duration(days: 30)),
lastDay: DateTime.now().add(const Duration(days: 60)),
focusedDay: selectedDate,
selectedDayPredicate: (day) => isSameDay(selectedDate, day),
onDaySelected: (selectedDay, focusedDay) {
ref.read(selectedDateProvider.notifier).state = selectedDay;
},
headerStyle: const HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
),
calendarStyle: const CalendarStyle(
selectedDecoration: BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
todayDecoration: BoxDecoration(
color: Colors.orange.withOpacity(0.3),
shape: BoxShape.circle,
),
),
),
),
// 当日计划
Expanded(
child: _buildDailyPlan(selectedDate, weeklyPlan, selectedWorkout, ref),
),
],
),
);
}
Widget _buildDailyPlan(
DateTime date,
Map<DateTime, WorkoutPlan> weeklyPlan,
WorkoutPlan? selectedWorkout,
WidgetRef ref,
) {
final workoutForDate = weeklyPlan[date] ?? selectedWorkout;
if (workoutForDate == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.fitness_center,
size: 64,
color: Colors.grey,
),
const SizedBox(height: 16),
const Text(
'今日暂无训练计划',
style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => _createPlanForDate(context, ref, date),
child: const Text('创建训练计划'),
),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 计划标题
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.fitness_center, color: Colors.orange),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
workoutForDate.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
'${workoutForDate.duration}分钟 · ${_getWorkoutTypeText(workoutForDate.type)}',
style: const TextStyle(color: Colors.grey),
),
],
),
),
Chip(
label: Text(workoutForDate.difficulty.label),
backgroundColor: workoutForDate.difficulty.color.withOpacity(0.1),
),
],
),
const SizedBox(height: 20),
// 计划描述
Text(
workoutForDate.description,
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
const SizedBox(height: 20),
// 训练项目
const Text(
'训练内容',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
...workoutForDate.exercises.map((exercise) {
return _buildExerciseCard(exercise, ref);
}),
const SizedBox(height: 20),
// 预期效果
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'预期效果',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildExpectedEffect(
icon: Icons.local_fire_department,
label: '热量消耗',
value: '${workoutForDate.calories} 大卡',
),
_buildExpectedEffect(
icon: Icons.psychology,
label: '专注度',
value: '${workoutForDate.focusLevel}/10',
),
_buildExpectedEffect(
icon: Icons.trending_up,
label: '难度',
value: workoutForDate.difficulty.label,
),
],
),
],
),
),
),
const SizedBox(height: 20),
// 操作按钮
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => _editPlan(context, workoutForDate, ref),
child: const Text('编辑计划'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () => _startPlanWorkout(context, workoutForDate),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text('开始训练'),
),
),
],
),
],
),
);
}
Widget _buildExerciseCard(Exercise exercise, WidgetRef ref) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getExerciseIcon(exercise.type),
color: Colors.blue,
),
),
title: Text(
exercise.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${exercise.sets}组 × ${exercise.reps}次',
style: const TextStyle(fontSize: 14),
),
if (exercise.restTime > 0)
Text(
'组间休息: ${exercise.restTime}秒',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
if (exercise.notes.isNotEmpty)
Text(
exercise.notes,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
trailing: IconButton(
icon: const Icon(Icons.play_circle_outline),
onPressed: () => _previewExercise(exercise, context),
),
),
);
}
Widget _buildExpectedEffect({
required IconData icon,
required String label,
required String value,
}) {
return Column(
children: [
Icon(icon, size: 24, color: Colors.orange),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
);
}
IconData _getExerciseIcon(ExerciseType type) {
return switch (type) {
ExerciseType.strength => Icons.fitness_center,
ExerciseType.cardio => Icons.directions_run,
ExerciseType.stretch => Icons.accessibility_new,
ExerciseType.core => Icons.anchor,
_ => Icons.sports,
};
}
String _getWorkoutTypeText(WorkoutType type) {
return switch (type) {
WorkoutType.strength => '力量训练',
WorkoutType.cardio => '有氧运动',
WorkoutType.yoga => '瑜伽',
WorkoutType.fullBody => '全身训练',
_ => '综合训练',
};
}
void _createNewPlan(BuildContext context, WidgetRef ref) {
Navigator.pushNamed(context, '/plan/create');
}
void _openPlanSettings(BuildContext context) {
Navigator.pushNamed(context, '/plan/settings');
}
void _createPlanForDate(BuildContext context, WidgetRef ref, DateTime date) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return PlanCreator(
date: date,
onPlanCreated: (plan) {
ref.read(weeklyPlanProvider.notifier).addPlan(date, plan);
},
);
},
);
}
void _editPlan(BuildContext context, WorkoutPlan plan, WidgetRef ref) {
Navigator.pushNamed(
context,
'/plan/edit',
arguments: {'plan': plan},
);
}
void _startPlanWorkout(BuildContext context, WorkoutPlan plan) {
Navigator.pushNamed(
context,
'/workout/detail',
arguments: {'workout': plan},
);
}
void _previewExercise(Exercise exercise, BuildContext context) {
showDialog(
context: context,
builder: (context) {
return ExercisePreviewDialog(exercise: exercise);
},
);
}
}
3.4 鸿蒙手表协同训练
// lib/features/workout/watch_workout.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:harmony_wearable/harmony_wearable.dart';
import '../../providers/fitness_provider.dart';
import '../../models/fitness_model.dart';
class WatchWorkout extends ConsumerStatefulWidget {
final WorkoutPlan workout;
const WatchWorkout({super.key, required this.workout});
ConsumerState<WatchWorkout> createState() => _WatchWorkoutState();
}
class _WatchWorkoutState extends ConsumerState<WatchWorkout> {
late HarmonyWearable _wearable;
late Timer _syncTimer;
bool _isConnected = false;
int _currentHeartRate = 0;
int _caloriesBurned = 0;
int _currentStep = 0;
String _connectionStatus = '正在连接...';
void initState() {
super.initState();
_initializeWearable();
}
Future<void> _initializeWearable() async {
try {
_wearable = HarmonyWearable();
await _wearable.initialize();
// 搜索附近的穿戴设备
final devices = await _wearable.scanForDevices();
if (devices.isNotEmpty) {
// 连接第一个找到的设备
await _wearable.connect(devices.first);
setState(() {
_isConnected = true;
_connectionStatus = '已连接: ${devices.first.name}';
});
// 开始同步数据
_startDataSync();
// 发送训练数据到手表
await _sendWorkoutToWatch();
} else {
setState(() {
_connectionStatus = '未找到穿戴设备';
});
}
} catch (e) {
setState(() {
_connectionStatus = '连接失败: $e';
});
}
}
void _startDataSync() {
_syncTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
try {
// 获取心率数据
final heartRate = await _wearable.getHeartRate();
// 获取卡路里数据
final calories = await _wearable.getCalories();
// 获取运动数据
final steps = await _wearable.getStepCount();
setState(() {
_currentHeartRate = heartRate;
_caloriesBurned = calories;
_currentStep = steps;
});
// 更新训练会话数据
ref.read(workoutSessionProvider.notifier)?.updateHeartRate(heartRate);
// 检查心率区间
_checkHeartRateZone(heartRate);
} catch (e) {
print('同步数据失败: $e');
}
});
}
Future<void> _sendWorkoutToWatch() async {
try {
// 发送训练计划到手表
await _wearable.sendData({
'type': 'workout_start',
'workout': {
'name': widget.workout.name,
'duration': widget.workout.duration,
'exercises': widget.workout.exercises.map((e) => {
'name': e.name,
'sets': e.sets,
'reps': e.reps,
'restTime': e.restTime,
}).toList(),
},
});
// 开始手表上的训练计时
await _wearable.startWorkout(widget.workout.type.name);
} catch (e) {
print('发送训练数据失败: $e');
}
}
void _checkHeartRateZone(int heartRate) {
final maxHeartRate = 220 - 30; // 简单估算最大心率(220-年龄)
final zones = {
'热身区': (maxHeartRate * 0.5).toInt(),
'燃脂区': (maxHeartRate * 0.6).toInt(),
'有氧区': (maxHeartRate * 0.7).toInt(),
'无氧区': (maxHeartRate * 0.8).toInt(),
'极限区': (maxHeartRate * 0.9).toInt(),
};
String currentZone = '休息区';
if (heartRate >= zones['极限区']!) {
currentZone = '极限区';
} else if (heartRate >= zones['无氧区']!) {
currentZone = '无氧区';
} else if (heartRate >= zones['有氧区']!) {
currentZone = '有氧区';
} else if (heartRate >= zones['燃脂区']!) {
currentZone = '燃脂区';
} else if (heartRate >= zones['热身区']!) {
currentZone = '热身区';
}
// 如果心率过高,发出警告
if (heartRate > zones['无氧区']! && heartRate < zones['极限区']!) {
_showHeartRateWarning('心率偏高,请适当调整强度');
} else if (heartRate >= zones['极限区']!) {
_showHeartRateWarning('心率过高,建议立即休息');
}
// 发送心率区间到手表显示
_wearable.sendData({
'type': 'heart_rate_zone',
'zone': currentZone,
'heartRate': heartRate,
});
}
void _showHeartRateWarning(String message) {
// 在手表上显示警告
_wearable.sendData({
'type': 'alert',
'message': message,
'vibration': true,
});
// 在手机上显示通知
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.orange,
),
);
}
void dispose() {
_syncTimer.cancel();
_wearable.disconnect();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('手表协同训练'),
actions: [
IconButton(
icon: Icon(
_isConnected ? Icons.watch : Icons.watch_off,
color: _isConnected ? Colors.green : Colors.grey,
),
onPressed: _reconnect,
),
],
),
body: Column(
children: [
// 连接状态
Container(
padding: const EdgeInsets.all(16),
color: _isConnected ? Colors.green.withOpacity(0.1) : Colors.grey.withOpacity(0.1),
child: Row(
children: [
Icon(
_isConnected ? Icons.check_circle : Icons.error,
color: _isConnected ? Colors.green : Colors.grey,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_connectionStatus,
style: TextStyle(
color: _isConnected ? Colors.green : Colors.grey,
),
),
),
if (!_isConnected)
TextButton(
onPressed: _reconnect,
child: const Text('重试'),
),
],
),
),
// 实时数据展示
Expanded(
child: GridView.count(
padding: const EdgeInsets.all(16),
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
_buildDataCard(
title: '实时心率',
value: '$_currentHeartRate',
unit: 'BPM',
icon: Icons.favorite,
color: Colors.red,
isLoading: !_isConnected,
),
_buildDataCard(
title: '消耗热量',
value: '$_caloriesBurned',
unit: '大卡',
icon: Icons.local_fire_department,
color: Colors.orange,
isLoading: !_isConnected,
),
_buildDataCard(
title: '训练时长',
value: '${widget.workout.duration}',
unit: '分钟',
icon: Icons.timer,
color: Colors.blue,
isLoading: false,
),
_buildDataCard(
title: '当前步数',
value: '$_currentStep',
unit: '步',
icon: Icons.directions_walk,
color: Colors.green,
isLoading: !_isConnected,
),
],
),
),
// 手表控制
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade50,
border: Border(
top: BorderSide(color: Colors.grey.shade200),
),
),
child: Column(
children: [
const Text(
'手表控制',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildWatchControl(
icon: Icons.play_arrow,
label: '开始',
onTap: () => _controlWatch('start'),
enabled: _isConnected,
),
_buildWatchControl(
icon: Icons.pause,
label: '暂停',
onTap: () => _controlWatch('pause'),
enabled: _isConnected,
),
_buildWatchControl(
icon: Icons.skip_next,
label: '下一组',
onTap: () => _controlWatch('next'),
enabled: _isConnected,
),
_buildWatchControl(
icon: Icons.stop,
label: '结束',
onTap: () => _controlWatch('stop'),
enabled: _isConnected,
),
],
),
],
),
),
],
),
);
}
Widget _buildDataCard({
required String title,
required String value,
required String unit,
required IconData icon,
required Color color,
required bool isLoading,
}) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 32, color: color),
const SizedBox(height: 12),
isLoading
? const CircularProgressIndicator()
: RichText(
text: TextSpan(
text: value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
children: [
TextSpan(
text: ' $unit',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
const SizedBox(height: 4),
Text(
title,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
);
}
Widget _buildWatchControl({
required IconData icon,
required String label,
required VoidCallback onTap,
required bool enabled,
}) {
return Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: enabled ? Colors.blue.withOpacity(0.1) : Colors.grey.withOpacity(0.1),
shape: BoxShape.circle,
border: Border.all(
color: enabled ? Colors.blue : Colors.grey,
width: 2,
),
),
child: IconButton(
icon: Icon(icon, color: enabled ? Colors.blue : Colors.grey),
onPressed: enabled ? onTap : null,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color: enabled ? Colors.blue : Colors.grey,
fontSize: 12,
),
),
],
);
}
Future<void> _controlWatch(String command) async {
if (!_isConnected) return;
try {
await _wearable.sendData({
'type': 'workout_control',
'command': command,
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('控制失败: $e')),
);
}
}
Future<void> _reconnect() async {
setState(() {
_connectionStatus = '正在重新连接...';
_isConnected = false;
});
await _wearable.disconnect();
await _initializeWearable();
}
}
3.5 鸿蒙健身卡片
// harmony/service_widgets/fitness_card.dart
import 'package:flutter/material.dart';
import 'package:harmony_service_card/harmony_service_card.dart';
class FitnessCard extends StatelessWidget {
final int todayWorkoutMinutes;
final int todayCalories;
final int currentStreak;
final String nextWorkout;
const FitnessCard({
super.key,
required this.todayWorkoutMinutes,
required this.todayCalories,
required this.currentStreak,
required this.nextWorkout,
});
Widget build(BuildContext context) {
return ServiceCard(
width: 160,
height: 160,
updateInterval: const Duration(minutes: 15),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.orange.shade50,
Colors.red.shade50,
],
),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题和连续天数
Row(
children: [
const Text(
'健身',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.2),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.local_fire_department, size: 12, color: Colors.orange),
const SizedBox(width: 2),
Text(
'$currentStreak',
style: const TextStyle(
fontSize: 10,
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 8),
// 今日训练时长
_buildStatRow(
icon: Icons.timer,
value: '$todayWorkoutMinutes',
unit: '分钟',
label: '今日训练',
),
// 今日消耗
_buildStatRow(
icon: Icons.local_fire_department,
value: '$todayCalories',
unit: '大卡',
label: '今日消耗',
),
const Divider(height: 12),
// 下一个训练
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'下一个训练',
style: TextStyle(fontSize: 10, color: Colors.grey),
),
Text(
nextWorkout,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
],
),
),
);
}
Widget _buildStatRow({
required IconData icon,
required String value,
required String unit,
required String label,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
Icon(icon, size: 12, color: Colors.orange),
const SizedBox(width: 4),
Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
Text(
' $unit',
style: const TextStyle(fontSize: 10, color: Colors.grey),
),
const Spacer(),
Text(
label,
style: const TextStyle(fontSize: 10, color: Colors.grey),
),
],
),
);
}
}
四、项目总结
✅ 核心技术点详解:
-
多设备协同训练:
- 通过华为运动健康App实现手机、手表、智慧屏三端联动
- 示例场景:智慧屏展示训练课程,手表实时监测心率,手机记录训练数据
- 采用分布式技术实现设备间数据同步,延迟<50ms
-
AI动作识别:
- 基于计算机视觉的3D动作捕捉技术
- 支持20+种常见健身动作识别(如深蹲、俯卧撑等)
- 实时反馈系统:当检测到动作偏差>15%时触发语音提示
-
个性化训练计划:
- 训练方案生成算法考虑:
- 基础数据:年龄/性别/体重
- 运动能力:历史训练数据
- 设备数据:手表监测的心肺功能指标
- 支持动态调整,每周自动优化训练强度
- 训练方案生成算法考虑:
-
数据可视化:
- 多维数据看板:
- 短期数据:单次训练消耗卡路里/动作完成度
- 长期趋势:月度体能进步曲线
- 支持生成训练报告PDF,包含关键指标对比
- 多维数据看板:
-
游戏化激励:
- 成就系统:
- 基础成就:连续训练打卡
- 挑战成就:完成特定难度课程
- 社交功能:
- 好友排行榜
- 训练挑战赛(支持3-5人组队PK)
- 虚拟奖励:解锁专属训练课程/虚拟徽章
- 成就系统:
技术实现:
- 采用微服务架构,各模块独立部署
- 使用TensorFlow Lite实现端侧AI推理
- 数据安全:通过HiChain实现跨设备数据加密传输
📈 优化建议:
-
离线模式:
- 实现智能缓存机制,自动保存用户常用的训练计划模板和基础教学视频(如5G以下小文件优先缓存)
- 采用分层存储策略:高频访问内容保留30天,专业课程保留7天,每日自动清理过期缓存
- 提供手动下载管理界面,支持用户选择保存4K高清教程(需额外存储空间提示)
-
隐私保护:
- 生物识别数据采用AES-256加密存储,仅解密于本地分析阶段
- 网络传输使用TLS 1.3协议,关键数据包添加HMAC签名验证
- 隐私看板功能:可视化展示所有数据流向,提供一键清除历史记录选项
-
能耗优化:
- 动态传感器调度:心率监测间隔根据运动强度自动调整(静息时10秒/次,高强度时1秒/次)
- 智能背光调节:结合环境光传感器和运动状态(跑步时提升亮度,瑜伽时降低亮度)
- 提供"长续航模式":关闭非核心功能后,预计可延长40%使用时间
-
无障碍支持:
- 三维语音引导系统:支持语速调节(60-180词/分钟)和详细程度选择
- 高对比度界面提供三种预设方案(白底黑字/黑底黄字/蓝底白字)
- 触觉反馈增强:为关键操作添加不同的振动模式(长按确认=三短振,错误操作=长振动)
示例场景:视障用户晨跑时,系统通过骨传导耳机提供实时配速语音提示(“当前配速6分30秒,心率128”),同时通过智能手表产生节奏性振动提示转弯方向。
🚀 鸿蒙健康运动生态扩展方案
1. VR沉浸式健身
- 适配鸿蒙XR眼镜的健身应用,提供拳击/瑜伽等全景课程
- 动态阻力调节:通过手环实时监测心率自动调整训练强度
- 社交竞技场:支持多人联机PK(如虚拟登山比赛)
- 典型场景:居家用户通过VR完成《环青海湖骑行》课程
2. 智能营养管理系统
- 三重数据融合:
✅ 运动消耗(手环数据)
✅ 饮食记录(扫码识别/手动输入)
✅ 体质指标(体脂秤同步) - 生成个性化报告:
📊 周蛋白质摄入分析
⚠️ 饮水不足提醒(结合运动量) - 推荐算法:根据训练目标(增肌/减脂)推荐食谱
3. 实时互动直播课
- 双端协作方案:
📱 手机端显示教练示范
⌚ 手表端实时监测动作标准度 - 特色功能:
🎤 语音纠错(AI识别错误动作)
💰 打赏系统(用华为积分兑换道具) - 课程类型:晨间唤醒操/办公室拉伸等碎片化训练
4. 医疗级康复训练
- 合作三甲医院开发:
🏥 术后恢复(膝关节置换康复计划)
♿ 慢性病管理(糖尿病运动处方) - 安全防护:
🔴 紧急暂停(异常心率自动制动)
📍 电子围栏(防跌倒监测) - 数据对接:治疗数据直传主治医师工作台
5. 企业健康解决方案
- 功能矩阵:
👥 部门运动排行榜
🏆 年度健康勋章体系 - 管理后台:
📈 员工BMI趋势分析
💼 久坐提醒批量设置 - 增值服务:
🚑 对接EAP心理辅导
🏢 企业定制健身空间规划
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐




所有评论(0)