Flutter 3.38 and Dart 3.10 are here!Learn more
Animate a widget using a physics simulation
How to implement a physics animation.
Physics simulations can make app interactions feel realistic and interactive. For example, you might want to animate a widget to act as if it were attached to a spring or falling with gravity.
This recipe demonstrates how to move a widget from a dragged point back to the center using a spring simulation.
This recipe uses these steps:
- Set up an animation controller
- Move the widget using gestures
- Animate the widget
- Calculate the velocity to simulate a springing motion
Step 1: Set up an animation controller
#Start with a stateful widget calledDraggableCard:
import'package:flutter/material.dart';voidmain(){runApp(constMaterialApp(home:PhysicsCardDragDemo()));}classPhysicsCardDragDemoextendsStatelessWidget{constPhysicsCardDragDemo({super.key});@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(),body:constDraggableCard(child:FlutterLogo(size:128)),);}}classDraggableCardextendsStatefulWidget{constDraggableCard({requiredthis.child,super.key});finalWidgetchild;@overrideState<DraggableCard>createState()=>_DraggableCardState();}class_DraggableCardStateextendsState<DraggableCard>{@overridevoidinitState(){super.initState();}@overridevoiddispose(){super.dispose();}@overrideWidgetbuild(BuildContextcontext){returnAlign(child:Card(child:widget.child));}} Make the_DraggableCardState class extend fromSingleTickerProviderStateMixin. Then construct anAnimationController ininitState and setvsync tothis.
ExtendingSingleTickerProviderStateMixin allows the state object to be aTickerProvider for theAnimationController. For more information, see the documentation forTickerProvider.
class_DraggableCardStateextendsState<DraggableCard>{class_DraggableCardStateextendsState<DraggableCard>withSingleTickerProviderStateMixin{lateAnimationController_controller;@overridevoidinitState(){super.initState();_controller=AnimationController(vsync:this,duration:constDuration(seconds:1));}@overridevoiddispose(){_controller.dispose();super.dispose();}Step 2: Move the widget using gestures
# Make the widget move when it's dragged, and add anAlignment field to the_DraggableCardState class:
class_DraggableCardStateextendsState<DraggableCard>withSingleTickerProviderStateMixin{lateAnimationController_controller;Alignment_dragAlignment=Alignment.center; Add aGestureDetector that handles theonPanDown,onPanUpdate, andonPanEnd callbacks. To adjust the alignment, use aMediaQuery to get the size of the widget, and divide by 2. (This converts units of "pixels dragged" to coordinates thatAlign uses.) Then, set theAlign widget'salignment to_dragAlignment:
@overrideWidgetbuild(BuildContextcontext){returnAlign(child:Card(child:widget.child,varsize=MediaQuery.of(context).size;returnGestureDetector(onPanDown:(details){},onPanUpdate:(details){setState((){_dragAlignment+=Alignment(details.delta.dx/(size.width/2),details.delta.dy/(size.height/2),);});},onPanEnd:(details){},child:Align(alignment:_dragAlignment,child:Card(child:widget.child,),),);}Step 3: Animate the widget
#When the widget is released, it should spring back to the center.
Add anAnimation<Alignment> field and an_runAnimation method. This method defines aTween that interpolates between the point the widget was dragged to, to the point in the center.
class_DraggableCardStateextendsState<DraggableCard>withSingleTickerProviderStateMixin{lateAnimationController_controller;lateAnimation<Alignment>_animation;Alignment_dragAlignment=Alignment.center;void_runAnimation(){_animation=_controller.drive(AlignmentTween(begin:_dragAlignment,end:Alignment.center),);_controller.reset();_controller.forward();} Next, update_dragAlignment when theAnimationController produces a value:
@overridevoidinitState(){super.initState();_controller=AnimationController(vsync:this,duration:constDuration(seconds:1));_controller.addListener((){setState((){_dragAlignment=_animation.value;});});}Next, make theAlign widget use the_dragAlignment field:
child:Align(alignment:_dragAlignment,child:Card(child:widget.child),),Finally, update theGestureDetector to manage the animation controller:
returnGestureDetector(onPanDown:(details){},onPanDown:(details){_controller.stop();},onPanUpdate:(details){// ...},onPanEnd:(details){},onPanEnd:(details){_runAnimation();},child:Align(Step 4: Calculate the velocity to simulate a springing motion
# The last step is to do a little math, to calculate the velocity of the widget after it's finished being dragged. This is so that the widget realistically continues at that speed before being snapped back. (The_runAnimation method already sets the direction by setting the animation's start and end alignment.)
First, import thephysics package:
import'package:flutter/physics.dart'; TheonPanEnd callback provides aDragEndDetails object. This object provides the velocity of the pointer when it stopped contacting the screen. The velocity is in pixels per second, but theAlign widget doesn't use pixels. It uses coordinate values between [-1.0, -1.0] and [1.0, 1.0], where [0.0, 0.0] represents the center. Thesize calculated in step 2 is used to convert pixels to coordinate values in this range.
Finally,AnimationController has ananimateWith() method that can be given aSpringSimulation:
/// Calculates and runs a [SpringSimulation].void_runAnimation(OffsetpixelsPerSecond,Sizesize){_animation=_controller.drive(AlignmentTween(begin:_dragAlignment,end:Alignment.center),);// Calculate the velocity relative to the unit interval, [0,1],// used by the animation controller.finalunitsPerSecondX=pixelsPerSecond.dx/size.width;finalunitsPerSecondY=pixelsPerSecond.dy/size.height;finalunitsPerSecond=Offset(unitsPerSecondX,unitsPerSecondY);finalunitVelocity=unitsPerSecond.distance;constspring=SpringDescription(mass:1,stiffness:1,damping:1);finalsimulation=SpringSimulation(spring,0,1,-unitVelocity);_controller.animateWith(simulation);}Don't forget to call_runAnimation() with the velocity and size:
onPanEnd:(details){_runAnimation(details.velocity.pixelsPerSecond,size);}, Now that the animation controller uses a simulation, itsduration argument is no longer required.
Interactive Example
#import 'package:flutter/material.dart';import 'package:flutter/physics.dart';void main() { runApp(const MaterialApp(home: PhysicsCardDragDemo()));}class PhysicsCardDragDemo extends StatelessWidget { const PhysicsCardDragDemo({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: const DraggableCard(child: FlutterLogo(size: 128)), ); }}/// A draggable card that moves back to [Alignment.center] when it's/// released.class DraggableCard extends StatefulWidget { const DraggableCard({required this.child, super.key}); final Widget child; @override State<DraggableCard> createState() => _DraggableCardState();}class _DraggableCardState extends State<DraggableCard> with SingleTickerProviderStateMixin { late AnimationController _controller; /// The alignment of the card as it is dragged or being animated. /// /// While the card is being dragged, this value is set to the values computed /// in the GestureDetector onPanUpdate callback. If the animation is running, /// this value is set to the value of the [_animation]. Alignment _dragAlignment = Alignment.center; late Animation<Alignment> _animation; /// Calculates and runs a [SpringSimulation]. void _runAnimation(Offset pixelsPerSecond, Size size) { _animation = _controller.drive( AlignmentTween(begin: _dragAlignment, end: Alignment.center), ); // Calculate the velocity relative to the unit interval, [0,1], // used by the animation controller. final unitsPerSecondX = pixelsPerSecond.dx / size.width; final unitsPerSecondY = pixelsPerSecond.dy / size.height; final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY); final unitVelocity = unitsPerSecond.distance; const spring = SpringDescription(mass: 1, stiffness: 1, damping: 1); final simulation = SpringSimulation(spring, 0, 1, -unitVelocity); _controller.animateWith(simulation); } @override void initState() { super.initState(); _controller = AnimationController(vsync: this); _controller.addListener(() { setState(() { _dragAlignment = _animation.value; }); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return GestureDetector( onPanDown: (details) { _controller.stop(); }, onPanUpdate: (details) { setState(() { _dragAlignment += Alignment( details.delta.dx / (size.width / 2), details.delta.dy / (size.height / 2), ); }); }, onPanEnd: (details) { _runAnimation(details.velocity.pixelsPerSecond, size); }, child: Align( alignment: _dragAlignment, child: Card(child: widget.child), ), ); }}Unless stated otherwise, the documentation on this site reflects Flutter 3.38.1. Page last updated on 2025-10-28.View source orreport an issue.