上周我在公司摸鱼的时候,发现 Github 上有一个开源库叫 Jindong,没错,听起来就是京东!
我以为我看错了,京东什么时候开始干这玩意儿了?后面才发现误会了。
Jindong 是什么
Jindong 为 Compose 跨平台应用(支持 Android 和 iOS)提供了声明式的触觉反馈 DSL。它基于 Compose 构建,让你能通过简单直观的 API 定义复杂的触觉模式。
该开源库的作者是来自韩国的开发者。“Jindong”[tɕindooŋ](진동)在韩语中意为“振动”。
好朴实无华的库名称,如果是中文名那岂不就是——Zhendong?
我们知道,当前移动 UI 开发已成功转向声明式范式,但与硬件 API 的交互(尤其是触觉反馈)仍然主要依赖命令式编程。
所以这个库的初衷,是为了让触觉反馈的交互也转向声明范式,它解决了在 Compose Multiplatform 应用中实现触觉反馈的复杂性。借助 Compose,开发者现在可以将触觉模式以结构化描述的形式进行定义。
也就是说,它提供了一种声明范式的方式调用硬件 API,同时还支持跨平台。
传统震动
我们先来看看传统的命令式方式——使用标准 Android API 创建波形振动,需要并行的时序和振幅值数组:
Column(
modifier = Modifier.fillMaxSize(),
) {
Button(onClick = {
val timings = longArrayOf(0, 50, 100, 50, 100, 50)
val amplitudes = intArrayOf(0, 255, 0, 128, 0, 255)
val effect = VibrationEffect.createWaveform(timings, amplitudes, -1)
defaultVibrator.vibrate(effect)
}) {
Text("Vibrate")
}
}
调整某个节拍的持续时间需要重新计算数组索引。更不用说,这种逻辑是平台相关的,iOS 的 Core Haptics 需要完全不同的代码实现。
声明式
Jindong 将这些底层细节抽象为一种 DSL。开发者只需定义反馈结构,库会负责执行。
首先,我们添加权限和依赖:
<uses-permission android:name="android.permission.VIBRATE" />
implementation("io.github.compose-jindong:jindong:1.0.0")
然后,开始 DSL 形式的震动:
var count by remember { mutableIntStateOf(0) }
JindongProvider { // 需要提供一个 JindongProvider
if (count > 0) { // 防止第一次进来就震动
Jindong(count) { // 传入一个 key,当 key 发生变化的时候,就会震动
Sequence {
repeat(3) { // 重复三次震动
Haptic(50.ms, HapticIntensity.STRONG)
Delay(100.ms)
}
}
}
}
}
Column(
modifier = Modifier.fillMaxSize(),
) {
Button(onClick = {
count++
}) {
Text("Vibrate")
}
}
在这里,开发者只需定义触觉模式(一个重复三次的序列),而库会负责处理平台相关的执行细节。
这种差异不仅仅是语法上的不同——关键在于谁来控制执行过程。
这也正是旧式的 View 写法和新式的 Compose 写法的核心差别:
| 命令式思维 | 声明式思维 |
|---|---|
| “先振动 50ms,然后等待 100ms,再振动 50ms” | “我想要一个 50ms-100ms-50ms 的震动模式” |
| “循环三次,中间有延迟,启动震动” | “我想要三次重复” |
声明式编程关注于定义目标的逻辑和关系,而不指定如何实现它们。
“描述要做什么,而不是怎么做。”
生命周期管理
在命令式触觉反馈中,生命周期管理是最大的痛点之一。如果用户在长时间振动模式播放过程中导航离开当前页面,会发生什么?
在命令式编程的世界里,你必须手动在 onPause 或 onDispose 中调用 vibrator.cancel()。
而使用 Jindong 时,触觉模式会自动绑定到 Compose 的生命周期:
- 自动取消:当 Composable 离开组合时,触觉引擎会立即停止。
- 响应式更新:就像
LaunchedEffect一样,如果在模式播放过程中keys发生变化,Jindong 会自动取消当前模式,并根据新状态重新开始执行。
/**
* 当 keys 发生变化时,触发触觉模式执行的可组合函数。
*/
@Composable
fun jindong(
vararg keys: Any?,
content: @Composable JindongScope.() -> Unit,
) {
val pattern = rememberHapticPattern(content)
val executor = LocalHapticExecutor.current
LaunchedEffect(*keys) {
executor.execute(pattern)
}
}
这正是 Compose 的一大优势,无需特别关心生命周期。
当你离开组合时,当前的副作用自动结束!
搞点例子
以下是一些 Jindong 的实用场景。
下面的例子因为篇幅我没有显示的指定 JindongProvider,实际上所有的 Jindong 都需要在 JindongProvider 内部。
屏幕进入反馈
使用 Unit 作为触发键,可在进入屏幕时立即触发热反馈:
@Composable
fun WelcomeScreen() {
// 页面出现时执行一次
Jindong(Unit) {
Haptic(100.ms, HapticIntensity.MEDIUM)
}
}
按钮点击反馈
@Composable
fun TapButton() {
var taps by remember { mutableIntStateOf(0) }
if (taps > 0) {
Jindong(taps) {
Haptic(100.ms, HapticIntensity.LIGHT)
}
}
Button(onClick = { taps++ }) {
Text("Tap")
}
}
淡出效果
@Composable
fun FadeOutHaptic(trigger: Int) {
Jindong(trigger) {
RepeatWithIndex(5) { index ->
val intensity = HapticIntensity.Custom(1.0f - (index * 0.2f))
Haptic(50.ms, intensity)
Delay(100.ms)
}
}
}
模拟心跳
@Composable
fun HeartbeatHaptic(trigger: Int) {
Jindong(trigger) {
Repeat(5) {
// Lub(强脉冲)
Haptic(60.ms, HapticIntensity.STRONG)
Delay(80.ms)
// Dub(弱脉冲)
Haptic(40.ms, HapticIntensity.MEDIUM)
Delay(400.ms)
}
}
}
强烈建议各位试一下,这个特别像!
内部原理:Compose Runtime 与 CMP
Compose Runtime 的作用是什么?
Compose Runtime 是一个与 UI 渲染无关的树管理引擎。Jindong 依赖于 Compose 运行时的两个核心组件:
- ComposeNode:这是一个通用函数,用于将节点插入到树中。在 Jindong 中,每个 DSL 函数(如
Haptic、Delay和Sequence)都会在内部调用ComposeNode,以创建特定的HapticNode实现。 - Applier:运行时需要知道如何将这些节点连接起来。
Applier负责处理HapticNode树的结构更新,该树将在播放前被遍历,以收集ScheduledHapticEvent事件。
就像 Compose UI 通过构建 LayoutNode 树来渲染像素一样,Jindong 通过构建 HapticNode 树来合成振动效果。这使我们能够使用标准的 Compose 特性(如 SideEffect、remember),结合 Kotlin 的标准控制流(如 if/when/for),自然地构建动态的触觉体验。
Compose Multiplatform 实现
Jindong 遵循标准的 CMP(Compose Multiplatform)架构:
- 通用层(Common Layer) :包含 Compose DSL 定义、节点(Nodes)以及
HapticPattern数据模型。所有时间计算和模式编译都在此层完成,确保跨平台行为的一致性。 - 平台层(Platform Layer,即执行器 Executor) :
- Android:将
HapticPattern转换为VibrationEffect(使用VibrationEffect.createWaveform)。 - iOS:将
HapticPattern转换为CHHapticPattern,用于 Core Haptics。
- Android:将
Android 和 iOS 对振动强度的处理方式不同。
Android 使用振幅整数(1–255),而 iOS 的 Core Haptics 使用归一化的浮点值(0.0–1.0)。
Jindong 提供了一个统一的抽象层,使开发者可以使用归一化的百分比(例如,intensity = 0.5f)来定义强度,无需关心平台特定的数学转换。
总结
Jindong 旨在解决在 Compose Multiplatform 应用中实现触觉反馈的复杂性。通过利用 Compose 运行时,开发者可以将触觉模式定义为结构化描述,而非一系列电机命令序列。
各位还在等什么,赶紧用起来试试呀!