如何在 Android 项目中复现 Google Fit 的 Heart Point 计算逻辑?
可以实现近似计分,但无法保证与 Google Fit 完全一致
Android 项目可以根据 Google 公开的规则,计算类似 Heart Points 的积分。不过,公开文档没有给出完整的计算流程,多设备数据的合并、心率缺失的处理和积分取整方式也未说明。因此,仅凭公开规则,无法保证每天算出的积分都与 Google Fit 一致。
关于停服,需要区分新项目注册和既有集成:不能为新项目注册 API,不等于所有既有集成都已停止服务。Google 的迁移指南写明,Google Fit APIs 的支持将持续到 2026 年底,并将 Health Connect 列为 Android 本地健康数据场景的迁移路径。
Google 公开了哪些计分规则?
根据 Google Fit 官方帮助,使用心率数据时,积分取决于心率占估算最大心率的比例:
estimatedMaxHeartRate = 205.8 - 0.685 × age
heartRateRatio = heartRate / estimatedMaxHeartRate
| 运动时的心率强度 | Heart Points |
|---|---|
| 中等强度,估算最大心率的 50% 至 69% | 每分钟 1 分 |
| 高强度,估算最大心率的 70% 及以上 | 每分钟 2 分 |
实现时,可以将中等强度写成连续区间 0.50 <= ratio < 0.70,高强度写成 ratio >= 0.70。这是将公开的分区规则转换为程序判断条件;官方帮助没有说明百分比如何舍入。
没有心率数据时,Google Fit 也会通过活动识别计分。例如,骑行每分钟得 1 分,快走达到每分钟 100 步得 1 分,跑步达到每分钟 130 步得 2 分。公开的 MET 规则则是:3.0 至 5.9 METs 每分钟 1 分,6.0 METs 及以上每分钟 2 分。
这些规则使用不同的数据判断运动强度,同一段运动按心率、步频和 MET 算出的积分不能直接相加。
Health Connect 能否直接返回 Heart Points?
Health Connect 没有与 Google Fit com.google.heart_minutes 同名、且保证计算语义一致的数据类型。
它目前支持 ActivityIntensityRecord,可以记录中等或高强度活动,并提供 INTENSITY_MINUTES_TOTAL 等聚合指标。使用前,需要检查 FEATURE_ACTIVITY_INTENSITY,确认用户已授权,并且已有数据来源写入记录。这个指标不能直接当作 Google Fit 原有的 Heart Points。
自行计算时,通常需要读取以下记录:
HeartRateRecord:带时间戳的心率样本。ExerciseSessionRecord:运动类型及时间范围。StepsRecord:在缺少心率时,为步频估算提供数据。
对应的权限和字段以 Health Connect 数据类型文档为准。获得读取权限后,仍需确认目标记录是否实际存在;Google Fit 的全部历史数据不会因此自动出现在 Health Connect 中。
Android 中如何实现?
建议先实现心率计分,再按实际需求补充基于步频或活动类型的估算。
- 声明并请求
READ_HEART_RATE,需要读取运动会话时再请求READ_EXERCISE。 - 使用
readRecords()分页读取目标时间范围的数据,直到pageToken为空。具体调用方式见读取原始数据。 - 按数据来源处理重复样本,选定同一时段采用的心率来源。
- 将样本限制在要计分的运动区间内,按持续时间累计积分。
以下 Kotlin 函数按心率估算积分:相邻样本之间沿用前一个心率值,超过 maxGap 的间隔不计分,最后一个样本不向后补齐,计算结果保留小数。这些近似处理由应用自行定义,Google Fit 并未公布其内部是否采用相同方式。
import androidx.health.connect.client.records.HeartRateRecord
import java.time.Duration
import java.time.Instant
fun estimateHeartPoints(
samples: List<HeartRateRecord.Sample>,
ageYears: Int,
exerciseStart: Instant,
exerciseEnd: Instant,
maxGap: Duration
): Double {
require(ageYears > 0)
require(exerciseStart < exerciseEnd)
require(maxGap > Duration.ZERO)
val maxHeartRate = 205.8 - 0.685 * ageYears
require(maxHeartRate > 0.0)
val ordered = samples.sortedBy { it.time }
// 调用前须完成来源选择和重复时间戳处理。
require(ordered.zipWithNext().all { (a, b) ->
a.time < b.time
})
return ordered.zipWithNext().sumOf { (a, b) ->
val gap = Duration.between(a.time, b.time)
if (gap > maxGap) {
return@sumOf 0.0
}
val start = maxOf(a.time, exerciseStart)
val end = minOf(b.time, exerciseEnd)
if (start >= end) {
return@sumOf 0.0
}
val ratio = a.beatsPerMinute.toDouble() / maxHeartRate
val pointsPerMinute = when {
ratio >= 0.70 -> 2.0
ratio >= 0.50 -> 1.0
else -> 0.0
}
val minutes =
Duration.between(start, end).toMillis() / 60_000.0
minutes * pointsPerMinute
}
}
maxGap 应根据设备的采样频率设置。Google Fit 没有公布处理心率缺失时使用的间隔阈值,不能将某个固定秒数当作官方规则。
按这个近似模型,20 分钟中等强度运动加上 10 分钟高强度运动,积分为:
20 × 1 + 10 × 2 = 40
哪些地方最容易导致结果不同?
不要用整次运动的平均心率判断强度。心率跨越阈值时,先求平均再计分,会丢失各强度区间的持续时间。按心率样本数量计分也不合适:设备采样频率不同,同样的运动时长可能产生完全不同的样本数。
还需要处理运动暂停、跨天区间和重复会话。按日统计时,应明确使用的时区,并据此切分区间。多个运动会话重叠时,应先合并或裁剪,避免同一段时间重复得分。没有心率数据的区间应保留为缺失,或使用单独定义的估算规则,不能默认补成中等强度。
已有备份中截至 5 月的数据,应继续作为 Google Fit 历史结果保存,并与后续自行计算的积分区分来源。如果备份还包含同期心率和运动记录,可以对比相同时间区间内的两套结果,排查采样缺失、时间边界和重复数据造成的差异。如果只有每日积分总数,就无法反推出唯一的计算过程。
产品中建议将新结果标明为“按公开规则估算的运动积分”,并保存算法版本,方便在后续调整采样间隔或取整方式时,追溯历史数值变化的原因。
备注:内容仅供参考。
