Skip to content

Flutter

pulsar_haptics_lottie plays a Pulsar haptic pattern locked to a Lottie animation in Flutter. It exposes two entry points: the HapticLottie widget, and HapticLottieController for an AnimationController you already drive.

It is a pure-Dart package — no native halves of its own. Both the haptics and the rendering come from packages you already have: pulsar_haptics and lottie.

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

  • Flutter 3.29+, Dart 3.7+
  • Android API 24+ (Android 7.0), iOS 13+
  • The Pulsar Flutter SDK (pulsar_haptics) and lottie 3.1+, both pulled in automatically

Latest available version: 0.1.0

Add the package to your pubspec.yaml:

dependencies:
pulsar_haptics_lottie: ^0.1.0

Or via the Flutter CLI:

Terminal window
flutter pub add pulsar_haptics_lottie

Declare the vibration permission in your Android 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. iOS needs nothing beyond pod install.


A drop-in widget that owns its own AnimationController and plays a pattern locked to the animation:

import 'package:pulsar_haptics/pulsar.dart';
import 'package:pulsar_haptics_lottie/pulsar_haptics_lottie.dart';
final pattern = PatternData.fromArrays(
amplitude: [[0, 0], [400, 1], [800, 0]],
frequency: [[0, 0.3], [800, 0.8]],
discrete: [[0, 1, 0.5]],
);
HapticLottie.asset(
'assets/success.json',
haptics: pattern,
autoPlay: true,
width: 200,
height: 200,
);

HapticLottie.network(url, ...) takes the same arguments and loads from a URL instead.

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

final pack = await pulsar.loadBundleAsync(acmePack); // acmePack is generated
HapticLottie.preset(pack.celebration, autoPlay: true, width: 200, height: 200);

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.

ParameterTypeDefaultDescription
srcStringAsset name or URL, passed positionally. Replaced by the preset in HapticLottie.preset.
presetPresetHandle?nullBundle preset supplying the animation, pattern and duration.
hapticsPatternData?nullPattern to sync. Overrides the preset’s pattern; omit both for a plain animation.
hapticModeHapticMode?derivedrealtime, or pattern for a preset carrying audio.
hapticOffsetdouble (ms)0Shift the haptics by ±ms relative to the animation.
hapticsEnabledbooltrueTurn haptics off without touching the animation.
durationMsdouble?derivedClock length in ms. Overrides every derived duration.
autoPlayboolfalseStart as soon as the animation loads.
repeat / repeatCount / repeatReverseLooping, with autoPlay.
onControllerCreatedvoid Function(HapticLottieController)?nullCalled once with the controller, so you can drive transport.
width / height / fit / alignmentForwarded to the underlying Lottie widget.

The widget sets the controller’s duration from the loaded composition and disposes both it and the haptics on unmount.


Attaches haptics to an AnimationController you already pass to Lottie, and becomes the transport for both:

final anim = AnimationController(vsync: this);
final haptic = HapticLottieController(
animationController: anim,
preset: pack.celebration,
);
Lottie.asset('assets/success.json', controller: anim,
onLoaded: (composition) => anim.duration = composition.duration);
haptic.play();
haptic.setTimestamp(1200);
haptic.pause();

It takes the same arguments as the widget — preset, haptics, hapticMode, hapticOffset, hapticsEnabled, durationMs — plus animationController and an optional 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()Reset the animation to the start and stop the haptics.
setTimestamp(double ms)Seek both to ms from the start.
setLoop(bool loop, {int? count, bool reverse})Loop the animation. In pattern mode the pattern fires once at the start, not per iteration.
durationMsResolvedThe clock length actually in use.
dispose()Detach and release the haptic resources.

The transport methods return Future<void>await them when ordering matters.


PresetHandle carries everything the widget reads:

MemberTypeDescription
id / nameStringCode-safe id, and the human label it was authored under.
durationdoubleAuthored length in ms, or 0 when the bundle carries no hint.
patternPatternDataThe authored pattern — what realtime samples.
animationBundleAnimation?Lottie data (ready for Lottie.memory), frameRate and totalFrames.
hasAudio / hasAnimationboolWhat the preset was authored with.
play() / stop()Play the preset natively: haptics, plus its synced audio.

The animation bytes cross the platform channel once, when the bundle loads; pass includeAnimations: false to loadBundleAsync to skip that when nothing will render them. A preset with no animation renders an empty box.

See the Flutter SDK page for how bundles are loaded.