PAYATHON 2026

如何在 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 中如何实现?

建议先实现心率计分,再按实际需求补充基于步频或活动类型的估算。

  1. 声明并请求 READ_HEART_RATE,需要读取运动会话时再请求 READ_EXERCISE。
  2. 使用 readRecords() 分页读取目标时间范围的数据,直到 pageToken 为空。具体调用方式见读取原始数据。
  3. 按数据来源处理重复样本,选定同一时段采用的心率来源。
  4. 将样本限制在要计分的运动区间内,按持续时间累计积分。

以下 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 历史结果保存,并与后续自行计算的积分区分来源。如果备份还包含同期心率和运动记录,可以对比相同时间区间内的两套结果,排查采样缺失、时间边界和重复数据造成的差异。如果只有每日积分总数,就无法反推出唯一的计算过程。

产品中建议将新结果标明为“按公开规则估算的运动积分”,并保存算法版本,方便在后续调整采样间隔或取整方式时,追溯历史数值变化的原因。

备注:内容仅供参考。