jetpack compose 无内置震动api,需手动调用vibrator并处理权限、api级别及硬件限制;视觉抖动用animatable+graphicslayer实现,频率需分ui、系统、感知三层独立配置并降级适配。

Jetpack Compose 里没有现成的“设备震动” API 封装,androidx.core:haptic-feedback 提供的 HapticFeedbackType 仅支持系统级触感反馈(如点击、长按),无法直接控制震动时长、波形或频率。所谓“模拟机器运行状态的震动动画”,本质是视觉 + 触觉协同的拟物表达,必须拆解为 UI 动画 + 主动调用系统震动两部分,且后者受 Android 权限、API 级别和硬件限制极严。
Compose 中触发真实震动必须手动调用 Vibrator
Compose 是声明式 UI 框架,不负责硬件交互。要让手机真正震动,你得绕过 Compose 生命周期,从 Context 获取 Vibrator 实例并调用其方法:
-
Vibrator需要android.permission.VIBRATE权限(Android 12+ 还需在AndroidManifest.xml中声明android:required="false",否则可能被 Google Play 拒绝) - Android 8.0(API 26)后,
vibrate(long[] pattern, int repeat)被标记为 deprecated,推荐用vibrate(VibrationEffect);但VibrationEffect.createWaveform()才支持自定义频率序列,而它最低只支持 API 29(Android 10) - 低于 API 29 的设备只能用
VibrationEffect.createOneShot()或静音 fallback —— 没法真正“模拟频率变化”
示例片段(需在 LaunchedEffect 或 DisposableEffect 中安全调用):
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (vibrator.hasVibrator()) {
val pattern = longArrayOf(0, 50, 100, 50, 200) // 延迟 + 震动/停顿交替
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
vibrator.vibrate(
VibrationEffect.createWaveform(pattern, -1)
)
} else {
vibrator.vibrate(pattern, -1)
}
}
用 Animatable + graphicsLayer 做视觉震动动画
视觉上的“机器运行抖动”不能依赖真实震动,得靠 Canvas 或修饰符动画实现。核心是让 UI 元素在小范围内高频位移,配合缩放/旋转增强机械感:
围绕关键发现、作用机制、临床相关性及研究局限性展开讨论。适用于撰写或优化任何生物医学论文的“讨论(Discussion)”部分——包括结果解读、与既往文献关联、阐释意外发现、界定研究局限性,以及撰写结论。当用户输入以下任一指令时也会自动触发该功能: - “write my discussion” - “help me discuss my findings” - “how do I compare to prior studies” - “write the limitations par
- 用
Animatable驱动offset或rotation,比animate*AsState更适合循环高频动画(避免 recomposition 开销) - 频率控制靠
infiniteRepeatable+tween的durationMillis:比如 100ms 一圈 ≈ 10Hz,200ms ≈ 5Hz;但注意人眼可分辨的抖动上限约 15–20Hz,再高就变模糊了 - 避免用
rememberInfiniteTransition()做多属性联动(如同时 offset + scale),容易因不同动画周期错相导致不自然
简例(水平抖动 + 微缩放):
val offsetX = remember { Animatable(0f) }
val scale = remember { Animatable(1f) }
LaunchedEffect(Unit) {
offsetX.animateTo(
1f,
animationSpec = infiniteRepeatable(
animation = tween(100, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
scale.animateTo(
0.98f,
animationSpec = infiniteRepeatable(
animation = tween(200, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
}
Box(
modifier = Modifier
.graphicsLayer(
translationX = offsetX.value,
scaleX = scale.value,
scaleY = scale.value
)
)
“模拟运行频率”必须区分 UI 刷新率与物理震动能力
用户说的“频率”,在代码里常被误当成单一数值去设。实际上它横跨三层:
-
UI 层:Compose 动画帧率由
MonotonicFrameClock控制,默认尽量跟屏幕刷新率(60/90/120Hz)同步;你设的tween(durationMillis = 50)只是目标周期,实际帧间隔受系统调度影响 - 系统震动层:Android 对连续波形有最小间隔限制(通常 ≥20ms),且低端设备可能把高频 pattern 合并执行,导致“标称 10Hz”实际只有 3–4 次有效震动
- 感知层:人对震动频率的敏感区间是 30–300Hz,但手机马达物理响应带宽往往只有 50–150Hz;低于 20Hz 感觉是“咚咚”脉冲,高于 200Hz 就变成“麻刺感”——这和你 UI 抖动的 5–20Hz 完全不是一回事
所以别试图用一个 frequencyHz: Int 参数统管所有行为。该分开配置:UI 抖动周期用 visualPulseIntervalMs,震动波形用 vibrationPattern 数组,运行状态切换用独立的 isRunning boolean。
真正难的不是写几行动画代码,而是判断当前设备能否支撑你想要的“频率感”:API 级别够不够、马达类型是 ERM 还是 LRA、用户是否关闭了触感反馈。这些没法在 Compose 里声明式处理,得在调用前做运行时探测,并准备好降级路径 —— 比如震动失效时,只保留视觉抖动 + 声音提示。










