在移动设备上使用传感器管理器测量步数

如本指南所述,使用传感器管理器在移动应用中填充步骤数据。如需详细了解如何设计和管理锻炼应用界面,请参阅构建基本的健身应用

快速入门

如需开始在移动设备上测量基本计步器的步骤,您需要将相关依赖项添加到您的应用模块 build.gradle 文件中。确保您使用的是最新版本的依赖项。此外,当您将应用支持扩展到其他设备规格(例如 Wear OS)时,请添加这些设备规格所需的依赖项。

下面是一些界面依赖项的一些示例。如需查看完整列表,请参阅此界面元素指南。

implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.activity:activity-compose")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material")

获取计步器传感器

在用户授予必要的运动状态识别权限后,您就可以访问计步器传感器:

  1. getSystemService() 获取 SensorManager 对象。
  2. SensorManager 获取计步器传感器:
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

有些设备没有计步器传感器。您应检查是否存在传感器,如果设备没有传感器,则显示错误消息:

if (sensor == null) {
    Text(text = "Step counter sensor is not present on this device")
}

创建前台服务

在基本的健身应用中,您可能会有一个按钮,用于接收来自用户的开始和停止事件,以跟踪步数。

请谨记传感器最佳实践。 特别是,计步器传感器应仅在传感器监听器已注册时计算步数。通过将传感器注册与前台服务相关联,可以根据需要注册传感器,并且当应用不在前台时,传感器可以保持注册状态。

使用以下代码段在前台服务的 onPause() 方法中取消注册传感器:

override fun onPause() {
    super.onPause()
    sensorManager.unregisterListener(this)
}

分析事件数据

如需访问传感器数据,请实现 SensorEventListener 接口。请注意,您应将传感器注册与前台服务的生命周期相关联,并在服务暂停或结束时取消注册传感器。以下代码段展示了如何为 Sensor.TYPE_STEP_COUNTER 实现 SensorEventListener 接口:

private const val TAG = "STEP_COUNT_LISTENER"

context(Context)
class StepCounter {
    private val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)

    suspend fun steps() = suspendCancellableCoroutine { continuation ->
        Log.d(TAG, "Registering sensor listener... ")

        val listener: SensorEventListener by lazy {
            object : SensorEventListener {
                override fun onSensorChanged(event: SensorEvent?) {
                    if (event == null) return

                    val stepsSinceLastReboot = event.values[0].toLong()
                    Log.d(TAG, "Steps since last reboot: $stepsSinceLastReboot")

                    if (continuation.isActive) {
                        continuation.resume(stepsSinceLastReboot)
                    }
                }

                override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
                      Log.d(TAG, "Accuracy changed to: $accuracy")
                }
            }
       }

        val supportedAndEnabled = sensorManager.registerListener(listener,
                sensor, SensorManager.SENSOR_DELAY_UI)
        Log.d(TAG, "Sensor listener registered: $supportedAndEnabled")
    }
}

为传感器事件创建数据库

您的应用可能会显示一个屏幕,在该屏幕中,用户可以随时查看自己的步数。如需在您的应用中提供此功能,请使用 Room 持久性库

以下代码段会创建一个表,其中包含一组步数测量结果,以及您的应用访问每个测量结果的时间:

@Entity(tableName = "steps")
data class StepCount(
  @ColumnInfo(name = "steps") val steps: Long,
  @ColumnInfo(name = "created_at") val createdAt: String,
)

创建数据访问对象 (DAO) 以读取和写入数据:

@Dao
interface StepsDao {
    @Query("SELECT * FROM steps")
    suspend fun getAll(): List<StepCount>

    @Query("SELECT * FROM steps WHERE created_at >= date(:startDateTime) " +
            "AND created_at < date(:startDateTime, '+1 day')")
    suspend fun loadAllStepsFromToday(startDateTime: String): Array<StepCount>

    @Insert
    suspend fun insertAll(vararg steps: StepCount)

    @Delete
    suspend fun delete(steps: StepCount)
}

如需实例化 DAO,请创建一个 RoomDatabase 对象:

@Database(entities = [StepCount::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun stepsDao(): StepsDao
}

将传感器数据存储在数据库中

ViewModel 使用新的 StepCounter 类,因此您可以在阅读完步骤后立即将其存储:

viewModelScope.launch {
    val stepsFromLastBoot = stepCounter.steps()
    repository.storeSteps(stepsFromLastBoot)
}

repository 类将如下所示:

class Repository(
    private val stepsDao: StepsDao,
) {

    suspend fun storeSteps(stepsSinceLastReboot: Long) = withContext(Dispatchers.IO) {
        val stepCount = StepCount(
            steps = stepsSinceLastReboot,
            createdAt = Instant.now().toString()
        )
        Log.d(TAG, "Storing steps: $stepCount")
        stepsDao.insertAll(stepCount)
    }

    suspend fun loadTodaySteps(): Long = withContext(Dispatchers.IO) {
        printTheWholeStepsTable() // DEBUG

        val todayAtMidnight = (LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT).toString())
        val todayDataPoints = stepsDao.loadAllStepsFromToday(startDateTime = todayAtMidnight)
        when {
            todayDataPoints.isEmpty() -> 0
            else -> {
                val firstDataPointOfTheDay = todayDataPoints.first()
                val latestDataPointSoFar = todayDataPoints.last()

                val todaySteps = latestDataPointSoFar.steps - firstDataPointOfTheDay.steps
                Log.d(TAG, "Today Steps: $todaySteps")
                todaySteps
            }
        }
    }
}


定期检索传感器数据

如果您使用前台服务,则无需配置 WorkManager,因为在应用主动跟踪用户步数期间,应用中应该会显示更新后的总步数。

但是,如果您想要批量处理步数记录,可以使用 WorkManager 以特定的时间间隔测量步数,例如每 15 分钟一次。WorkManager 是用于执行后台工作以确保执行有保证的组件。如需了解详情,请参阅 WorkManager Codelab

如需配置 Worker 对象以检索数据,请替换 doWork() 方法,如以下代码段所示:

private const val TAG = " StepCounterWorker"

@HiltWorker
class StepCounterWorker @AssistedInject constructor(
    @Assisted appContext: Context,
    @Assisted workerParams: WorkerParameters,
    val repository: Repository,
    val stepCounter: StepCounter
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        Log.d(TAG, "Starting worker...")

        val stepsSinceLastReboot = stepCounter.steps().first()
        if (stepsSinceLastReboot == 0L) return Result.success()

        Log.d(TAG, "Received steps from step sensor: $stepsSinceLastReboot")
        repository.storeSteps(stepsSinceLastReboot)

        Log.d(TAG, "Stopping worker...")
        return Result.success()
    }
}

如需设置 WorkManager 以每 15 分钟存储一次当前步数,请执行以下操作:

  1. 扩展 Application 类以实现 Configuration.Provider 接口。
  2. onCreate() 方法中,将 PeriodicWorkRequestBuilder 加入队列。

此过程显示在以下代码段中:

@HiltAndroidApp
@RequiresApi(Build.VERSION_CODES.S)
internal class PulseApplication : Application(), Configuration.Provider {

    @Inject
    lateinit var workerFactory: HiltWorkerFactory

    override fun onCreate() {
        super.onCreate()

        val myWork = PeriodicWorkRequestBuilder<StepCounterWorker>(
                15, TimeUnit.MINUTES).build()

        WorkManager.getInstance(this)
            .enqueueUniquePeriodicWork("MyUniqueWorkName",
                    ExistingPeriodicWorkPolicy.UPDATE, myWork)
    }

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .setMinimumLoggingLevel(android.util.Log.DEBUG)
            .build()
}

如需在应用启动时立即初始化控制对应用的计步器数据库访问权限的 content provider,请将以下元素添加到应用的清单文件中:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove" />