haptic_kit
用于 Flutter 的触觉反馈、振动和动画 UI 小部件 —— Android 和 iOS。冲击、通知、选择、预设效果、自定义波形、核心触觉模式,以及 8 个可直接投入生产的组件。
Flutter 的全面振动和触觉反馈 —— Android 和 iOS。冲击、通知、选择、预设效果、自定义波形以及 Core Haptics 强度和锐度模式。
{"sdk":"flutter"}{"sdk":"flutter"}^6.0.0以下为英文项目原文快照,最新内容请访问 GitHub。
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_animationandflutter_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.
Full hosted documentation lives on the Codigee open-source site:
HapticBounce, PressAndHoldToConfirm, …).Haptics, Vibration, HapticPattern and VibrationPatterns.Haptics — short, semantic taps (named Haptics to avoid clashing with Flutter's own HapticFeedback)
light, medium, heavy, soft, rigidsuccess, warning, errorprepare() to pre-warm generators on iOS for lowest latency
(returns true on iOS, false no-op on Android)Vibration — longer-form vibrations
tick, click, doubleClick, heavyClick)HapticPattern — fluent builder for Core Haptics patterns
intensity and sharpness (0.0–1.0)VibrationPatterns — ready-made: heartbeat, notification,
alarm, tick, success, failure, charge-upHapticCapabilities — runtime detection of vibrator hardware,
amplitude control, Core Haptics, predefined effectsHapticBounce — drop-in tap wrapper with squash + recoil + elastic
settle bounce (3-segment TweenSequence), wired to light/medium impactPressAndHoldToConfirm — long-press confirmation with a finger-tracking
progress ring and a 12-tick densifying haptic schedule that escalates
from selection → light → medium → heavy| 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 | ✅ | ✅ |
dependencies:
haptic_kit: ^1.0.0
The plugin's AndroidManifest.xml already declares VIBRATE — nothing else to do.
CoreHaptics, UIKit and AudioToolbox are linked automatically through
the podspec. Minimum deployment target: iOS 12.0.
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();
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();
CHHapticPattern with
hapticTransient / hapticContinuous events.intensity is mapped to amplitude; sharpness
is ignored (no perceptual analogue).play() throws UnsupportedHapticException — guard
with HapticCapabilities.query() if you need graceful degradation.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 |
Tap → squash → recoil → elastic settle | 3-segment TweenSequence, controller-driven |
PressAndHoldToConfirm |
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<State>.shake() |
HapticPulse |
Looping breathing pulse + tick per beat | Auto-playing controller, start()/stop() via GlobalKey<State> |
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 tapWraps 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.
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.0 → 0.92, easeIn0.92 → 1.12 (overshoots 1.0), easeOutCubic1.12 → 1.0, elasticOutSet 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 ringRequires 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.
final key = GlobalKey<PressAndHoldToConfirmState>();
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:
AnimationController drives the ring, the haptic schedule
and the completion callback — no race conditions between independent
timers.Listener (not GestureDetector)
so the press starts immediately and the live finger position is
available.HapticToggle — animated switch with selection tickHapticToggle(
value: _enabled,
onChanged: (v) => setState(() => _enabled = v),
)
HapticSlider — slider with detent ticksHapticSlider(
value: _v,
min: 0,
max: 100,
divisions: 10, // tick every 10 units
onChanged: (v) => setState(() => _v = v),
)
HapticStepper — bouncy −/+ counterHapticStepper(
value: _count,
min: 0,
max: 99,
onChanged: (v) => setState(() => _count = v),
)
HapticShake — error wigglefinal shakeKey = GlobalKey<HapticShakeState>();
HapticShake(key: shakeKey, child: TextField(/* ... */));
// On validation failure:
shakeKey.currentState?.shake();
HapticPulse — looping attention pulseThe breathing counterpart to HapticShake: a looping minScale → maxScale
pulse that fires a light impact on every beat. Use it to draw the eye to a
CTA, an unread badge or a recording dot. It starts pulsing on mount by
default:
HapticPulse(
child: const Icon(Icons.notifications),
)
For manual control, set autoPlay: false and drive it via a GlobalKey:
final pulseKey = GlobalKey<HapticPulseState>();
HapticPulse(
key: pulseKey,
autoPlay: false,
pulseCount: 3, // stop after 3 beats; omit for an infinite pulse
impactStyle: HapticImpactStyle.medium,
child: const Icon(Icons.notifications),
);
pulseKey.currentState?.start();
// ...later:
pulseKey.currentState?.stop();
An infinite pulse never settles — in widget tests, drive the clock with
tester.pump(duration)rather thantester.pumpAndSettle(), or pass a finitepulseCount.
SlideToConfirm — drag-to-confirm pillSlideToConfirm(
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 starsHapticRating(
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.
The widgets above are intentionally small (~100–200 lines each). To add a new one, follow this pattern:
lib/src/widgets/your_widget.dart.GestureDetector(onTapDown / onTapUp / onTap / onTapCancel)
when you want the press-down + release lifecycle.Listener so you get
onPointerDown / onPointerUp / event.localPosition immediately
and can implement single-pointer guards.GestureDetector(onHorizontalDragUpdate / End) for
anything slidey, or a draggable handle.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.addListener callback, gated by a "what was
the last threshold I crossed" cursor (int _lastIndex, Set<double> _fired). while loops, not if, so a stuttered frame still fires
every tick it crossed.lib/haptic_kit.dart.test/widgets_test.dart for the
pattern (mock the channel with messenger.setMockMethodCallHandler).| 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 |
Most apps ship a "Haptics" toggle in their settings, and then wrap every
call in the same if. Flip it once instead:
HapticSettings.enabled = preferences.hapticsEnabled;
// A no-op now, from anywhere, with no check at the call site
await Haptics.impact(HapticImpactStyle.light);
await VibrationPatterns.success();
While disabled, calls return normally and never reach the method channel, so an app whose user turned haptics off makes no platform calls at all.
Two things deliberately keep working while disabled:
Vibration.cancel() - refusing to cancel would leave the device buzzing
after the user opted out.This is the app's own preference. It does not read the system-level haptic setting, which the platforms apply themselves underneath.
// Cached: the platform is queried once, values cannot change at runtime
final caps = await HapticSettings.capabilities;
// Covers the device and the app preference in one check
if (await HapticSettings.isAvailable) {
await VibrationPatterns.success();
}
HapticCapabilities.query() remains available for a deliberately fresh read:
final caps = await HapticCapabilities.query();
if (caps.supportsCustomPatterns) {
await VibrationPatterns.success();
} else {
await Haptics.notification(HapticNotificationStyle.success);
}
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 |
A runnable demo lives in example/ — buttons for every kind of
feedback, side by side.
MIT — see LICENSE.