27、Jetpack Compose:在新UI框架下使用传感器
说实话,刚接触 Jetpack Compose 那会儿,我还有点不习惯。
毕竟做了好几年 XML 布局,突然换成纯 Kotlin 写 UI,总觉得少了点什么。但用了一段时间后,我发现 Compose 在处理传感器这类实时数据流时,反而比传统 View 系统更顺手。为什么呢?因为 Compose 天生就是「响应式」的——数据变了,UI 自动跟着变。这不正是传感器应用最需要的特性吗?
Compose 里的传感器思路
在传统 View 系统中,我们通常这样用传感器:
- 在 Activity 或 Fragment 里注册 SensorEventListener
- 在 onSensorChanged 回调里拿到数据
- 手动更新 TextView 或自定义 View
这套流程没什么大问题,但有个痛点:生命周期管理。你得小心翼翼地处理 onResume 注册、onPause 注销,稍不留神就会内存泄漏。
在 Compose 里,我们换了个思路。利用 LaunchedEffect 和 remember,可以把传感器数据变成 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 对象在重组时不被重复创建DisposableEffect的onDispose会在 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 处理传感器数据时,有几个性能要点需要留意:
| 要点 | 说明 | 我的建议 |
|---|---|---|
| 采样频率 | SENSOR_DELAY_NORMAL 约 200ms 一次 | 非游戏场景用 NORMAL 就够了 |
| 重组范围 | 尽量把传感器数据读取放在子 Composable 中 | 避免父级频繁重组 |
| 状态提升 | 如果多个 Composable 需要同一份传感器数据 | 把 State 提升到公共父级 |
| 生命周期 | DisposableEffect 自动处理注册/注销 | 比传统方式省心太多 |
小技巧:如果你只需要在屏幕可见时才监听传感器,可以结合 LifecycleEventEffect 或 LifecycleResumeEffect 来控制。不过大多数场景下 DisposableEffect 已经够用了。
总结一下
Jetpack Compose 让传感器开发变得很自然。你不需要再手动管理监听器的生命周期,也不需要担心 UI 更新不及时。把传感器数据变成 State,剩下的交给 Compose 的响应式机制就好。
我个人觉得,Compose 最大的价值在于「声明式」的思维方式。你告诉 UI:「当传感器数据变化时,你应该长这样」,而不是「当传感器数据变化时,你去执行这段代码更新那个控件」。这两种思维方式的差异,用过之后才能体会。
嗯,如果你之前一直在用传统 View 系统,不妨拿一个小项目试试 Compose 版本的传感器应用。从简单的数据显示开始,慢慢加上动画、图表,你会发现——原来写传感器应用也可以这么愉快。
公众号:蓝海资料掘金营,微信deep3321