HarmonyOS APP开发:健康数据采集与华为运动健康对接
HarmonyOS APP开发:健康数据采集与华为运动健康对接
核心要点:本文系统讲解基于HarmonyOS的健康数据采集与华为运动健康生态对接技术,涵盖多源健康数据(步数/心率/睡眠/血氧)的采集与持久化、关系型数据库存储方案、数据可视化展示,以及通过华为Health Kit接入运动健康生态的完整实现。
| 项目 | 说明 |
|---|---|
| 开发语言 | ArkTS |
| 核心API | @ohos.data.relationalStore、@ohos.health、@ohos.sensor |
一、背景与动机
健康数据是运动传感应用的"最后一公里"——传感器采集的原始数据只有经过聚合、存储、分析后,才能转化为对用户有价值的健康洞察。然而,健康数据管理面临三大挑战:
- 数据孤岛:手机、手表、手环各自采集数据,互不相通
- 数据碎片化:步数、心率、睡眠、血氧分散在不同应用中
- 数据安全:健康数据属于最敏感的个人信息,存储和传输必须加密
华为运动健康生态(Health Kit)为解决这些问题提供了系统级方案:
- 统一数据仓库:所有健康数据汇聚到华为运动健康平台
- 标准数据类型:定义了步数、心率、睡眠等标准数据格式
- 权限管控:用户完全控制哪些应用可以读写哪些健康数据
- 跨设备同步:手机、手表、平板数据自动同步
健康数据类型全景
健康数据体系
├── 运动数据
│ ├── 步数(Step Count)
│ ├── 步频(Cadence)
│ ├── 距离(Distance)
│ ├── 卡路里(Calories)
│ └── 运动类型(Activity Type)
│
├── 生理数据
│ ├── 心率(Heart Rate)
│ ├── 血氧(Blood Oxygen)
│ ├── 体温(Body Temperature)
│ └── 血压(Blood Pressure)
│
├── 睡眠数据
│ ├── 睡眠时长(Sleep Duration)
│ ├── 深睡/浅睡(Sleep Stage)
│ └── 睡眠质量评分(Sleep Score)
│
└── 体测数据
├── 体重(Body Weight)
├── 体脂率(Body Fat)
└── BMI指数
二、核心原理
2.1 健康数据采集架构
flowchart TD
subgraph 数据源
A1[加速度传感器<br/>步数/步频]
A2[心率传感器<br/>PPG光学心率]
A3[血氧传感器<br/>SpO2红外检测]
A4[华为运动健康APP<br/>系统级数据]
end
subgraph 采集层
B1[Sensor API<br/>实时数据订阅]
B2[Health Kit<br/>历史数据读取]
end
subgraph 存储层
C1[关系型数据库<br/>RDB本地存储]
C2[偏好设置<br/>用户配置]
C3[分布式数据<br/>跨设备同步]
end
subgraph 应用层
D1[数据可视化<br/>图表/仪表盘]
D2[健康报告<br/>周报/月报]
D3[智能提醒<br/>久坐/目标]
end
A1 --> B1
A2 --> B1
A3 --> B1
A4 --> B2
B1 --> C1
B2 --> C1
B1 --> C2
C1 --> C3
C1 --> D1
C1 --> D2
C2 --> D3
C3 --> D1
classDef sourceStyle fill:#1a1a2e,stroke:#e94560,color:#fff,stroke-width:2px
classDef collectStyle fill:#16213e,stroke:#0f3460,color:#e0e0e0,stroke-width:2px
classDef storeStyle fill:#0f3460,stroke:#533483,color:#fff,stroke-width:2px
classDef appStyle fill:#533483,stroke:#e94560,color:#fff,stroke-width:2px
class A1,A2,A3,A4 sourceStyle
class B1,B2 collectStyle
class C1,C2,C3 storeStyle
class D1,D2,D3 appStyle
2.2 数据库设计
健康数据采用关系型数据库(RDB)存储,核心表结构:
health_record表(健康记录主表):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键自增 |
| type | TEXT | 数据类型(STEP/HEART_RATE/SLEEP/…) |
| value | REAL | 数值 |
| unit | TEXT | 单位 |
| start_time | INTEGER | 开始时间戳 |
| end_time | INTEGER | 结束时间戳 |
| source | TEXT | 数据来源(SENSOR/HEALTH_KIT/MANUAL) |
| device_id | TEXT | 设备标识 |
| metadata | TEXT | 附加元数据(JSON格式) |
| created_at | INTEGER | 记录创建时间 |
daily_summary表(每日汇总表):
| 字段 | 类型 | 说明 |
|---|---|---|
| date | TEXT | 日期(YYYY-MM-DD) |
| total_steps | INTEGER | 总步数 |
| total_distance | REAL | 总距离(米) |
| total_calories | REAL | 总卡路里 |
| avg_heart_rate | REAL | 平均心率 |
| max_heart_rate | INTEGER | 最大心率 |
| sleep_duration | INTEGER | 睡眠时长(分钟) |
| deep_sleep | INTEGER | 深睡时长(分钟) |
| activity_minutes | INTEGER | 活动时长(分钟) |
2.3 Health Kit数据读写流程
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ 应用层 │────→│ Health Kit │────→│ 华为运动健康 │
│ (APP) │ │ Client API │ │ Platform │
└──────────┘ └──────────────┘ └──────────────┘
│ │ │
│ 1.申请权限 │ 2.鉴权+权限校验 │ 3.数据读写
│─────────────────→│────────────────────→│
│ │ │
│ 4.返回数据 │ 5.聚合/过滤 │ 6.存储/同步
│←─────────────────│←────────────────────│
三、代码实战
3.1 健康数据本地存储管理器
实现基于RDB的健康数据持久化存储:
// HealthDataStore.ets — 健康数据本地存储管理器
import relationalStore from '@ohos.data.relationalStore';
import { BusinessError } from '@ohos.base';
// 健康数据记录
export interface HealthRecord {
id?: number;
type: HealthDataType;
value: number;
unit: string;
startTime: number;
endTime: number;
source: string;
deviceId: string;
metadata?: Record<string, string>;
}
// 健康数据类型枚举
export enum HealthDataType {
STEP_COUNT = 'STEP_COUNT',
HEART_RATE = 'HEART_RATE',
BLOOD_OXYGEN = 'BLOOD_OXYGEN',
DISTANCE = 'DISTANCE',
CALORIES = 'CALORIES',
SLEEP_DURATION = 'SLEEP_DURATION',
BODY_WEIGHT = 'BODY_WEIGHT',
ACTIVITY_MINUTES = 'ACTIVITY_MINUTES',
}
// 每日汇总数据
export interface DailySummary {
date: string;
totalSteps: number;
totalDistance: number;
totalCalories: number;
avgHeartRate: number;
maxHeartRate: number;
sleepDuration: number;
deepSleep: number;
activityMinutes: number;
}
// 数据库配置
const DB_CONFIG: relationalStore.StoreConfig = {
name: 'HealthData.db',
securityLevel: relationalStore.SecurityLevel.S3, // S3级安全(健康数据)
encrypt: true, // 加密存储
};
// 建表SQL
const CREATE_HEALTH_RECORD_TABLE = `
CREATE TABLE IF NOT EXISTS health_record (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
value REAL NOT NULL,
unit TEXT NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER NOT NULL,
source TEXT NOT NULL DEFAULT 'SENSOR',
device_id TEXT NOT NULL DEFAULT '',
metadata TEXT,
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
)
`;
const CREATE_DAILY_SUMMARY_TABLE = `
CREATE TABLE IF NOT EXISTS daily_summary (
date TEXT PRIMARY KEY,
total_steps INTEGER DEFAULT 0,
total_distance REAL DEFAULT 0,
total_calories REAL DEFAULT 0,
avg_heart_rate REAL DEFAULT 0,
max_heart_rate INTEGER DEFAULT 0,
sleep_duration INTEGER DEFAULT 0,
deep_sleep INTEGER DEFAULT 0,
activity_minutes INTEGER DEFAULT 0
)
`;
// 索引SQL
const CREATE_INDEXES = [
'CREATE INDEX IF NOT EXISTS idx_health_type ON health_record(type)',
'CREATE INDEX IF NOT EXISTS idx_health_start_time ON health_record(start_time)',
'CREATE INDEX IF NOT EXISTS idx_health_type_time ON health_record(type, start_time)',
];
@ObservedV2
export class HealthDataStore {
// 数据库实例
private store: relationalStore.RdbStore | null = null;
// 数据库是否就绪
@Trace isReady: boolean = false;
/**
* 初始化数据库
*/
async init(context: Context): Promise<void> {
try {
this.store = await relationalStore.getRdbStore(context, DB_CONFIG);
// 创建表
await this.store.executeSql(CREATE_HEALTH_RECORD_TABLE);
await this.store.executeSql(CREATE_DAILY_SUMMARY_TABLE);
// 创建索引
for (const sql of CREATE_INDEXES) {
await this.store.executeSql(sql);
}
this.isReady = true;
console.info('[HealthDataStore] 数据库初始化成功');
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthDataStore] 数据库初始化失败: ${e.message}`);
}
}
/**
* 插入健康数据记录
*/
async insertRecord(record: HealthRecord): Promise<number> {
if (!this.store) return -1;
try {
const valueBucket: relationalStore.ValuesBucket = {
type: record.type,
value: record.value,
unit: record.unit,
start_time: record.startTime,
end_time: record.endTime,
source: record.source,
device_id: record.deviceId,
metadata: record.metadata ? JSON.stringify(record.metadata) : null,
};
const rowId = await this.store.insert('health_record', valueBucket);
console.info(`[HealthDataStore] 插入记录成功, rowId=${rowId}`);
return rowId;
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthDataStore] 插入记录失败: ${e.message}`);
return -1;
}
}
/**
* 批量插入健康数据记录
*/
async batchInsertRecords(records: HealthRecord[]): Promise<number> {
if (!this.store || records.length === 0) return 0;
try {
const valueBuckets: relationalStore.ValuesBucket[] = records.map(record => ({
type: record.type,
value: record.value,
unit: record.unit,
start_time: record.startTime,
end_time: record.endTime,
source: record.source,
device_id: record.deviceId,
metadata: record.metadata ? JSON.stringify(record.metadata) : null,
}));
const count = await this.store.batchInsert('health_record', valueBuckets);
console.info(`[HealthDataStore] 批量插入${count}条记录`);
return count;
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthDataStore] 批量插入失败: ${e.message}`);
return 0;
}
}
/**
* 查询指定时间范围的健康数据
*/
async queryRecords(
type: HealthDataType,
startTime: number,
endTime: number
): Promise<HealthRecord[]> {
if (!this.store) return [];
try {
const predicates = new relationalStore.RdbPredicates('health_record');
predicates.equalTo('type', type)
.between('start_time', startTime, endTime)
.orderByAsc('start_time');
const resultSet = await this.store.query(predicates);
const records: HealthRecord[] = [];
while (resultSet.goToNextRow()) {
records.push({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
type: resultSet.getString(resultSet.getColumnIndex('type')) as HealthDataType,
value: resultSet.getDouble(resultSet.getColumnIndex('value')),
unit: resultSet.getString(resultSet.getColumnIndex('unit')),
startTime: resultSet.getLong(resultSet.getColumnIndex('start_time')),
endTime: resultSet.getLong(resultSet.getColumnIndex('end_time')),
source: resultSet.getString(resultSet.getColumnIndex('source')),
deviceId: resultSet.getString(resultSet.getColumnIndex('device_id')),
metadata: resultSet.getString(resultSet.getColumnIndex('metadata'))
? JSON.parse(resultSet.getString(resultSet.getColumnIndex('metadata')))
: undefined,
});
}
resultSet.close();
return records;
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthDataStore] 查询失败: ${e.message}`);
return [];
}
}
/**
* 获取今日汇总数据
*/
async getTodaySummary(): Promise<DailySummary> {
const today = new Date();
const dateStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
const dayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
const dayEnd = dayStart + 86400000; // 24小时
// 查询各类型数据并汇总
const steps = await this.queryRecords(HealthDataType.STEP_COUNT, dayStart, dayEnd);
const heartRates = await this.queryRecords(HealthDataType.HEART_RATE, dayStart, dayEnd);
const distances = await this.queryRecords(HealthDataType.DISTANCE, dayStart, dayEnd);
const calories = await this.queryRecords(HealthDataType.CALORIES, dayStart, dayEnd);
const sleeps = await this.queryRecords(HealthDataType.SLEEP_DURATION, dayStart, dayEnd);
const totalSteps = steps.reduce((sum, r) => sum + r.value, 0);
const totalDistance = distances.reduce((sum, r) => sum + r.value, 0);
const totalCalories = calories.reduce((sum, r) => sum + r.value, 0);
const avgHR = heartRates.length > 0
? heartRates.reduce((sum, r) => sum + r.value, 0) / heartRates.length : 0;
const maxHR = heartRates.length > 0
? Math.max(...heartRates.map(r => r.value)) : 0;
const sleepDuration = sleeps.reduce((sum, r) => sum + r.value, 0);
return {
date: dateStr,
totalSteps: Math.round(totalSteps),
totalDistance: Math.round(totalDistance),
totalCalories: Math.round(totalCalories * 10) / 10,
avgHeartRate: Math.round(avgHR),
maxHeartRate: Math.round(maxHR),
sleepDuration: Math.round(sleepDuration),
deepSleep: 0, // 需要睡眠分期数据
activityMinutes: 0, // 需要活动检测数据
};
}
/**
* 获取最近7天汇总数据
*/
async getWeeklySummary(): Promise<DailySummary[]> {
const summaries: DailySummary[] = [];
const now = new Date();
for (let i = 6; i >= 0; i--) {
const date = new Date(now);
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const dayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
const dayEnd = dayStart + 86400000;
const steps = await this.queryRecords(HealthDataType.STEP_COUNT, dayStart, dayEnd);
const distances = await this.queryRecords(HealthDataType.DISTANCE, dayStart, dayEnd);
const calories = await this.queryRecords(HealthDataType.CALORIES, dayStart, dayEnd);
summaries.push({
date: dateStr,
totalSteps: Math.round(steps.reduce((sum, r) => sum + r.value, 0)),
totalDistance: Math.round(distances.reduce((sum, r) => sum + r.value, 0)),
totalCalories: Math.round(calories.reduce((sum, r) => sum + r.value, 0) * 10) / 10,
avgHeartRate: 0,
maxHeartRate: 0,
sleepDuration: 0,
deepSleep: 0,
activityMinutes: 0,
});
}
return summaries;
}
/**
* 删除指定时间范围的数据
*/
async deleteRecords(type: HealthDataType, startTime: number, endTime: number): Promise<number> {
if (!this.store) return 0;
try {
const predicates = new relationalStore.RdbPredicates('health_record');
predicates.equalTo('type', type).between('start_time', startTime, endTime);
const count = await this.store.delete(predicates);
return count;
} catch (error) {
return 0;
}
}
/**
* 关闭数据库
*/
async close(): Promise<void> {
if (this.store) {
try {
await relationalStore.deleteRdbStore('HealthData.db');
} catch (e) { /* 忽略 */ }
this.store = null;
this.isReady = false;
}
}
}
3.2 华为运动健康Kit对接
实现与华为Health Kit的数据读写对接:
// HealthKitManager.ets — 华为运动健康Kit对接管理器
import health from '@ohos.health';
import { BusinessError } from '@ohos.base';
// Health Kit权限类型
export enum HealthPermissionType {
STEP_COUNT = 'ohos.permission.READ_HEALTH_DATA:STEP_COUNT',
HEART_RATE = 'ohos.permission.READ_HEALTH_DATA:HEART_RATE',
BLOOD_OXYGEN = 'ohos.permission.READ_HEALTH_DATA:BLOOD_OXYGEN',
DISTANCE = 'ohos.permission.READ_HEALTH_DATA:DISTANCE',
CALORIES = 'ohos.permission.READ_HEALTH_DATA:CALORIES',
SLEEP = 'ohos.permission.READ_HEALTH_DATA:SLEEP',
}
// Health Kit数据记录
export interface HealthKitRecord {
dataType: health.HealthDataType;
startTime: number;
endTime: number;
value: number;
unit: string;
}
@ObservedV2
export class HealthKitManager {
// 权限状态
@Trace permissionsGranted: boolean = false;
@Trace isAvailable: boolean = false;
// 需要申请的权限列表
private readonly requiredPermissions: string[] = [
HealthPermissionType.STEP_COUNT,
HealthPermissionType.HEART_RATE,
HealthPermissionType.BLOOD_OXYGEN,
HealthPermissionType.DISTANCE,
HealthPermissionType.CALORIES,
HealthPermissionType.SLEEP,
];
/**
* 检查Health Kit可用性
*/
async checkAvailability(): Promise<boolean> {
try {
// 检查设备是否支持Health Kit
this.isAvailable = true; // 简化判断
return this.isAvailable;
} catch (e) {
this.isAvailable = false;
return false;
}
}
/**
* 申请Health Kit权限
*/
async requestPermissions(): Promise<boolean> {
try {
// 在实际应用中,需要通过AbilityContext申请权限
// 此处为简化示例
console.info('[HealthKit] 正在申请健康数据权限...');
// 检查已有权限
for (const permission of this.requiredPermissions) {
// 实际应使用 abilityAccessCtrl.checkAccessToken 检查
console.info(`[HealthKit] 检查权限: ${permission}`);
}
this.permissionsGranted = true;
console.info('[HealthKit] 权限申请成功');
return true;
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthKit] 权限申请失败: ${e.message}`);
this.permissionsGranted = false;
return false;
}
}
/**
* 读取今日步数数据
*/
async readTodayStepCount(): Promise<number> {
if (!this.permissionsGranted) {
console.warn('[HealthKit] 未获得步数读取权限');
return 0;
}
try {
const today = new Date();
const startTime = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
const endTime = Date.now();
// 使用Health Kit查询步数
const result = await health.query({
dataType: health.HealthDataType.STEP_COUNT,
startTime: startTime,
endTime: endTime,
});
let totalSteps = 0;
if (result && Array.isArray(result)) {
for (const record of result) {
totalSteps += record.value ?? 0;
}
}
console.info(`[HealthKit] 今日步数: ${totalSteps}`);
return totalSteps;
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthKit] 读取步数失败: ${e.message}`);
return 0;
}
}
/**
* 读取心率数据
*/
async readHeartRate(startTime: number, endTime: number): Promise<HealthKitRecord[]> {
if (!this.permissionsGranted) return [];
try {
const result = await health.query({
dataType: health.HealthDataType.HEART_RATE,
startTime: startTime,
endTime: endTime,
});
const records: HealthKitRecord[] = [];
if (result && Array.isArray(result)) {
for (const item of result) {
records.push({
dataType: health.HealthDataType.HEART_RATE,
startTime: item.startTime ?? 0,
endTime: item.endTime ?? 0,
value: item.value ?? 0,
unit: 'bpm',
});
}
}
return records;
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthKit] 读取心率失败: ${e.message}`);
return [];
}
}
/**
* 写入步数数据到Health Kit
*/
async writeStepCount(steps: number, startTime: number, endTime: number): Promise<boolean> {
if (!this.permissionsGranted) return false;
try {
await health.insert({
dataType: health.HealthDataType.STEP_COUNT,
startTime: startTime,
endTime: endTime,
value: steps,
});
console.info(`[HealthKit] 写入步数成功: ${steps}`);
return true;
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthKit] 写入步数失败: ${e.message}`);
return false;
}
}
/**
* 写入运动记录到Health Kit
*/
async writeWorkoutRecord(
activityType: string,
startTime: number,
endTime: number,
steps: number,
distance: number,
calories: number
): Promise<boolean> {
if (!this.permissionsGranted) return false;
try {
// 写入步数
if (steps > 0) {
await health.insert({
dataType: health.HealthDataType.STEP_COUNT,
startTime: startTime,
endTime: endTime,
value: steps,
});
}
// 写入距离
if (distance > 0) {
await health.insert({
dataType: health.HealthDataType.DISTANCE,
startTime: startTime,
endTime: endTime,
value: distance,
});
}
// 写入卡路里
if (calories > 0) {
await health.insert({
dataType: health.HealthDataType.CALORIES,
startTime: startTime,
endTime: endTime,
value: calories,
});
}
console.info('[HealthKit] 运动记录写入成功');
return true;
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthKit] 运动记录写入失败: ${e.message}`);
return false;
}
}
/**
* 订阅健康数据变更
*/
async subscribeHealthDataChange(
dataType: health.HealthDataType,
callback: (record: HealthKitRecord) => void
): Promise<void> {
try {
health.on('healthDataChange', {
dataType: dataType,
}, (data) => {
callback({
dataType: data.dataType,
startTime: data.startTime ?? 0,
endTime: data.endTime ?? 0,
value: data.value ?? 0,
unit: '',
});
});
console.info(`[HealthKit] 已订阅数据变更: ${dataType}`);
} catch (error) {
const e = error as BusinessError;
console.error(`[HealthKit] 订阅数据变更失败: ${e.message}`);
}
}
/**
* 取消订阅健康数据变更
*/
async unsubscribeHealthDataChange(dataType: health.HealthDataType): Promise<void> {
try {
health.off('healthDataChange', {
dataType: dataType,
});
} catch (e) { /* 忽略 */ }
}
}
3.3 健康数据仪表盘界面
实现健康数据可视化展示,包含今日汇总、7天趋势图和数据详情:
// HealthDashboardPage.ets — 健康数据仪表盘
import { HealthDataStore, HealthRecord, HealthDataType, DailySummary } from './HealthDataStore';
import { HealthKitManager, HealthKitRecord } from './HealthKitManager';
import sensor from '@ohos.sensor';
@Entry
@Component
struct HealthDashboardPage {
// 管理器
private dataStore: HealthDataStore = new HealthDataStore();
private healthKit: HealthKitManager = new HealthKitManager();
// 今日数据
@State todaySteps: number = 0;
@State todayDistance: number = 0;
@State todayCalories: number = 0;
@State avgHeartRate: number = 0;
@State maxHeartRate: number = 0;
@State sleepHours: number = 0;
@State bloodOxygen: number = 0;
@State stepGoal: number = 10000;
@State stepProgress: number = 0;
// 7天数据
@State weeklySteps: number[] = [0, 0, 0, 0, 0, 0, 0];
@State weeklyLabels: string[] = [];
// 心率实时数据
@State currentHeartRate: number = 0;
@State isHeartRateMonitoring: boolean = false;
// UI状态
@State isLoading: boolean = true;
@State activeTab: number = 0;
// 心率订阅ID
private heartRateSubscribeId: number = -1;
async aboutToAppear(): Promise<void> {
// 初始化数据库
await this.dataStore.init(getContext(this));
// 检查Health Kit
await this.healthKit.checkAvailability();
if (this.healthKit.isAvailable) {
await this.healthKit.requestPermissions();
}
// 加载今日数据
await this.loadTodayData();
await this.loadWeeklyData();
this.isLoading = false;
}
build() {
Navigation() {
Column({ space: 0 }) {
// 标签栏
this.TabBar()
// 内容区
if (this.isLoading) {
this.LoadingView()
} else {
if (this.activeTab === 0) {
this.TodayOverview()
} else {
this.WeeklyTrend()
}
}
}
.width('100%')
.height('100%')
}
.title('健康数据')
.titleMode(NavigationTitleMode.Mini)
}
// ===== 标签栏 =====
@Builder
TabBar() {
Row({ space: 0 }) {
ForEach(['今日概览', '7天趋势'], (label: string, index: number) => {
Column() {
Text(label)
.fontSize(15)
.fontColor(this.activeTab === index ? '#e94560' : '#78909C')
.fontWeight(this.activeTab === index ? FontWeight.Bold : FontWeight.Normal)
// 指示条
Row()
.width(24)
.height(3)
.borderRadius(2)
.backgroundColor(this.activeTab === index ? '#e94560' : 'transparent')
.margin({ top: 6 })
}
.layoutWeight(1)
.padding({ top: 12, bottom: 12 })
.onClick(() => { this.activeTab = index as number; })
})
}
.width('100%')
.backgroundColor('rgba(15, 52, 96, 0.6)')
}
// ===== 今日概览 =====
@Builder
TodayOverview() {
Scroll() {
Column({ space: 16 }) {
// 步数环形进度
this.StepProgressCard()
// 健康数据卡片网格
this.HealthDataGrid()
// 心率监测卡片
this.HeartRateCard()
// 睡眠数据卡片
this.SleepCard()
// 数据来源信息
this.DataSourceInfo()
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 40 })
}
.scrollBar(BarState.Off)
}
// ===== 步数环形进度 =====
@Builder
StepProgressCard() {
Column({ space: 16 }) {
Stack() {
// 背景圆环
Circle()
.width(200)
.height(200)
.fill('transparent')
.stroke('#1a1a2e')
.strokeWidth(14)
// 进度圆环
Circle()
.width(200)
.height(200)
.fill('transparent')
.stroke('#e94560')
.strokeWidth(14)
.strokeDasharray({
lineDash: [
Math.PI * 200 * Math.min(this.stepProgress, 1),
Math.PI * 200
]
})
// 中心数据
Column({ space: 4 }) {
Text(this.todaySteps.toString())
.fontSize(44)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.fontFamily('HarmonyOS Sans')
Text(`/ ${this.stepGoal} 步`)
.fontSize(13)
.fontColor('#78909C')
Text(`${Math.round(this.stepProgress * 100)}%`)
.fontSize(12)
.fontColor('#e94560')
.margin({ top: 4 })
}
}
// 副数据
Row({ space: 24 }) {
this.MiniStat('📏', `${(this.todayDistance / 1000).toFixed(1)}`, 'km')
this.MiniStat('🔥', `${this.todayCalories.toFixed(0)}`, 'kcal')
}
}
.width('100%')
.padding(24)
.borderRadius(24)
.backgroundColor('rgba(15, 52, 96, 0.6)')
.backdropBlur(20)
.alignItems(HorizontalAlign.Center)
}
@Builder
MiniStat(icon: string, value: string, unit: string) {
Column({ space: 4 }) {
Text(icon)
.fontSize(20)
Row({ space: 2 }) {
Text(value)
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Text(unit)
.fontSize(10)
.fontColor('#546E7A')
}
}
}
// ===== 健康数据卡片网格 =====
@Builder
HealthDataGrid() {
Grid() {
GridItem() {
this.HealthCard('❤️', '心率', `${this.avgHeartRate}`, 'bpm',
this.avgHeartRate >= 60 && this.avgHeartRate <= 100 ? '#00E676' : '#FF5252')
}
GridItem() {
this.HealthCard('🫁', '血氧', `${this.bloodOxygen}`, '%',
this.bloodOxygen >= 95 ? '#00E676' : '#FF5252')
}
GridItem() {
this.HealthCard('📏', '距离', `${(this.todayDistance / 1000).toFixed(1)}`, 'km', '#4FC3F7')
}
GridItem() {
this.HealthCard('🔥', '卡路里', `${this.todayCalories.toFixed(0)}`, 'kcal', '#FFB74D')
}
}
.columnsTemplate('1fr 1fr')
.rowsTemplate('1fr 1fr')
.width('100%')
.height(200)
.columnsGap(10)
.rowsGap(10)
}
@Builder
HealthCard(icon: string, label: string, value: string, unit: string, color: string) {
Column({ space: 8 }) {
Text(icon)
.fontSize(24)
Text(label)
.fontSize(11)
.fontColor('#78909C')
Row({ space: 2 }) {
Text(value)
.fontSize(20)
.fontColor(color)
.fontWeight(FontWeight.Bold)
Text(unit)
.fontSize(10)
.fontColor('#546E7A')
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
// ===== 心率监测卡片 =====
@Builder
HeartRateCard() {
Column({ space: 12 }) {
Row({ space: 8 }) {
Text('❤️')
.fontSize(20)
Text('心率监测')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Blank()
Button(this.isHeartRateMonitoring ? '停止' : '开始')
.height(28)
.fontSize(12)
.fontColor(this.isHeartRateMonitoring ? '#FF5252' : '#00E676')
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(14)
.onClick(() => this.toggleHeartRateMonitoring())
}
.width('100%')
if (this.isHeartRateMonitoring) {
// 实时心率显示
Row({ space: 12 }) {
Text('当前心率')
.fontSize(13)
.fontColor('#78909C')
Text(`${this.currentHeartRate}`)
.fontSize(36)
.fontColor('#FF5252')
.fontWeight(FontWeight.Bold)
Text('BPM')
.fontSize(12)
.fontColor('#546E7A')
}
// 心率区间指示
Row({ space: 4 }) {
ForEach([
{ label: '静息', range: [60, 80], color: '#4FC3F7' },
{ label: '燃脂', range: [80, 120], color: '#00E676' },
{ label: '有氧', range: [120, 150], color: '#FFB74D' },
{ label: '无氧', range: [150, 180], color: '#FF5252' },
], (zone: { label: string; range: number[]; color: string }) => {
Column() {
Text(zone.label)
.fontSize(9)
.fontColor('#78909C')
Row()
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor(
this.currentHeartRate >= zone.range[0] && this.currentHeartRate < zone.range[1]
? zone.color : `${zone.color}33`
)
}
})
}
} else {
Text('点击"开始"监测实时心率')
.fontSize(13)
.fontColor('#546E7A')
}
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
// ===== 睡眠数据卡片 =====
@Builder
SleepCard() {
Column({ space: 12 }) {
Row({ space: 8 }) {
Text('🌙')
.fontSize(20)
Text('睡眠数据')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
Row({ space: 24 }) {
Column({ space: 4 }) {
Text(`${this.sleepHours.toFixed(1)}`)
.fontSize(28)
.fontColor('#7C4DFF')
.fontWeight(FontWeight.Bold)
Text('小时')
.fontSize(11)
.fontColor('#78909C')
}
Column({ space: 4 }) {
Text(this.sleepHours >= 7 ? '充足' : this.sleepHours >= 5 ? '不足' : '严重不足')
.fontSize(14)
.fontColor(this.sleepHours >= 7 ? '#00E676' : this.sleepHours >= 5 ? '#FFB74D' : '#FF5252')
.fontWeight(FontWeight.Bold)
Text('睡眠评价')
.fontSize(11)
.fontColor('#78909C')
}
}
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
// ===== 数据来源信息 =====
@Builder
DataSourceInfo() {
Row({ space: 8 }) {
Text('📡')
.fontSize(14)
Text(this.healthKit.isAvailable ? '数据来源:本地传感器 + 华为运动健康' : '数据来源:本地传感器')
.fontSize(11)
.fontColor('#546E7A')
}
.width('100%')
.padding(12)
.borderRadius(10)
.backgroundColor('rgba(83, 52, 131, 0.2)')
}
// ===== 7天趋势 =====
@Builder
WeeklyTrend() {
Scroll() {
Column({ space: 16 }) {
Text('📊 近7天步数趋势')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
// 柱状图
Row({ space: 8 }) {
ForEach(this.weeklySteps, (steps: number, index: number) => {
Column({ space: 4 }) {
// 数值标签
Text(`${Math.round(steps / 1000)}k`)
.fontSize(9)
.fontColor('#78909C')
// 柱子
Column()
.width(32)
.height(`${Math.max(steps / this.stepGoal * 100, 3)}%`)
.backgroundColor(steps >= this.stepGoal ? '#00E676' : '#e94560')
.borderRadius({ topLeft: 4, topRight: 4 })
}
.height(150)
.justifyContent(FlexAlign.End)
.layoutWeight(1)
})
}
.width('100%')
// 日期标签
Row({ space: 8 }) {
ForEach(this.weeklyLabels, (label: string) => {
Text(label)
.fontSize(10)
.fontColor('#546E7A')
.layoutWeight(1)
.textAlign(TextAlign.Center)
})
}
.width('100%')
// 统计信息
this.WeeklyStatsSection()
}
.width('100%')
.padding({ left: 20, right: 20, top: 16, bottom: 40 })
}
.scrollBar(BarState.Off)
}
@Builder
WeeklyStatsSection() {
Column({ space: 12 }) {
Text('📋 本周统计')
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Row({ space: 16 }) {
this.StatItem('日均步数', `${Math.round(this.weeklySteps.reduce((a, b) => a + b, 0) / 7)}`, '步')
this.StatItem('达标天数', `${this.weeklySteps.filter(s => s >= this.stepGoal).length}`, '天')
this.StatItem('最高步数', `${Math.max(...this.weeklySteps)}`, '步')
}
}
.width('100%')
.padding(16)
.borderRadius(16)
.backgroundColor('rgba(22, 33, 62, 0.8)')
}
@Builder
StatItem(label: string, value: string, unit: string) {
Column({ space: 4 }) {
Text(label)
.fontSize(11)
.fontColor('#78909C')
Row({ space: 2 }) {
Text(value)
.fontSize(16)
.fontColor('#e94560')
.fontWeight(FontWeight.Bold)
Text(unit)
.fontSize(9)
.fontColor('#546E7A')
}
}
.layoutWeight(1)
}
// ===== 加载视图 =====
@Builder
LoadingView() {
Column() {
LoadingProgress()
.width(48)
.height(48)
.color('#e94560')
Text('加载健康数据...')
.fontSize(14)
.fontColor('#78909C')
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
// ===== 业务逻辑 =====
private async loadTodayData(): Promise<void> {
// 优先从Health Kit读取
if (this.healthKit.permissionsGranted) {
this.todaySteps = await this.healthKit.readTodayStepCount();
}
// 从本地数据库读取补充数据
if (this.dataStore.isReady) {
const summary = await this.dataStore.getTodaySummary();
if (this.todaySteps === 0) {
this.todaySteps = summary.totalSteps;
}
this.todayDistance = summary.totalDistance;
this.todayCalories = summary.totalCalories;
this.avgHeartRate = summary.avgHeartRate;
this.maxHeartRate = summary.maxHeartRate;
this.sleepHours = summary.sleepDuration / 60;
}
// 计算步数进度
this.stepProgress = Math.min(this.todaySteps / this.stepGoal, 1);
// 模拟血氧数据(实际应从传感器读取)
this.bloodOxygen = 98;
}
private async loadWeeklyData(): Promise<void> {
if (!this.dataStore.isReady) return;
const summaries = await this.dataStore.getWeeklySummary();
this.weeklySteps = summaries.map(s => s.totalSteps);
this.weeklyLabels = summaries.map(s => {
const date = new Date(s.date);
return ['日', '一', '二', '三', '四', '五', '六'][date.getDay()];
});
}
/**
* 切换心率监测
*/
private toggleHeartRateMonitoring(): void {
if (this.isHeartRateMonitoring) {
this.stopHeartRateMonitoring();
} else {
this.startHeartRateMonitoring();
}
}
private startHeartRateMonitoring(): void {
this.isHeartRateMonitoring = true;
try {
this.heartRateSubscribeId = sensor.on(sensor.SensorId.HEART_RATE,
(data: sensor.HeartRateResponse) => {
this.currentHeartRate = data.heartRate;
// 保存心率数据
if (this.dataStore.isReady) {
this.dataStore.insertRecord({
type: HealthDataType.HEART_RATE,
value: data.heartRate,
unit: 'bpm',
startTime: Date.now(),
endTime: Date.now(),
source: 'SENSOR',
deviceId: '',
});
}
},
{ interval: 1_000_000_000 } // 1秒采样
);
} catch (e) {
console.error('[HealthDashboard] 心率传感器订阅失败');
this.isHeartRateMonitoring = false;
}
}
private stopHeartRateMonitoring(): void {
this.isHeartRateMonitoring = false;
if (this.heartRateSubscribeId !== -1) {
try {
sensor.off(sensor.SensorId.HEART_RATE, this.heartRateSubscribeId);
} catch (e) { /* 忽略 */ }
}
}
aboutToDisappear(): void {
this.stopHeartRateMonitoring();
}
}
四、踩坑与注意事项
4.1 Health Kit权限申请流程
Health Kit权限申请比普通权限更复杂,需要经过华为审核:
申请流程:
1. 在AppGallery Connect中注册应用
2. 申请Health Kit使用权限(需说明用途)
3. 通过华为审核(通常1-3个工作日)
4. 在应用中动态申请用户授权
5. 用户同意后才能读写健康数据
⚠️ 重要:Health Kit权限不是标准的
requestPermissionsFromUser流程,需要使用专门的Health Kit授权API。
4.2 数据库安全等级选择
| 安全等级 | 适用场景 | 加密方式 |
|---|---|---|
| S1 | 公开数据 | 无加密 |
| S2 | 一般个人数据 | AES-128 |
| S3 | 敏感个人数据(健康) | AES-256 |
| S4 | 极敏感数据(金融) | AES-256+设备绑定 |
健康数据必须使用S3级安全,且
encrypt: true开启加密。
4.3 数据同步冲突处理
当本地数据和Health Kit数据冲突时:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 本地优先 | 以本地数据库为准 | 自研传感器算法更精确 |
| 远端优先 | 以Health Kit为准 | 华为手表数据更可靠 |
| 合并去重 | 按时间戳去重后合并 | 多数据源互补 |
| 最新优先 | 以最近更新的为准 | 实时性要求高 |
推荐策略:传感器实时数据写本地,Health Kit历史数据做补充,合并时按时间戳去重。
4.4 心率传感器可用性
不是所有设备都支持PPG心率传感器:
// 检查心率传感器可用性
import sensor from '@ohos.sensor';
const isHeartRateAvailable = sensor.isSensorSupported(sensor.SensorId.HEART_RATE);
if (!isHeartRateAvailable) {
console.warn('[Health] 心率传感器不可用,请使用手表设备');
// 降级方案:从Health Kit读取历史心率数据
}
4.5 数据量与性能优化
长期使用后,健康数据量会快速增长:
| 数据类型 | 每日记录数 | 30天数据量 |
|---|---|---|
| 步数 | ~100条 | ~3000条 |
| 心率 | ~86400条(1Hz) | ~260万条 |
| 血氧 | ~1440条(1次/分) | ~4.3万条 |
优化建议:
- 心率数据降采样:存储时1分钟聚合一次,只保存均值和极值
- 定期归档:超过30天的数据迁移到归档表
- 索引优化:为常用查询字段建立复合索引
- 批量写入:使用
batchInsert替代循环insert
五、HarmonyOS 6适配
5.1 Health Kit增强API
HarmonyOS 6对Health Kit进行了多项增强:
// HarmonyOS 6:Health Kit聚合查询
import health from '@ohos.health';
// 按小时聚合步数数据
const hourlySteps = await health.queryAggregated({
dataType: health.HealthDataType.STEP_COUNT,
startTime: dayStart,
endTime: dayEnd,
aggregateBy: 'hour', // 新增:按小时聚合
});
// 按天聚合心率数据
const dailyHeartRate = await health.queryAggregated({
dataType: health.HealthDataType.HEART_RATE,
startTime: weekStart,
endTime: weekEnd,
aggregateBy: 'day', // 按天聚合
statistics: ['avg', 'max', 'min', 'resting'], // 新增:统计类型
});
5.2 健康数据实时同步
HarmonyOS 6支持健康数据的跨设备实时同步:
// HarmonyOS 6:跨设备健康数据同步
import healthSync from '@ohos.health.sync';
// 开启自动同步
await healthSync.enableAutoSync({
dataTypes: [
health.HealthDataType.STEP_COUNT,
health.HealthDataType.HEART_RATE,
],
syncInterval: 300000, // 5分钟同步一次
wifiOnly: false, // 允许移动网络同步
});
// 监听同步状态
healthSync.on('syncComplete', (result) => {
console.info(`同步完成: ${result.syncedRecords}条记录`);
// 刷新UI数据
this.loadTodayData();
});
healthSync.on('syncError', (error) => {
console.error(`同步失败: ${error.message}`);
});
5.3 健康报告生成
HarmonyOS 6提供系统级健康报告生成能力:
// HarmonyOS 6:健康报告生成
import healthReport from '@ohos.health.report';
// 生成周报
const weeklyReport = await healthReport.generate({
type: 'WEEKLY',
startDate: weekStart,
endDate: weekEnd,
includeDataTypes: [
health.HealthDataType.STEP_COUNT,
health.HealthDataType.HEART_RATE,
health.HealthDataType.SLEEP,
],
language: 'zh-CN',
});
// 报告内容
console.info(`步数达标率: ${weeklyReport.stepGoalCompletion}%`);
console.info(`平均心率: ${weeklyReport.avgHeartRate} bpm`);
console.info(`睡眠评分: ${weeklyReport.sleepScore}/100`);
console.info(`健康建议: ${weeklyReport.recommendations}`);
5.4 隐私计算
HarmonyOS 6引入隐私计算框架,在不暴露原始数据的前提下进行健康分析:
// HarmonyOS 6:隐私计算
import privacyCompute from '@ohos.privacy.compute';
// 在加密数据上计算统计值(不暴露原始心率数据)
const result = await privacyCompute.aggregate({
dataType: health.HealthDataType.HEART_RATE,
operation: 'AVERAGE', // 只返回均值,不返回原始数据
timeRange: { start: weekStart, end: weekEnd },
});
console.info(`平均心率: ${result.value} bpm`); // 只有均值,无法反推原始数据
六、总结
本文完整实现了基于HarmonyOS的健康数据采集与华为运动健康对接系统,核心要点如下:
| 模块 | 关键技术 | 要点 |
|---|---|---|
| 本地存储 | RDB关系型数据库 | S3级安全+AES-256加密 |
| Health Kit | 华为健康生态对接 | 权限申请+数据读写+变更订阅 |
| 数据采集 | 多传感器+Health Kit | 传感器实时+Health Kit历史 |
| 数据可视化 | 环形进度+柱状图+心率区间 | 今日概览+7天趋势双视图 |
| 心率监测 | PPG心率传感器 | 1Hz采样+实时区间指示 |
最佳实践建议:
- 数据库必须S3级加密,健康数据泄露后果严重
- Health Kit权限需提前申请,审核周期1-3天
- 心率数据需降采样存储,1Hz全量存储30天可达260万条
- 多数据源合并需去重,按时间戳+来源去重
- 传感器不可用时降级到Health Kit,确保功能可用性
- 隐私合规是底线,健康数据的使用必须获得用户明确授权
系列总结:
本系列5篇文章从计步器、运动检测、跌倒检测、运动类型识别到健康数据采集,完整覆盖了HarmonyOS运动传感应用的开发全链路:
| 篇目 | 核心能力 | 关键API |
|---|---|---|
| 316 计步器 | 步数统计+步态分析 | ACCELEROMETER + PEDOMETER |
| 317 运动检测 | 活动状态识别 | ACCELEROMETER + GYROSCOPE + GRAVITY |
| 318 跌倒检测 | 三阶段跌倒判定+紧急求助 | ACCELEROMETER + NOTIFICATION + CALL |
| 319 运动类型 | 步行/跑步/骑行分类 | 多传感器融合 + 决策树分类 |
| 320 健康数据 | 数据采集+存储+生态对接 | RDB + Health Kit + 数据可视化 |
运动传感是HarmonyOS健康生态的基础能力,掌握这些技术将帮助你构建完整的运动健康应用。建议从系统级API(PEDOMETER、activityRecognition、Health Kit)入手,在系统能力不足时再自研算法补充。
- 点赞
- 收藏
- 关注作者
评论(0)