Skip to content

Android

pulsar-lottie plays a Pulsar haptic pattern locked to a Lottie animation on Android. It exposes two entry points: HapticLottieView, a drop-in LottieAnimationView subclass, and HapticLottieController, which attaches to a LottieAnimationView you already have.

Read the Lottie SDK overview for the engine modes, timing rules and emulator notes this page assumes.

Latest available version: 0.1.0

Add the dependency alongside Lottie itself:

dependencies {
implementation("com.swmansion:pulsar-lottie:0.1.0")
}
dependencies {
implementation("com.airbnb.android:lottie:6.6.0")
}

The Pulsar core is exposed transitively, so com.swmansion:pulsar needs no separate entry — its PatternData types are part of this library’s public API. Lottie is not exposed transitively: declare it yourself, since your code touches LottieAnimationView directly.

Declare the vibration permission in your app’s AndroidManifest.xml:

<manifest ...>
<uses-permission android:name="android.permission.VIBRATE" />
<application ...>
...
</application>
</manifest>

Without android.permission.VIBRATE, Android blocks vibration playback. Pulsar logs a warning and skips the vibration instead of crashing.


A LottieAnimationView subclass that plays Pulsar haptics in sync. With no haptics bound it behaves identically, so it is a drop-in replacement.

val pattern = PatternData(
continuousPattern = ContinuousPattern(
amplitude = listOf(ValuePoint(time = 0, value = 0f), ValuePoint(time = 800, value = 1f)),
frequency = listOf(ValuePoint(time = 0, value = 0.3f)),
),
discretePattern = listOf(ConfigPoint(time = 0, amplitude = 1f, frequency = 0.5f)),
)
val view = HapticLottieView(context).apply { setAnimation(R.raw.success) }
view.bindHaptics(pulsar, haptics = pattern).play()

In XML it is used exactly like LottieAnimationView:

<com.swmansion.pulsar.lottie.HapticLottieView
android:id="@+id/success"
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_rawRes="@raw/success" />

A preset in a .pulsar bundle already pairs an animation with the pattern its author aligned to it. Pass the preset and the view takes both — no setAnimation, no haptics:

val bundle = pulsar.loadBundleSync(AcmePack.descriptor)
val view = HapticLottieView(context)
view.bindHaptics(pulsar, preset = bundle.celebration).play()

The preset supplies the animation, haptics from its pattern, and durationMs from its authored length — each still overridable on its own. One that carries audio plays that audio too, which is why it starts in PATTERN mode. See bundle presets.

bindHaptics only sets the preset’s animation when the view has none, so an animation you set yourself is never replaced.

MethodDescription
bindHaptics(pulsar, preset, haptics, hapticMode, hapticOffset, hapticsEnabled, durationMs)Bind haptics and return the HapticLottieController. Releases any previously bound controller.
hapticController()The current controller, or null before bindHaptics is called.

Every LottieAnimationView member — setAnimation, progress, repeatCount, playAnimation — is still available. Drive playback through the returned controller so the haptics stay in step.

Wrap the view with AndroidView and keep the controller across recompositions:

@Composable
fun Celebration(pulsar: Pulsar, preset: PresetHandle) {
val controller = remember { arrayOfNulls<HapticLottieController>(1) }
AndroidView(
modifier = Modifier.size(220.dp),
factory = { ctx ->
HapticLottieView(ctx).apply {
controller[0] = bindHaptics(pulsar, preset = preset).also { it.play() }
}
},
)
DisposableEffect(Unit) { onDispose { controller[0]?.release() } }
}

Attaches haptics to a LottieAnimationView without swapping the view, and becomes the transport for both. The extension binds haptics only — the view keeps whatever animation you gave it.

import com.swmansion.pulsar.lottie.bindHaptics
val controller = animationView.bindHaptics(pulsar, preset = bundle.celebration)
controller.play()
controller.setTimestamp(1200) // seek both animation and haptics to 1.2s
controller.pause()
controller.release() // when the view goes away

bindHaptics is a convenience over the constructor, which takes the same arguments — preset, haptics, hapticMode, hapticOffset, hapticsEnabled, durationMs — plus lottieView and pulsar.

MethodDescription
play()Rewind to the start and play animation and haptics together.
pause()Pause both. In PATTERN mode the haptic stops rather than suspending.
resume()Resume from the current position.
stop() / reset()Stop and rewind to the start.
setTimestamp(ms: Long)Seek both to ms from the start.
setLoop(loop: Boolean, count: Int, reverse: Boolean)Loop the animation. count defaults to LottieDrawable.INFINITE; reverse plays a boomerang.
release()Detach the animator listener and stop the haptics.

Unlike iOS, the animation always plays through the view’s own animator in both modes — realtime only observes it, so anything that moves the view’s progress, including your own playAnimation() calls, keeps the haptics aligned.


PresetHandle carries everything the view reads, so you can render into a view of your own:

preset.animation?.let { animationView.setAnimation(it.data.inputStream(), preset.id) }
val controller = animationView.bindHaptics(pulsar, preset = preset)
MemberTypeDescription
id / nameStringCode-safe id, and the human label it was authored under.
durationLongAuthored length in ms, or 0 when the bundle carries no hint.
patternPatternDataThe authored pattern — what REALTIME samples.
animationBundleAnimation?Lottie data, frameRate and totalFrames.
hasAudio / hasAnimationBooleanWhat the preset was authored with.
play() / stop()Play the preset natively: haptics, plus its synced audio.

See the Android SDK page for how bundles are loaded.