https://github.com/erykkruk/flutter_vibration_animation
Comprehensive vibration and haptic feedback for Flutter — Android & iOS. Impact, notification, selection, predefined effects, custom waveforms and Core Haptics intensity+sharpness patterns.
https://github.com/erykkruk/flutter_vibration_animation
dart flutter haptic-feedback haptics vibration
Last synced: 11 days ago
JSON representation
Comprehensive vibration and haptic feedback for Flutter — Android & iOS. Impact, notification, selection, predefined effects, custom waveforms and Core Haptics intensity+sharpness patterns.
- Host: GitHub
- URL: https://github.com/erykkruk/flutter_vibration_animation
- Owner: erykkruk
- License: mit
- Created: 2026-04-30T09:01:34.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-13T07:13:15.000Z (27 days ago)
- Last Synced: 2026-05-13T08:29:30.660Z (27 days ago)
- Topics: dart, flutter, haptic-feedback, haptics, vibration
- Language: Dart
- Size: 88.9 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# haptic_kit
[](https://pub.dev/packages/haptic_kit)
[](LICENSE)
**Haptic feedback, vibration and animated UI widgets** for Flutter — full
Android & iOS implementations covering everything from quick UI taps to
custom Core Haptics patterns with intensity and sharpness curves, plus a
set of production-ready widgets wired to the right haptic at the right
moment.
> Previously developed under the names `flutter_vibration_animation` and
> `flutter_haptics`. The repository URL is unchanged — only the package
> name and class identifiers were updated for consistency with the actual
> surface and pub.dev naming rules.
---
## Features
- **`Haptics`** — short, semantic taps (named `Haptics` to avoid clashing with Flutter's own `HapticFeedback`)
- Impact: `light`, `medium`, `heavy`, `soft`, `rigid`
- Notification: `success`, `warning`, `error`
- Selection (for pickers, sliders, segmented controls)
- `prepare()` to pre-warm generators on iOS for lowest latency
(returns `true` on iOS, `false` no-op on Android)
- **`Vibration`** — longer-form vibrations
- One-shot vibration with optional amplitude
- Custom waveforms with per-segment amplitudes
- Predefined OS effects (`tick`, `click`, `doubleClick`, `heavyClick`)
- Cancel any running vibration
- **`HapticPattern`** — fluent builder for **Core Haptics** patterns
- Transient taps + continuous events
- Per-event `intensity` and `sharpness` (0.0–1.0)
- Automatic translation to Android amplitude waveforms
- **`VibrationPatterns`** — ready-made: heartbeat, notification,
alarm, tick, success, failure, charge-up
- **`HapticCapabilities`** — runtime detection of vibrator hardware,
amplitude control, Core Haptics, predefined effects
- **`HapticBounce`** — drop-in tap wrapper with squash + recoil + elastic
settle bounce (3-segment `TweenSequence`), wired to light/medium impact
- **`PressAndHoldToConfirm`** — long-press confirmation with a finger-tracking
progress ring and a 12-tick densifying haptic schedule that escalates
from `selection` → `light` → `medium` → `heavy`
## Platform support
| Feature | Android | iOS |
|---------|---------|-----|
| Impact / notification / selection | ✅ API 21+ (best on 26+) | ✅ iOS 10+ |
| One-shot + amplitude | ✅ API 26+ | ✅ iPhone 8+ (Core Haptics) |
| Custom waveforms | ✅ API 26+ | ✅ iPhone 8+ |
| Predefined effects | ✅ API 29+ | ↩︎ mapped to closest impact |
| Custom patterns (intensity + sharpness) | ✅ API 26+ | ✅ iPhone 8+ |
| Capability detection | ✅ | ✅ |
## Installation
```yaml
dependencies:
haptic_kit: ^1.0.0
```
### Android
The plugin's `AndroidManifest.xml` already declares `VIBRATE` — nothing else to do.
### iOS
`CoreHaptics`, `UIKit` and `AudioToolbox` are linked automatically through
the podspec. Minimum deployment target: iOS 12.0.
## Quick start
```dart
import 'package:haptic_kit/haptic_kit.dart';
// Short UI taps
await Haptics.impact(HapticImpactStyle.medium);
await Haptics.notification(HapticNotificationStyle.success);
await Haptics.selection();
// Longer vibrations
await Vibration.vibrate(duration: const Duration(milliseconds: 300));
// Custom waveform — three pulses with growing amplitude
await Vibration.vibrateWaveform(
timings: const [
Duration.zero,
Duration(milliseconds: 100),
Duration(milliseconds: 100),
Duration(milliseconds: 100),
Duration(milliseconds: 100),
Duration(milliseconds: 100),
],
amplitudes: const [0, 80, 0, 160, 0, 255],
);
// Predefined OS effect
await Vibration.playPredefined(PredefinedEffect.doubleClick);
// Ready-made pattern
await VibrationPatterns.heartbeat();
```
## Custom haptic patterns (Core Haptics)
```dart
await HapticPattern.builder()
.tap(intensity: 0.4, sharpness: 0.6)
.pause(const Duration(milliseconds: 80))
.tap(intensity: 1.0, sharpness: 0.9)
.continuous(
duration: const Duration(milliseconds: 250),
intensity: 0.7,
sharpness: 0.3,
)
.play();
```
* On **iOS** (iPhone 8+) this renders as a `CHHapticPattern` with
`hapticTransient` / `hapticContinuous` events.
* On **Android** (API 26+) `intensity` is mapped to amplitude; `sharpness`
is ignored (no perceptual analogue).
* On older devices, `play()` throws `UnsupportedHapticException` — guard
with `HapticCapabilities.query()` if you need graceful degradation.
## Animated widgets
The library ships with a set of drop-in widgets that combine an animation
with the right haptic at the right moment. Each one is a single
self-contained file in `lib/src/widgets/` — read one, copy the pattern.
| Widget | What it does | Pattern |
|--------|--------------|---------|
| [`HapticBounce`](#hapticbounce--tactile-bounce-on-tap) | Tap → squash → recoil → elastic settle | 3-segment `TweenSequence`, controller-driven |
| [`PressAndHoldToConfirm`](#pressandholdtoconfirm--long-press-with-progress-ring) | Hold to confirm with ring + densifying ticks | One controller drives ring + haptics + callback |
| `HapticToggle` | Animated switch + tick on flip | Custom-painted thumb with `easeOutBack` slide |
| `HapticSlider` | Slider with detent ticks | Detect detent crossings via `lastIndex` cache |
| `HapticStepper` | −/+ counter with bouncing buttons | Composes `HapticBounce` + `AnimatedSwitcher` |
| `HapticShake` | Wiggle + error notification | Externally triggered via `GlobalKey.shake()` |
| `SlideToConfirm` | Drag handle to end to confirm | Drag-driven controller with snap-back |
| `HapticRating` | Tap a star → cascading fill + tick per star | Sequenced `Timer.periodic` |
### `HapticBounce` — tactile bounce on tap
Wraps any widget with a press-down → recoil → elastic-settle animation
synchronised with a light/medium impact. Drop-in replacement for
`GestureDetector(onTap: …)` on buttons that should feel alive.
```dart
HapticBounce(
onTap: () => doSomething(),
child: Container(
padding: const EdgeInsets.all(24),
decoration: const BoxDecoration(/* ... */),
child: const Text('Press me'),
),
)
```
The scale follows a 3-segment `TweenSequence` with weights 1 : 2 : 3:
1. **squash** — `1.0 → 0.92`, `easeIn`
2. **recoil** — `0.92 → 1.12` (overshoots 1.0), `easeOutCubic`
3. **settle** — `1.12 → 1.0`, `elasticOut`
Set `bounceOnRelease: false` for a plain symmetric press with no overshoot.
`pressedScale` must be in `(0, 1)` and `overshootScale` must be `>= 1.0`
— violations throw `ArgumentError` at construction time, both in debug
and release.
### `PressAndHoldToConfirm` — long-press with progress ring
Requires the user to hold for [holdDuration] before firing `onConfirm`. A
circular progress ring renders at the finger position, and a 12-tick
haptic schedule fires at progressively shorter intervals — escalating
from `selection` → `light` → `medium` → `heavy`, sealed with a final
`heavy` impact at completion.
```dart
final key = GlobalKey();
PressAndHoldToConfirm(
key: key,
holdDuration: const Duration(seconds: 2),
onConfirm: () => unbox(),
child: const SizedBox(
height: 240,
child: Center(child: Icon(Icons.card_giftcard, size: 96)),
),
)
// Re-arm for another confirmation later:
key.currentState?.reset();
```
Architecture notes:
* A single `AnimationController` drives the ring, the haptic schedule
and the completion callback — no race conditions between independent
timers.
* Pointer events are captured with a raw `Listener` (not `GestureDetector`)
so the press starts immediately and the live finger position is
available.
* A single-pointer guard rejects secondary touches that would otherwise
restart the animation.
* Releasing early snaps the ring back to zero and resets the haptic
cursor — a re-press starts fresh.
### `HapticToggle` — animated switch with selection tick
```dart
HapticToggle(
value: _enabled,
onChanged: (v) => setState(() => _enabled = v),
)
```
### `HapticSlider` — slider with detent ticks
```dart
HapticSlider(
value: _v,
min: 0,
max: 100,
divisions: 10, // tick every 10 units
onChanged: (v) => setState(() => _v = v),
)
```
### `HapticStepper` — bouncy −/+ counter
```dart
HapticStepper(
value: _count,
min: 0,
max: 99,
onChanged: (v) => setState(() => _count = v),
)
```
### `HapticShake` — error wiggle
```dart
final shakeKey = GlobalKey();
HapticShake(key: shakeKey, child: TextField(/* ... */));
// On validation failure:
shakeKey.currentState?.shake();
```
### `SlideToConfirm` — drag-to-confirm pill
```dart
SlideToConfirm(
label: 'Slide to pay',
onConfirmed: () => pay(),
)
```
Light ticks at 25%, 50%, 75% of drag, heavy thump on completion. Releasing
before the end snaps back with a light tick.
### `HapticRating` — cascading stars
```dart
HapticRating(
value: _rating,
starCount: 5,
onChanged: (v) => setState(() => _rating = v),
)
```
Tapping the 4th star fires 4 selection ticks in sequence (one per star
"lighting up"), driven by a `Timer.periodic` with a 65ms cascade delay.
## Building your own widget
The widgets above are intentionally small (~100–200 lines each). To add
a new one, follow this pattern:
1. **One file per widget** in `lib/src/widgets/your_widget.dart`.
2. **Pick one of three pickers for what to do per gesture**:
- **Tap** — `GestureDetector(onTapDown / onTapUp / onTap / onTapCancel)`
when you want the press-down + release lifecycle.
- **Long-press / hold** — raw `Listener` so you get
`onPointerDown` / `onPointerUp` / `event.localPosition` immediately
and can implement single-pointer guards.
- **Drag** — `GestureDetector(onHorizontalDragUpdate / End)` for
anything slidey, or a draggable handle.
3. **One `AnimationController` per widget**, driving everything that
needs to stay in sync (visual change + haptic schedule + callbacks).
Avoid running a `Timer` alongside an `AnimationController` — they
drift, and the user feels the drift.
4. **Fire haptics from the `addListener` callback**, gated by a "what was
the last threshold I crossed" cursor (`int _lastIndex`, `Set
_fired`). `while` loops, not `if`, so a stuttered frame still fires
every tick it crossed.
5. **Pick the right haptic for the moment** — see the table below.
6. **Cancel cleanly**: stop the controller, reset cursors, snap value
back to zero. Atomic, in one method.
7. **Export from the barrel** in `lib/haptic_kit.dart`.
8. **Write a widget test** — see `test/widgets_test.dart` for the
pattern (mock the channel with `messenger.setMockMethodCallHandler`).
### Picking the right haptic
| Moment | Haptic | Why |
|--------|--------|-----|
| Crossing a discrete step (slider, picker, page) | `Haptics.selection()` | Quietest tap — never fatiguing |
| Press-down on a button | `Haptics.impact(light)` | Subtle "I felt your touch" |
| Release / tap completes | `Haptics.impact(medium)` | The "click" |
| Long-press completes / drag confirms | `Haptics.impact(heavy)` | Closes the loop with weight |
| Validation passed | `Haptics.notification(success)` | Two-tap pattern, recognisable |
| Soft error / boundary hit | `Haptics.notification(warning)` | Three-tap warning pattern |
| Hard error / wrong input | `Haptics.notification(error)` | Sharp triple-tap |
| Continuous waveform / heartbeat | `Vibration.vibrateWaveform(...)` | When duration matters more than crispness |
| Custom intensity + sharpness curve | `HapticPattern.builder()...play()` | Core Haptics on iOS, amplitude on Android |
## Capability detection
```dart
final caps = await HapticCapabilities.query();
if (caps.supportsCustomPatterns) {
await VibrationPatterns.success();
} else {
await Haptics.notification(HapticNotificationStyle.success);
}
```
## Error handling
All public APIs throw subclasses of `VibrationException`:
| Exception | Thrown when |
|-----------|-------------|
| `InvalidVibrationArgumentException` | A parameter is out of range (negative duration, amplitude > 255, mismatched lists, …) |
| `UnsupportedHapticException` | The device cannot render the requested capability |
| `PlatformVibrationException` | The native side returned an error or the plugin is not registered |
## Example app
A runnable demo lives in [`example/`](example/) — buttons for every kind of
feedback, side by side.
## License
MIT — see [LICENSE](LICENSE).