{"id":50319622,"url":"https://github.com/erykkruk/flutter_vibration_animation","last_synced_at":"2026-05-29T02:31:00.207Z","repository":{"id":354828970,"uuid":"1225420771","full_name":"erykkruk/flutter_vibration_animation","owner":"erykkruk","description":"Comprehensive vibration and haptic feedback for Flutter — Android \u0026 iOS. Impact, notification, selection, predefined effects, custom waveforms and Core Haptics intensity+sharpness patterns.","archived":false,"fork":false,"pushed_at":"2026-05-13T07:13:15.000Z","size":91,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-13T08:29:30.660Z","etag":null,"topics":["dart","flutter","haptic-feedback","haptics","vibration"],"latest_commit_sha":null,"homepage":null,"language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/erykkruk.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-30T09:01:34.000Z","updated_at":"2026-05-13T07:13:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/erykkruk/flutter_vibration_animation","commit_stats":null,"previous_names":["erykkruk/flutter_vibration_animation"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/erykkruk/flutter_vibration_animation","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erykkruk%2Fflutter_vibration_animation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erykkruk%2Fflutter_vibration_animation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erykkruk%2Fflutter_vibration_animation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erykkruk%2Fflutter_vibration_animation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/erykkruk","download_url":"https://codeload.github.com/erykkruk/flutter_vibration_animation/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erykkruk%2Fflutter_vibration_animation/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33634611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dart","flutter","haptic-feedback","haptics","vibration"],"created_at":"2026-05-29T02:30:58.431Z","updated_at":"2026-05-29T02:31:00.193Z","avatar_url":"https://github.com/erykkruk.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# haptic_kit\n\n[![pub package](https://img.shields.io/pub/v/haptic_kit.svg)](https://pub.dev/packages/haptic_kit)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\n**Haptic feedback, vibration and animated UI widgets** for Flutter — full\nAndroid \u0026 iOS implementations covering everything from quick UI taps to\ncustom Core Haptics patterns with intensity and sharpness curves, plus a\nset of production-ready widgets wired to the right haptic at the right\nmoment.\n\n\u003e Previously developed under the names `flutter_vibration_animation` and\n\u003e `flutter_haptics`. The repository URL is unchanged — only the package\n\u003e name and class identifiers were updated for consistency with the actual\n\u003e surface and pub.dev naming rules.\n\n---\n\n## Features\n\n- **`Haptics`** — short, semantic taps (named `Haptics` to avoid clashing with Flutter's own `HapticFeedback`)\n  - Impact: `light`, `medium`, `heavy`, `soft`, `rigid`\n  - Notification: `success`, `warning`, `error`\n  - Selection (for pickers, sliders, segmented controls)\n  - `prepare()` to pre-warm generators on iOS for lowest latency\n    (returns `true` on iOS, `false` no-op on Android)\n- **`Vibration`** — longer-form vibrations\n  - One-shot vibration with optional amplitude\n  - Custom waveforms with per-segment amplitudes\n  - Predefined OS effects (`tick`, `click`, `doubleClick`, `heavyClick`)\n  - Cancel any running vibration\n- **`HapticPattern`** — fluent builder for **Core Haptics** patterns\n  - Transient taps + continuous events\n  - Per-event `intensity` and `sharpness` (0.0–1.0)\n  - Automatic translation to Android amplitude waveforms\n- **`VibrationPatterns`** — ready-made: heartbeat, notification,\n  alarm, tick, success, failure, charge-up\n- **`HapticCapabilities`** — runtime detection of vibrator hardware,\n  amplitude control, Core Haptics, predefined effects\n- **`HapticBounce`** — drop-in tap wrapper with squash + recoil + elastic\n  settle bounce (3-segment `TweenSequence`), wired to light/medium impact\n- **`PressAndHoldToConfirm`** — long-press confirmation with a finger-tracking\n  progress ring and a 12-tick densifying haptic schedule that escalates\n  from `selection` → `light` → `medium` → `heavy`\n\n## Platform support\n\n| Feature | Android | iOS |\n|---------|---------|-----|\n| Impact / notification / selection | ✅ API 21+ (best on 26+) | ✅ iOS 10+ |\n| One-shot + amplitude | ✅ API 26+ | ✅ iPhone 8+ (Core Haptics) |\n| Custom waveforms | ✅ API 26+ | ✅ iPhone 8+ |\n| Predefined effects | ✅ API 29+ | ↩︎ mapped to closest impact |\n| Custom patterns (intensity + sharpness) | ✅ API 26+ | ✅ iPhone 8+ |\n| Capability detection | ✅ | ✅ |\n\n## Installation\n\n```yaml\ndependencies:\n  haptic_kit: ^1.0.0\n```\n\n### Android\n\nThe plugin's `AndroidManifest.xml` already declares `VIBRATE` — nothing else to do.\n\n### iOS\n\n`CoreHaptics`, `UIKit` and `AudioToolbox` are linked automatically through\nthe podspec. Minimum deployment target: iOS 12.0.\n\n## Quick start\n\n```dart\nimport 'package:haptic_kit/haptic_kit.dart';\n\n// Short UI taps\nawait Haptics.impact(HapticImpactStyle.medium);\nawait Haptics.notification(HapticNotificationStyle.success);\nawait Haptics.selection();\n\n// Longer vibrations\nawait Vibration.vibrate(duration: const Duration(milliseconds: 300));\n\n// Custom waveform — three pulses with growing amplitude\nawait Vibration.vibrateWaveform(\n  timings: const [\n    Duration.zero,\n    Duration(milliseconds: 100),\n    Duration(milliseconds: 100),\n    Duration(milliseconds: 100),\n    Duration(milliseconds: 100),\n    Duration(milliseconds: 100),\n  ],\n  amplitudes: const [0, 80, 0, 160, 0, 255],\n);\n\n// Predefined OS effect\nawait Vibration.playPredefined(PredefinedEffect.doubleClick);\n\n// Ready-made pattern\nawait VibrationPatterns.heartbeat();\n```\n\n## Custom haptic patterns (Core Haptics)\n\n```dart\nawait HapticPattern.builder()\n    .tap(intensity: 0.4, sharpness: 0.6)\n    .pause(const Duration(milliseconds: 80))\n    .tap(intensity: 1.0, sharpness: 0.9)\n    .continuous(\n      duration: const Duration(milliseconds: 250),\n      intensity: 0.7,\n      sharpness: 0.3,\n    )\n    .play();\n```\n\n* On **iOS** (iPhone 8+) this renders as a `CHHapticPattern` with\n  `hapticTransient` / `hapticContinuous` events.\n* On **Android** (API 26+) `intensity` is mapped to amplitude; `sharpness`\n  is ignored (no perceptual analogue).\n* On older devices, `play()` throws `UnsupportedHapticException` — guard\n  with `HapticCapabilities.query()` if you need graceful degradation.\n\n## Animated widgets\n\nThe library ships with a set of drop-in widgets that combine an animation\nwith the right haptic at the right moment. Each one is a single\nself-contained file in `lib/src/widgets/` — read one, copy the pattern.\n\n| Widget | What it does | Pattern |\n|--------|--------------|---------|\n| [`HapticBounce`](#hapticbounce--tactile-bounce-on-tap) | Tap → squash → recoil → elastic settle | 3-segment `TweenSequence`, controller-driven |\n| [`PressAndHoldToConfirm`](#pressandholdtoconfirm--long-press-with-progress-ring) | Hold to confirm with ring + densifying ticks | One controller drives ring + haptics + callback |\n| `HapticToggle` | Animated switch + tick on flip | Custom-painted thumb with `easeOutBack` slide |\n| `HapticSlider` | Slider with detent ticks | Detect detent crossings via `lastIndex` cache |\n| `HapticStepper` | −/+ counter with bouncing buttons | Composes `HapticBounce` + `AnimatedSwitcher` |\n| `HapticShake` | Wiggle + error notification | Externally triggered via `GlobalKey\u003cState\u003e.shake()` |\n| `SlideToConfirm` | Drag handle to end to confirm | Drag-driven controller with snap-back |\n| `HapticRating` | Tap a star → cascading fill + tick per star | Sequenced `Timer.periodic` |\n\n### `HapticBounce` — tactile bounce on tap\n\nWraps any widget with a press-down → recoil → elastic-settle animation\nsynchronised with a light/medium impact. Drop-in replacement for\n`GestureDetector(onTap: …)` on buttons that should feel alive.\n\n```dart\nHapticBounce(\n  onTap: () =\u003e doSomething(),\n  child: Container(\n    padding: const EdgeInsets.all(24),\n    decoration: const BoxDecoration(/* ... */),\n    child: const Text('Press me'),\n  ),\n)\n```\n\nThe scale follows a 3-segment `TweenSequence` with weights 1 : 2 : 3:\n\n1. **squash** — `1.0 → 0.92`, `easeIn`\n2. **recoil** — `0.92 → 1.12` (overshoots 1.0), `easeOutCubic`\n3. **settle** — `1.12 → 1.0`, `elasticOut`\n\nSet `bounceOnRelease: false` for a plain symmetric press with no overshoot.\n\n`pressedScale` must be in `(0, 1)` and `overshootScale` must be `\u003e= 1.0`\n— violations throw `ArgumentError` at construction time, both in debug\nand release.\n\n### `PressAndHoldToConfirm` — long-press with progress ring\n\nRequires the user to hold for [holdDuration] before firing `onConfirm`. A\ncircular progress ring renders at the finger position, and a 12-tick\nhaptic schedule fires at progressively shorter intervals — escalating\nfrom `selection` → `light` → `medium` → `heavy`, sealed with a final\n`heavy` impact at completion.\n\n```dart\nfinal key = GlobalKey\u003cPressAndHoldToConfirmState\u003e();\n\nPressAndHoldToConfirm(\n  key: key,\n  holdDuration: const Duration(seconds: 2),\n  onConfirm: () =\u003e unbox(),\n  child: const SizedBox(\n    height: 240,\n    child: Center(child: Icon(Icons.card_giftcard, size: 96)),\n  ),\n)\n\n// Re-arm for another confirmation later:\nkey.currentState?.reset();\n```\n\nArchitecture notes:\n\n* A single `AnimationController` drives the ring, the haptic schedule\n  and the completion callback — no race conditions between independent\n  timers.\n* Pointer events are captured with a raw `Listener` (not `GestureDetector`)\n  so the press starts immediately and the live finger position is\n  available.\n* A single-pointer guard rejects secondary touches that would otherwise\n  restart the animation.\n* Releasing early snaps the ring back to zero and resets the haptic\n  cursor — a re-press starts fresh.\n\n### `HapticToggle` — animated switch with selection tick\n\n```dart\nHapticToggle(\n  value: _enabled,\n  onChanged: (v) =\u003e setState(() =\u003e _enabled = v),\n)\n```\n\n### `HapticSlider` — slider with detent ticks\n\n```dart\nHapticSlider(\n  value: _v,\n  min: 0,\n  max: 100,\n  divisions: 10,                              // tick every 10 units\n  onChanged: (v) =\u003e setState(() =\u003e _v = v),\n)\n```\n\n### `HapticStepper` — bouncy −/+ counter\n\n```dart\nHapticStepper(\n  value: _count,\n  min: 0,\n  max: 99,\n  onChanged: (v) =\u003e setState(() =\u003e _count = v),\n)\n```\n\n### `HapticShake` — error wiggle\n\n```dart\nfinal shakeKey = GlobalKey\u003cHapticShakeState\u003e();\n\nHapticShake(key: shakeKey, child: TextField(/* ... */));\n\n// On validation failure:\nshakeKey.currentState?.shake();\n```\n\n### `SlideToConfirm` — drag-to-confirm pill\n\n```dart\nSlideToConfirm(\n  label: 'Slide to pay',\n  onConfirmed: () =\u003e pay(),\n)\n```\n\nLight ticks at 25%, 50%, 75% of drag, heavy thump on completion. Releasing\nbefore the end snaps back with a light tick.\n\n### `HapticRating` — cascading stars\n\n```dart\nHapticRating(\n  value: _rating,\n  starCount: 5,\n  onChanged: (v) =\u003e setState(() =\u003e _rating = v),\n)\n```\n\nTapping the 4th star fires 4 selection ticks in sequence (one per star\n\"lighting up\"), driven by a `Timer.periodic` with a 65ms cascade delay.\n\n## Building your own widget\n\nThe widgets above are intentionally small (~100–200 lines each). To add\na new one, follow this pattern:\n\n1. **One file per widget** in `lib/src/widgets/your_widget.dart`.\n2. **Pick one of three pickers for what to do per gesture**:\n   - **Tap** — `GestureDetector(onTapDown / onTapUp / onTap / onTapCancel)`\n     when you want the press-down + release lifecycle.\n   - **Long-press / hold** — raw `Listener` so you get\n     `onPointerDown` / `onPointerUp` / `event.localPosition` immediately\n     and can implement single-pointer guards.\n   - **Drag** — `GestureDetector(onHorizontalDragUpdate / End)` for\n     anything slidey, or a draggable handle.\n3. **One `AnimationController` per widget**, driving everything that\n   needs to stay in sync (visual change + haptic schedule + callbacks).\n   Avoid running a `Timer` alongside an `AnimationController` — they\n   drift, and the user feels the drift.\n4. **Fire haptics from the `addListener` callback**, gated by a \"what was\n   the last threshold I crossed\" cursor (`int _lastIndex`, `Set\u003cdouble\u003e\n   _fired`). `while` loops, not `if`, so a stuttered frame still fires\n   every tick it crossed.\n5. **Pick the right haptic for the moment** — see the table below.\n6. **Cancel cleanly**: stop the controller, reset cursors, snap value\n   back to zero. Atomic, in one method.\n7. **Export from the barrel** in `lib/haptic_kit.dart`.\n8. **Write a widget test** — see `test/widgets_test.dart` for the\n   pattern (mock the channel with `messenger.setMockMethodCallHandler`).\n\n### Picking the right haptic\n\n| Moment | Haptic | Why |\n|--------|--------|-----|\n| Crossing a discrete step (slider, picker, page) | `Haptics.selection()` | Quietest tap — never fatiguing |\n| Press-down on a button | `Haptics.impact(light)` | Subtle \"I felt your touch\" |\n| Release / tap completes | `Haptics.impact(medium)` | The \"click\" |\n| Long-press completes / drag confirms | `Haptics.impact(heavy)` | Closes the loop with weight |\n| Validation passed | `Haptics.notification(success)` | Two-tap pattern, recognisable |\n| Soft error / boundary hit | `Haptics.notification(warning)` | Three-tap warning pattern |\n| Hard error / wrong input | `Haptics.notification(error)` | Sharp triple-tap |\n| Continuous waveform / heartbeat | `Vibration.vibrateWaveform(...)` | When duration matters more than crispness |\n| Custom intensity + sharpness curve | `HapticPattern.builder()...play()` | Core Haptics on iOS, amplitude on Android |\n\n## Capability detection\n\n```dart\nfinal caps = await HapticCapabilities.query();\nif (caps.supportsCustomPatterns) {\n  await VibrationPatterns.success();\n} else {\n  await Haptics.notification(HapticNotificationStyle.success);\n}\n```\n\n## Error handling\n\nAll public APIs throw subclasses of `VibrationException`:\n\n| Exception | Thrown when |\n|-----------|-------------|\n| `InvalidVibrationArgumentException` | A parameter is out of range (negative duration, amplitude \u003e 255, mismatched lists, …) |\n| `UnsupportedHapticException` | The device cannot render the requested capability |\n| `PlatformVibrationException` | The native side returned an error or the plugin is not registered |\n\n## Example app\n\nA runnable demo lives in [`example/`](example/) — buttons for every kind of\nfeedback, side by side.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferykkruk%2Fflutter_vibration_animation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ferykkruk%2Fflutter_vibration_animation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferykkruk%2Fflutter_vibration_animation/lists"}