gesture_physics_animator πŸŒ€

Turn any widget into a spring-backed, draggable, elastic element β€” driven by real physics, not a tween that fakes it.

Free drag, bounded, and snap-to-dock demos

πŸ“± Platforms supported

Pure Dart/Flutter β€” no platform channels, no native code β€” so it runs anywhere Flutter does:

Android iOS Web Windows macOS Linux
βœ… βœ… βœ… βœ… βœ… βœ…

✨ Why not just Tween + Curves.elasticOut? πŸ€”

Most "drag and snap back" widgets animate release with a Tween and a canned curve. That looks fine for a slow drag, but a fast flick feels disconnected β€” the curve has no idea how fast you were actually moving, so the snap-back always starts from a standstill.

GesturePhysicsAnimator seeds a real SpringSimulation (or, when not snapping back, a FrictionSimulation) with the actual release velocity from your drag, driven through a raw AnimationController via animateWith() β€” never a Tween. The result carries your throw's momentum straight into the settle animation: overshoot-and-settle, elastic boundary bouncing, momentum-based throws, all continuous with how you actually moved your finger. 🎯

πŸš€ Features

  • 🧲 Velocity-seeded spring release β€” no jarring switch to a "fake" curve
  • 🧱 Elastic boundary bouncing with configurable energy loss per bounce
  • 🎯 Snap-to-target (dock icons, layout guides, anywhere you want)
  • πŸ”’ Optional single-axis locking
  • πŸŽ›οΈ Named spring presets (gentle, snappy, bouncy) plus full control over raw SpringDescription values
  • πŸͺΆ Physics math is fully isolated and unit-tested independent of widgets

πŸ“¦ Installation

dependencies:
  gesture_physics_animator: ^0.1.0

⚑ Quick start

import 'package:gesture_physics_animator/gesture_physics_animator.dart';

GesturePhysicsAnimator(
  spring: SpringPresets.snappy,
  child: const FlutterLogo(size: 80),
)

That's it β€” drag it, flick it, watch it settle. Since it moves its child with a Transform (no extra layout space reserved), it's usually placed inside a Stack for free 2-D dragging:

Stack(
  children: [
    GesturePhysicsAnimator(
      spring: SpringPresets.bouncy,
      bounds: Rect.fromLTRB(-150, -150, 150, 150),
      boundaryDamping: 0.4,
      child: const FlutterLogo(size: 80),
    ),
  ],
)

πŸŽ›οΈ Physics parameters, in plain terms

SpringDescription has three numbers. You don't need a physics degree to tune them:

Parameter Plain-English meaning
stiffness How hard it pulls back toward rest. Higher = snappier, faster to settle. Think "how stiff is the rubber band."
damping How much the motion is resisted. Higher = less bouncy, fewer (or zero) oscillations before it stops. Think "how much air resistance."
mass How heavy the object feels. Higher = slower to speed up and slower to slow down, for the same stiffness/damping.

Don't want to hand-tune these? Use a preset: 🧘 SpringPresets.gentle for a calm, controlled settle Β· ⚑ SpringPresets.snappy (the default) for a crisp, native-feeling response Β· πŸ€ SpringPresets.bouncy for a playful, elastic bounce.

GesturePhysicsAnimator(
  spring: SpringPresets.gentle, // or .snappy, .bouncy, or your own SpringDescription(...)
  child: myWidget,
)

🧱 Bounded, elastic dragging

Give it a bounds rectangle (relative to the widget's own starting position β€” Offset.zero is always inside it) and it bounces elastically off the edges instead of dragging straight through them:

GesturePhysicsAnimator(
  bounds: Rect.fromLTRB(-100, -100, 100, 100),
  boundaryDamping: 0.5, // 0 = perfectly elastic, 1 = stops dead at the edge
  child: myWidget,
)

🎯 Snapping to a target

By default the widget always springs back to its origin on release (snapBackToOrigin: true). Point it at a custom rest position with snapTarget, or set snapBackToOrigin: false to let a throw keep going and settle wherever momentum carries it β€” useful for "leave it where you dropped it" widgets like a draggable FAB.

🧩 Presets

  • DraggableCard β€” the common "drag it, let go, it springs back" card
  • ElasticBoundedDraggable β€” bounded + bouncy, for a draggable FAB or floating panel that shouldn't leave the screen
DraggableCard(child: myCard);

ElasticBoundedDraggable(
  bounds: Rect.fromLTRB(-200, -400, 200, 400),
  child: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
);

πŸ–ΌοΈ Example app

The example/ app has three interactive demos:

  1. Free drag β€” sliders for stiffness and damping so you can feel the difference live
  2. Bounded β€” a card that bounces elastically off the screen edges
  3. Snap dock β€” drag and release snaps to the nearest of three dock zones

Run it with:

cd example
flutter run

πŸ§ͺ Testing

The physics core (GestureSpringController, BoundedAxisSimulation) is plain Dart with zero widget dependencies, so it's tested by driving it directly with TestVSync and a manually-pumped clock β€” no widget tree, no synthesized pointer events. Gesture-level behavior (drag, fling, bounds, axis lock, interrupted animations) is covered separately with real WidgetTester.fling()/drag() simulation. See test/.

πŸ“„ License

MIT β€” see LICENSE.

Libraries

gesture_physics_animator
Velocity-seeded spring physics for draggable widgets: real momentum, elastic boundary bouncing, and configurable damping β€” not tween-based "snap back" animation.