Skip to content

Android SDK

The Pulsar Android SDK provides haptic feedback through three building blocks: Presets, PatternComposer, and RealtimeComposer. All are accessed through the main Pulsar class.

  • Android API 24+ (Android 7.0)
  • Kotlin 1.9+

Latest available version: 1.3.0

Add Pulsar as a Gradle dependency:

dependencies {
implementation("com.swmansion:pulsar:1.3.0")
}

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 will block vibration playback. Pulsar logs a warning and skips the vibration instead of crashing.

Create a Pulsar instance with an Android Context:

import com.swmansion.pulsar.Pulsar
val pulsar = Pulsar(context)

A collection of ready-to-use haptic patterns accessed through pulsar.getPresets().

val presets = pulsar.getPresets()
MethodDescription
afterglow()A three-beat phrase that dissolves gently, ideal for soft endings or gradually quieting feedback.
aftershock()A firm opening that settles calmly, ideal for transitions needing a strong start and a gentle finish.
alarm()Relentless and urgent, best for critical errors or emergencies that require immediate attention.
anvil()The full weight of a massive collision, conveys sheer physical force and momentum.
applause()A growing wave of appreciation, ideal for celebratory moments or social approval.

Example

val presets = pulsar.getPresets()
presets.applause()

These mirror standard haptic feedback styles shared across platforms.

MethodDescription
systemImpactHeavy()Heavy impact
systemImpactLight()Light impact
systemImpactMedium()Medium impact
systemImpactRigid()Rigid impact
systemImpactSoft()Soft impact

Example

val presets = pulsar.getPresets()
presets.systemImpactMedium()

Android exposes additional system effects beyond the cross-platform set.

MethodDescription
systemCalendarDate()HapticFeedbackConstants.CALENDAR_DATE
systemClockTick()HapticFeedbackConstants.CLOCK_TICK
systemConfirm()HapticFeedbackConstants.CONFIRM
systemContextClick()HapticFeedbackConstants.CONTEXT_CLICK
systemDragCrossing()HapticFeedbackConstants.DRAG_CROSSING

Example

val presets = pulsar.getPresets()
presets.systemPrimitiveClick()

You can play any preset by its string name using getByName(). This returns a Preset? that you call play() on.

Available names:

  • Afterglow
  • Aftershock
  • Alarm
  • Anvil
  • Applause
  • Ascent
  • BalloonPop
  • Barrage

Example

val presets = pulsar.getPresets()
// Direct call
presets.dogBark()
// By name
presets.getByName("SystemImpactMedium")?.play()

Composes and plays custom haptic patterns. Get a new instance from pulsar.getPatternComposer(). Each call returns a fresh composer, so you can manage multiple patterns independently.

val composer = pulsar.getPatternComposer()

Parses a PatternData object and prepares it for playback.

fun parsePattern(hapticsData: PatternData, fromMs: Long = 0)

fromMs starts the pattern that far into its own timeline. The engine can only play a parsed pattern from zero, so Pulsar re-anchors it instead: discrete events before the seek are dropped and the rest rebased, and each continuous envelope is re-anchored on its value at that instant.

Parses a pattern together with a short sound played in sync with the haptics.

fun parsePatternWithSound(hapticsData: PatternData, sound: SoundData, fromMs: Long = 0)

fromMs seeks the whole preset: the haptics are re-anchored and the sound advances into the file by the same amount, so both move together. It composes with the sound’s own startMs/durationMs trim window rather than replacing it.

On devices that support audio-coupled haptics, provide an .ogg whose baked haptic channels drive the vibrator for perfect, single-stream sync. Any other file — .wav/.mp3, or a bare name (which defaults to .wav) — plays the audio while the pattern’s own generated VibrationEffect fires in parallel. See SoundData for uri, volume, offset, hapticChannels, and the startMs/durationMs trim window.

Plays the previously parsed pattern.

fun play()

Stops all playback.

fun stop()
val composer = pulsar.getPatternComposer()
val pattern = PatternData(
continuousPattern = ContinuousPattern(
amplitude = listOf(
ValuePoint(time = 0, value = 0f),
ValuePoint(time = 200, value = 1f),
ValuePoint(time = 400, value = 0f),
),
frequency = listOf(
ValuePoint(time = 0, value = 0.3f),
ValuePoint(time = 400, value = 0.8f),
)
),
discretePattern = listOf(
ConfigPoint(time = 0, amplitude = 1f, frequency = 0.5f),
ConfigPoint(time = 100, amplitude = 0.5f, frequency = 0.5f),
)
)
composer.parsePattern(pattern)
composer.play()

Provides real-time haptic control with live amplitude and frequency modulation. Useful for gesture-driven or continuously evolving haptic experiences. Get the shared instance from pulsar.getRealtimeComposer().

val realtime = pulsar.getRealtimeComposer()

An optional strategy parameter controls how continuous haptics are simulated:

val realtime = pulsar.getRealtimeComposer(strategy = RealtimeComposerStrategy.ENVELOPE)
StrategyDescription
ENVELOPEEnvelope API approximation (API 36+)
PRIMITIVE_TICKComposition API with varying tick intervals
PRIMITIVE_COMPLEXMultiple primitives selected based on frequency
ENVELOPE_WITH_DISCRETE_PRIMITIVESDefault. Envelope API for continuous events (API 36+); composition primitives for discrete events (API 33+).

Updates the ongoing haptic with new amplitude and frequency values. Automatically starts playback if it is not already active. Values should be in the 0-1 range.

fun set(amplitude: Float, frequency: Float)

Plays a single discrete haptic event.

fun playDiscrete(amplitude: Float, frequency: Float)

Stops the active continuous haptic.

fun stop()

Returns true if a continuous haptic is currently playing.

fun isActive(): Boolean
val realtime = pulsar.getRealtimeComposer()
// Start a continuous haptic
realtime.set(amplitude = 0.5f, frequency = 0.8f)
// Update parameters over time
realtime.set(amplitude = 1.0f, frequency = 0.3f)
// Play a one-off discrete event
realtime.playDiscrete(amplitude = 0.7f, frequency = 0.5f)
// Stop
realtime.stop()

A .pulsar bundle exported from Pulsar Studio holds your own presets — each a haptic pattern with optional synced audio and animation. pulsar-gen turns a bundle into a typed accessor, so every preset is a real member: a renamed preset becomes a compile error rather than a silent runtime miss.

Apply the Gradle plugin and drop bundles into src/pulsarBundles/. On every build it generates the typed accessor and packages the bundle into the APK assets — no manual codegen step:

// settings.gradle.kts — the plugin resolves from Maven Central
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
app/build.gradle.kts
plugins {
id("com.swmansion.pulsar.gen") version "0.1.0" // pulsar-sync:pulsar-gen-gradle-version
}
pulsarBundles {
packageName.set("com.example.app.bundles")
}
val pulsar = Pulsar(context)
val bundle = pulsar.loadBundleSync(AcmePack.descriptor)
bundle.heartbeatV2.play()
bundle.explosion.stop()

loadBundleAsync is the same load as a suspend function, reading and decoding on Dispatchers.IO:

val bundle = pulsar.loadBundleAsync(AcmePack.descriptor)

Both check the packaged bundle’s content hash against the generated types, so a stale APK asset fails loudly instead of quietly playing the wrong pattern. Pass strict = false to skip it.

Each PresetHandle exposes id, name, duration, pattern, hasAudio, hasAnimation, play(fromMs = 0), stop(), and animation — the Lottie bytes and timing for your own animation view. Pulsar carries and time-aligns the animation; the app renders it:

Pass fromMs to start a preset that far into its own timeline — audio and haptics seek together, so a progress bar can scrub it:

bundle.heartbeatV2.play(fromMs = 2500)

Every non-zero seek re-parses the preset; playing from the start reuses the cached parse.

The Lottie SDK takes a PresetHandle directly and reads all of that for you — pattern, animation and duration — so you rarely have to unpack it by hand.

bundle.heartbeatV2.animation?.let {
myLottieView.setAnimation(it.data.inputStream(), null)
}

Audio authored into a preset plays natively alongside the haptics — nothing extra to wire.

For an id only known at runtime, bundle.get(id) returns a handle or null. Call bundle.dispose() to release the native patterns when you are done with a bundle.

See Android/PulsarApp for a working screen.


Configuration methods available directly on the Pulsar instance.

MethodDescription
enableHaptics(state: Boolean)Enable or disable all haptic feedback
enableSound(state: Boolean)Enable or disable audio simulation
enableCache(state: Boolean)Enable or disable preset caching
clearCache()Clear the preset cache
preloadPresets(presetNames: List<String>)Preload presets by name for faster playback
stopHaptics()Stop all currently playing haptics
hapticSupport()Returns the device’s CompatibilityMode
hapticCapabilities()Returns the HapticCapabilities behind that level
forceHapticsSupportLevel(mode: CompatibilityMode)Override the detected support level
enableImpulseCompositionMode(state: Boolean)Enable or disable VibrationEffect.Composition for impulse-only presets (enabled by default)
val pulsar = Pulsar(context)
// Preload frequently used presets
pulsar.preloadPresets(listOf("Earthquake", "Success"))
// Disable haptics temporarily
pulsar.enableHaptics(false)
// Check device support
val support = pulsar.hapticSupport()
if (support >= CompatibilityMode.STANDARD_SUPPORT) {
pulsar.getPresets().success()
}

Describes a complete haptic pattern with discrete pulses and continuous envelope curves.

data class PatternData(
val continuousPattern: ContinuousPattern,
val discretePattern: List<ConfigPoint>
)

A short sound to play in sync with a haptic pattern via parsePatternWithSound.

data class SoundData(
val uri: String, // File path, file:// uri, or res/raw resource name
val volume: Float = 1f, // Playback volume (0-1)
val offset: Long = 0L, // Audio delay relative to haptics, in ms (fallback path)
val hapticChannels: Boolean = true,
val startMs: Long = 0L, // Where playback begins in the source file, in ms
val durationMs: Long = 0L // How much to play from startMs, in ms (0 = to the end)
)

The format comes from uri’s extension; when none is given the default is .wav, so a bare name like "beep" loads res/raw/beep.wav. Only an explicit .ogg with baked haptic channels uses the audio-coupled path (on supported devices); everything else plays the audio while the pattern’s own haptics fire in parallel. Set hapticChannels = false to force that fallback for an .ogg that does not carry haptic channels.

startMs and durationMs trim the clip: startMs is where playback begins in the source file and durationMs is how much of it to play (0 plays to the end). The audio is sliced to that window before playing rather than seeked at runtime, so one bundled file can back several trimmed sounds without losing sync.

Represents continuous haptic curves for amplitude and frequency.

data class ContinuousPattern(
val amplitude: List<ValuePoint>,
val frequency: List<ValuePoint>
)

A single point in a continuous curve.

data class ValuePoint(
val time: Long, // Milliseconds from pattern start
val value: Float // Normalized value (0-1)
)

A single discrete haptic event.

data class ConfigPoint(
val time: Long, // Milliseconds from pattern start
val amplitude: Float, // Intensity (0-1)
val frequency: Float // Sharpness (0-1)
)

Use discretePattern for distinct taps and impacts. Use continuousPattern envelopes to shape a sustained haptic over time.

The haptic capability level of the current device, returned by pulsar.hapticSupport(). The SDK gracefully degrades based on what the device supports.

enum class CompatibilityMode {
NO_SUPPORT, // No haptic support
LIMITED_SUPPORT, // Timing-based waveform vibration (API 26+)
STANDARD_SUPPORT, // Amplitude + timing control
ADVANCED_SUPPORT, // Envelope + frequency profile (API 36+)
}

The individual vibrator capabilities behind CompatibilityMode, returned by pulsar.hapticCapabilities().

data class HapticCapabilities(
val hasAmplitudeControl: Boolean, // Intensity control, not on/off only
val hasPrimitiveSupport: Boolean, // Composition primitives (API 30+)
val isEnvelopeSupported: Boolean, // Envelope effects (API 36+)
val isFrequencyProfileSupported: Boolean, // Vendor frequency profile (API 36+)
val minControlPointDurationMillis: Long, // Shortest envelope control point honored
)

The support level is decided by amplitude control (and, on Android 16+, the frequency profile). Primitive support is not part of it, so a STANDARD_SUPPORT device can still lack composition primitives. If you render your own single-hit patterns, check hasPrimitiveSupport and fall back to a waveform rather than composing primitives the vibrator will silently drop.

if (!pulsar.hapticCapabilities().hasPrimitiveSupport) {
pulsar.enableImpulseCompositionMode(false)
}

Controls how continuous haptics are rendered on the device.

enum class RealtimeComposerStrategy {
ENVELOPE, // Envelope API approximation (API 36+)
PRIMITIVE_TICK, // Composition API with varying intervals
PRIMITIVE_COMPLEX, // Multiple primitives based on frequency
ENVELOPE_WITH_DISCRETE_PRIMITIVES, // Envelope for continuous; primitives for discrete
}

The interface that all preset implementations conform to.

interface Preset {
fun play()
}