27、Jetpack Compose:在新UI框架下使用传感器

说实话,刚接触 Jetpack Compose 那会儿,我还有点不习惯。

毕竟做了好几年 XML 布局,突然换成纯 Kotlin 写 UI,总觉得少了点什么。但用了一段时间后,我发现 Compose 在处理传感器这类实时数据流时,反而比传统 View 系统更顺手。为什么呢?因为 Compose 天生就是「响应式」的——数据变了,UI 自动跟着变。这不正是传感器应用最需要的特性吗?

Compose 里的传感器思路

在传统 View 系统中,我们通常这样用传感器:

  • 在 Activity 或 Fragment 里注册 SensorEventListener
  • 在 onSensorChanged 回调里拿到数据
  • 手动更新 TextView 或自定义 View

这套流程没什么大问题,但有个痛点:生命周期管理。你得小心翼翼地处理 onResume 注册、onPause 注销,稍不留神就会内存泄漏。

在 Compose 里,我们换了个思路。利用 LaunchedEffectremember,可以把传感器数据变成 Compose 可观察的状态。说白了,就是把传感器事件流「接入」到 Compose 的响应式管道里。

核心思想:传感器数据 → State 对象 → Compose 自动重组 UI

实战:在 Compose 中读取加速度传感器

先看一个最简单的例子。我习惯把传感器逻辑封装成一个 Composable 函数,这样调用方只需要关心数据,不用管底层细节。

@Composable
fun rememberAccelerometerData(): State<SensorEvent?> {
    val context = LocalContext.current
    val sensorManager = remember {
        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }
    val accelerometer = remember {
        sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    }
    val sensorData = remember { mutableStateOf<SensorEvent?>(null) }

    DisposableEffect(Unit) {
        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                sensorData.value = event
            }
            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
        }
        sensorManager.registerListener(
            listener, accelerometer, SensorManager.SENSOR_DELAY_NORMAL
        )
        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }
    return sensorData
}

嗯,这里要注意几个点:

  • remember 保证 SensorManager 和 Sensor 对象在重组时不被重复创建
  • DisposableEffectonDispose 会在 Composable 离开组合时自动注销监听器——再也不用担心忘记注销了
  • mutableStateOf 让传感器数据变成可观察状态

我曾经在项目里用这个模式写过计步器,效果很好。唯一踩过的坑是:别忘了检查传感器是否为 null。有些设备可能没有加速度传感器,直接注册会 crash。

避坑指南:我曾经在平板上跑这个代码,结果应用直接闪退。后来发现那台平板没有加速度传感器。所以一定要加判空:accelerometer ?: return remember { mutableStateOf(null) }

在 UI 中消费传感器数据

有了上面的基础函数,在 UI 层使用就非常简单了:

@Composable
fun AccelerometerDisplay() {
    val sensorData by rememberAccelerometerData()
    val x = sensorData?.values?.get(0) ?: 0f
    val y = sensorData?.values?.get(1) ?: 0f
    val z = sensorData?.values?.get(2) ?: 0f

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("加速度传感器数据", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(24.dp))
        Text("X: %.2f m/s²".format(x))
        Text("Y: %.2f m/s²".format(y))
        Text("Z: %.2f m/s²".format(z))
    }
}

你想想看,这比传统 View 系统简洁了多少?不需要 findViewById,不需要手动 setText,数据一变 UI 自动更新。

用动画让数据「活」起来

光显示数字太枯燥了。我个人习惯用 Compose 的动画 API 让传感器数据可视化。比如做一个跟随手机倾斜而移动的小球:

@Composable
fun BallFollowsTilt() {
    val sensorData by rememberAccelerometerData()
    val x = sensorData?.values?.get(0) ?: 0f
    val y = sensorData?.values?.get(1) ?: 0f

    // 将传感器值映射到屏幕偏移量
    val offsetX by animateFloatAsState(
        targetValue = x * 20f,
        animationSpec = tween(durationMillis = 100)
    )
    val offsetY by animateFloatAsState(
        targetValue = y * 20f,
        animationSpec = tween(durationMillis = 100)
    )

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Canvas(modifier = Modifier.size(200.dp)) {
            drawCircle(
                color = Color.Blue,
                radius = 30.dp.toPx(),
                center = Offset(
                    size.width / 2 + offsetX,
                    size.height / 2 + offsetY
                )
            )
        }
    }
}

这里用 animateFloatAsState 做了平滑过渡,不然小球会跳来跳去,看着头晕。为什么用 100 毫秒的动画时长?我试过 50 毫秒太灵敏,300 毫秒又感觉迟钝,100 毫秒刚刚好。

知识体系总览

下面这张图总结了在 Compose 中使用传感器的完整流程:

Compose 传感器数据流 硬件传感器 SensorManager DisposableEffect 注册 回调 mutableStateOf 更新状态 Composable UI 重组 硬件 → 系统服务 → Compose 状态 → UI 自动更新

性能与最佳实践

用 Compose 处理传感器数据时,有几个性能要点需要留意:

要点 说明 我的建议
采样频率 SENSOR_DELAY_NORMAL 约 200ms 一次 非游戏场景用 NORMAL 就够了
重组范围 尽量把传感器数据读取放在子 Composable 中 避免父级频繁重组
状态提升 如果多个 Composable 需要同一份传感器数据 把 State 提升到公共父级
生命周期 DisposableEffect 自动处理注册/注销 比传统方式省心太多

小技巧:如果你只需要在屏幕可见时才监听传感器,可以结合 LifecycleEventEffectLifecycleResumeEffect 来控制。不过大多数场景下 DisposableEffect 已经够用了。

总结一下

Jetpack Compose 让传感器开发变得很自然。你不需要再手动管理监听器的生命周期,也不需要担心 UI 更新不及时。把传感器数据变成 State,剩下的交给 Compose 的响应式机制就好。

我个人觉得,Compose 最大的价值在于「声明式」的思维方式。你告诉 UI:「当传感器数据变化时,你应该长这样」,而不是「当传感器数据变化时,你去执行这段代码更新那个控件」。这两种思维方式的差异,用过之后才能体会。

嗯,如果你之前一直在用传统 View 系统,不妨拿一个小项目试试 Compose 版本的传感器应用。从简单的数据显示开始,慢慢加上动画、图表,你会发现——原来写传感器应用也可以这么愉快。


公众号:蓝海资料掘金营,微信deep3321