马上就到春节了,最近也在学习Compose的动画,结合触觉反馈写了一个"赛博鞭炮"的动画效果,页面很简单,就一个鞭炮图片,点击后,回添加一个缩放动画,同时添加震动反馈,连续点击 20 次后会出现一个粒子爆炸的效果。

第一部分:技术拆解
在开始写代码之前,我们先准备好三样"火药":
1. 声明式动画 (Declarative Animation)
- 技术点 :
animateFloatAsState - 原理 :在 Compose 中,我们不再命令 View "开始播放动画"。相反,我们声明一个目标状态 (Target State)。比如,"现在的缩放比例是 1.5"。Compose 的动画系统会自动计算从"当前值"到"目标值"的补间数据,并逐帧重绘。
- 适用场景:按钮点击缩放、颜色渐变、位置移动等简单的过渡效果。
2. 帧级物理渲染 (Frame-Loop Rendering)
- 技术点 :
Canvas+withFrameNanos - 原理:对于包含成百上千个粒子的爆炸效果,使用普通的 Composable 组件(如 Box)会带来巨大的性能开销(因为每个粒子都是一个 Layout Node)。
- 解决方案 :我们使用
Canvas直接在 GPU 上绘制,并配合withFrameNanos挂钩屏幕刷新率(通常是 60Hz 或 120Hz)。每一帧我们都手动计算粒子的物理位置(重力、速度),实现极致流畅的粒子系统。
3. 沉浸式触觉反馈 (Haptics)
- 技术点 :
VibratorManager&VibrationEffect - 原理:震动不再是单一的"嗡嗡"声。Android 提供了丰富的震动原语(Primitives)。
-
- OneShot:单次短震,模拟点击。
- Waveform:波形震动,可以定义一连串强弱变化的震动序列,模拟爆炸的冲击波。
第二部分:技术解析
1. 物理粒子系统模型
要实现真实的爆炸,不能只靠随机乱飞。我们需要简单的物理公式:
- 水平运动 (匀速):
- 垂直运动 (加速下落):
- 衰减 (Fade Out): 透明度随时间线性降低。
在 Compose 中,我们在 Canvas 的 onDraw 阶段应用这些公式:
Kotlin
// 伪代码示例
particles.forEach { p ->
// 计算当前位置
val x = originX + p.vx * time
val y = originY + p.vy * time + 0.5 * gravity * time * time
// 绘制
drawCircle(center = Offset(x, y), alpha = calculateAlpha(time))
}
2. 震动兼容性处理
Android 的震动 API 经历了多次迭代。为了兼容性,我们需要根据 SDK 版本分流:
- Android 12 (S) 及以上 :使用
VibratorManager获取默认震动器。 - Android 12 以下 :直接使用
Vibrator服务。
第三部分:综合实战
接下来,我们将上述技术熔炼在一起,实现完整的赛博鞭炮功能。
1. 定义数据结构
首先,定义粒子模型。
Kotlin
data class ExplosionParticle(
val vx: Float, // 水平速度向量
val vy: Float, // 垂直速度向量
val color: Color // 粒子颜色
)
2. 实现粒子爆炸层 (ExplosionOverlay)
这是技术含量最高的部分,使用了 Canvas 和 FrameLoop。
Kotlin
@Composable
fun ExplosionOverlay(triggerId: Long) {
// 1. 初始化粒子池:生成 100 个具有随机速度和颜色的粒子
val particles = remember(triggerId) {
List(100) {
ExplosionParticle(
vx = (Math.random() - 0.5).toFloat() * 0.05f,
vy = (Math.random() - 0.5).toFloat() * 0.05f,
color = listOf(Color.Yellow, Color.Red, Color.White).random()
)
}
}
var timeNanos by remember(triggerId) { mutableStateOf(0L) }
// 2. 启动动画循环:每一帧更新时间
LaunchedEffect(triggerId) {
val startTime = withFrameNanos { it }
while (true) {
val playTime = withFrameNanos { it } - startTime
timeNanos = playTime
// 2秒后自动停止,释放资源
if (playTime > 2_000_000_000L) break
}
}
// 3. 绘制层:应用物理公式
Canvas(modifier = Modifier.fillMaxSize()) {
val timeFactor = timeNanos / 1_000_000_000f * 60f // 时间标准化
val gravity = 0.0005f // 重力系数
particles.forEach { p ->
// x = x0 + vt
val x = size.width / 2 + p.vx * timeFactor * size.width
// y = y0 + vt + 0.5gt^2
val y = size.height / 2 + (p.vy * timeFactor + 0.5f * gravity * timeFactor * timeFactor) * size.height
// 计算透明度
val alpha = (1f - timeFactor / 100f).coerceIn(0f, 1f)
if (alpha > 0) {
drawCircle(color = p.color.copy(alpha = alpha), radius = 10f * alpha, center = Offset(x, y))
}
}
}
}
3. 主界面交互逻辑 (CyberFirecrackerScreen)
整合 动画状态 、震动 和 业务逻辑。
Kotlin
@Composable
fun CyberFirecrackerScreen() {
val context = LocalContext.current
// 1. 准备震动器 (兼容性处理)
val vibrator = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
(context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager).defaultVibrator
} else {
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
}
// 2. 状态管理
var scale by remember { mutableStateOf(1f) }
val animatedScale by animateFloatAsState(targetValue = scale, label = "scale")
var clickCount by remember { mutableStateOf(0) }
var explosionTrigger by remember { mutableStateOf(0L) }
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null // 去除默认波纹
) {
// --- 交互逻辑开始 ---
clickCount++
// 3. 触发逻辑:满 20 次爆炸
if (clickCount >= 20) {
clickCount = 0
explosionTrigger = System.currentTimeMillis() // 触发粒子动画
// 播放爆炸震动 (Waveform)
val explosionWave = longArrayOf(0, 50, 50, 100, 50, 200)
vibrator.vibrate(VibrationEffect.createWaveform(explosionWave, -1))
} else {
// 播放普通震动 (OneShot)
vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
}
// 4. 触发缩放动画
scale = 1.2f // 变大
// 使用协程延时恢复
// 注意:在真实代码中需配合 LaunchedEffect 或 suspend function
}
) {
// 鞭炮主体
Text("🧨", modifier = Modifier.scale(animatedScale).align(Alignment.Center))
// 粒子层 (覆盖在最上层)
if (explosionTrigger > 0L) {
ExplosionOverlay(triggerId = explosionTrigger)
}
}
}
结语
Jetpack Compose 不仅仅是一个 UI 框架,它对 Canvas 绘图 和 动画流 的支持让开发者能够轻松实现高性能的游戏化交互。
- 声明式 UI 让状态管理变得清晰。
- Canvas API 让物理模拟成为可能。
- Vibrator API 补全了触觉体验的最后一块拼图。
现在,你已经拥有了一个赛博味十足的电子鞭炮,既环保又解压!