Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Implement Flutter animations: implicit, explicit, hero, staggered, and physics-based with a decision tree guide.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/physics.md
1# Physics-Based Animations Reference23Physics-based animations create natural-feeling motion using simulations like springs, gravity, and momentum.45## Core Concept67Instead of linear interpolation, use physics simulations for realistic motion that responds naturally to forces like velocity and damping.89## Fling Animation1011### Basic Fling1213```dart14class FlingWidget extends StatefulWidget {15@override16State<FlingWidget> createState() => _FlingWidgetState();17}1819class _FlingWidgetState extends State<FlingWidget>20with SingleTickerProviderStateMixin {21late AnimationController _controller;2223@override24void initState() {25super.initState();26_controller = AnimationController(27duration: const Duration(seconds: 1),28vsync: this,29);30_controller.fling(velocity: 2.0);31}3233@override34void dispose() {35_controller.dispose();36super.dispose();37}3839@override40Widget build(BuildContext context) {41return AnimatedBuilder(42animation: _controller,43builder: (context, child) {44return Transform.translate(45offset: Offset(_controller.value * 100, 0),46child: child,47);48},49child: const FlutterLogo(),50);51}52}53```5455### Fling with Gesture5657```dart58class DraggableFling extends StatefulWidget {59@override60State<DraggableFling> createState() => _DraggableFlingState();61}6263class _DraggableFlingState extends State<DraggableFling>64with SingleTickerProviderStateMixin {65late AnimationController _controller;66double _dragX = 0;67Offset? _startPosition;6869@override70void initState() {71super.initState();72_controller = AnimationController.unbounded(vsync: this);73}7475@override76void dispose() {77_controller.dispose();78super.dispose();79}8081void _handlePanStart(DragStartDetails details) {82_controller.stop();83_startPosition = details.globalPosition;84}8586void _handlePanUpdate(DragUpdateDetails details) {87if (_startPosition != null) {88setState(() {89_dragX += details.delta.dx;90});91}92}9394void _handlePanEnd(DragEndDetails details) {95_controller.fling(velocity: details.velocity.pixelsPerSecond.dx / 1000);96}9798@override99Widget build(BuildContext context) {100return GestureDetector(101onPanStart: _handlePanStart,102onPanUpdate: _handlePanUpdate,103onPanEnd: _handlePanEnd,104child: AnimatedBuilder(105animation: _controller,106builder: (context, child) {107return Transform.translate(108offset: Offset(_dragX + _controller.value, 0),109child: child,110);111},112child: const FlutterLogo(),113),114);115}116}117```118119## Spring Simulation120121### Basic Spring122123```dart124class SpringAnimation extends StatefulWidget {125@override126State<SpringAnimation> createState() => _SpringAnimationState();127}128129class _SpringAnimationState extends State<SpringAnimation>130with SingleTickerProviderStateMixin {131late AnimationController _controller;132133@override134void initState() {135super.initState();136_controller = AnimationController.unbounded(vsync: this);137138_controller.animateWith(139SpringSimulation(140spring: const SpringDescription(141mass: 1.0,142stiffness: 200.0,143damping: 10.0,144),145start: 0.0,146end: 1.0,147velocity: 0.0,148),149);150}151152@override153void dispose() {154_controller.dispose();155super.dispose();156}157158@override159Widget build(BuildContext context) {160return AnimatedBuilder(161animation: _controller,162builder: (context, child) {163return Transform.scale(164scale: _controller.value,165child: child,166);167},168child: const FlutterLogo(),169);170}171}172```173174### Spring Description Parameters175176```dart177SpringDescription(178mass: 1.0, // Mass of the object (higher = more inertia)179stiffness: 200.0, // Spring stiffness (higher = faster, stiffer)180damping: 10.0, // Damping ratio (higher = less oscillation)181)182)183```184185**Mass:**186- How "heavy" the object feels187- Typical values: 0.5 - 5.0188- Higher mass = more momentum, slower response189190**Stiffness:**191- How stiff the spring is192- Typical values: 100 - 500193- Higher stiffness = faster oscillation194- Lower stiffness = slower, more "bouncy"195196**Damping:**197- How quickly oscillation stops198- Typical values: 5 - 20199- Higher damping = less bounce200- Lower damping = more bounce201- Critical damping: ~15-18202203### Spring Presets204205**Bouncy:**206```dart207SpringDescription(208mass: 0.5,209stiffness: 300,210damping: 8,211)212```213214**Snappy:**215```dart216SpringDescription(217mass: 1.0,218stiffness: 400,219damping: 18,220)221```222223**Gentle:**224```dart225SpringDescription(226mass: 2.0,227stiffness: 150,228damping: 20,229)230```231232## Gravity Simulation233234### Falling Animation235236```dart237class GravityAnimation extends StatefulWidget {238@override239State<GravityAnimation> createState() => _GravityAnimationState();240}241242class _GravityAnimationState extends State<GravityAnimation>243with SingleTickerProviderStateMixin {244late AnimationController _controller;245246@override247void initState() {248super.initState();249_controller = AnimationController.unbounded(vsync: this);250251_controller.animateWith(252GravitySimulation(253acceleration: 980, // pixels/s²254distance: 500, // pixels to fall255startDistance: 0,256),257);258}259260@override261void dispose() {262_controller.dispose();263super.dispose();264}265266@override267Widget build(BuildContext context) {268return AnimatedBuilder(269animation: _controller,270builder: (context, child) {271return Transform.translate(272offset: Offset(0, _controller.value),273child: child,274);275},276child: const FlutterLogo(),277);278}279}280```281282## Scroll Physics283284### Bouncing Scroll285286```dart287CustomScrollView(288physics: const BouncingScrollPhysics(),289slivers: [290SliverList(291delegate: SliverChildListDelegate([292// Items293]),294),295],296)297```298299### Clamping Scroll300301```dart302CustomScrollView(303physics: const ClampingScrollPhysics(),304slivers: [305SliverList(306delegate: SliverChildListDelegate([307// Items308]),309),310],311)312```313314### Fixed Scroll315316```dart317CustomScrollView(318physics: const NeverScrollableScrollPhysics(),319slivers: [320SliverList(321delegate: SliverChildListDelegate([322// Items323]),324),325],326)327```328329### Platform-Adaptive Scroll330331```dart332CustomScrollView(333physics: const AlwaysScrollableScrollPhysics(),334slivers: [335SliverList(336delegate: SliverChildListDelegate([337// Items338]),339),340],341)342```343344## Custom Simulation345346### Creating Custom Simulation347348```dart349class CustomSimulation extends Simulation {350final double target;351352CustomSimulation({required this.target});353354@override355double x(double time) {356// Calculate position at given time357return target * (1 - math.exp(-time / 0.5));358}359360@override361double dx(double time) {362// Calculate velocity (derivative) at given time363return target * math.exp(-time / 0.5) / 0.5;364}365366@override367bool isDone(double time) {368return dx(time).abs() < 0.01;369}370}371```372373**Usage:**374```dart375_controller.animateWith(376CustomSimulation(target: 1.0),377);378```379380### Decay Simulation381382```dart383class DecaySimulation extends Simulation {384final double velocity;385386DecaySimulation({required this.velocity});387388@override389double x(double time) {390return velocity * (1 - math.exp(-time));391}392393@override394double dx(double time) {395return velocity * math.exp(-time);396}397398@override399bool isDone(double time) {400return dx(time).abs() < 0.1;401}402}403```404405## Combining Physics and Animation406407### Spring with Fallback408409```dart410void animateWithSpring({411required AnimationController controller,412required double start,413required double end,414}) {415try {416controller.animateWith(417SpringSimulation(418spring: const SpringDescription(419mass: 1.0,420stiffness: 200.0,421damping: 15.0,422),423start: start,424end: end,425velocity: 0.0,426),427);428} catch (e) {429// Fallback to linear animation430controller.animateTo(end);431}432}433```434435### Spring with Completion Callback436437```dart438_controller.animateWith(439SpringSimulation(440spring: const SpringDescription(441mass: 1.0,442stiffness: 200.0,443damping: 15.0,444),445start: 0.0,446end: 1.0,447velocity: 0.0,448),449);450451_controller.addStatusListener((status) {452if (status == AnimationStatus.completed) {453// Animation complete454}455});456```457458## Physics for Gestures459460### Swipe to Dismiss with Physics461462```dart463class DismissibleWithPhysics extends StatefulWidget {464final Widget child;465final VoidCallback onDismiss;466467const DismissibleWithPhysics({468super.key,469required this.child,470required this.onDismiss,471});472473@override474State<DismissibleWithPhysics> createState() => _DismissibleWithPhysicsState();475}476477class _DismissibleWithPhysicsState extends State<DismissibleWithPhysics>478with SingleTickerProviderStateMixin {479late AnimationController _controller;480double _dragX = 0;481bool _isDismissing = false;482483@override484void initState() {485super.initState();486_controller = AnimationController.unbounded(vsync: this);487}488489@override490void dispose() {491_controller.dispose();492super.dispose();493}494495void _handlePanEnd(DragEndDetails details) {496final velocity = details.velocity.pixelsPerSecond.dx;497final screenWidth = MediaQuery.of(context).size.width;498499if (velocity.abs() > 500 || _dragX.abs() > screenWidth / 3) {500// Dismiss501_controller.fling(velocity: velocity / screenWidth);502_isDismissing = true;503_controller.addStatusListener((status) {504if (status == AnimationStatus.completed && _isDismissing) {505widget.onDismiss();506}507});508} else {509// Spring back510_controller.animateWith(511SpringSimulation(512spring: const SpringDescription(513mass: 1.0,514stiffness: 300.0,515damping: 15.0,516),517start: _dragX,518end: 0,519velocity: velocity,520),521);522}523}524525@override526Widget build(BuildContext context) {527return GestureDetector(528onPanUpdate: (details) {529if (!_controller.isAnimating) {530setState(() {531_dragX += details.delta.dx;532});533}534},535onPanEnd: _handlePanEnd,536child: AnimatedBuilder(537animation: _controller,538builder: (context, child) {539return Transform.translate(540offset: Offset(_dragX + _controller.value, 0),541child: child,542);543},544child: widget.child,545),546);547}548}549```550551## Performance Considerations552553### Simulation Complexity554555Physics simulations can be computationally expensive. Optimize by:556557- Using simpler simulations when possible (e.g., linear interpolation)558- Reducing simulation resolution559- Caching simulation results560- Using built-in physics widgets561562### Spring Tuning563564Finding the right spring parameters can be trial and error:5655661. Start with moderate values (mass: 1.0, stiffness: 200, damping: 15)5672. Adjust for feel:568- Too slow? Increase stiffness or decrease mass569- Too bouncy? Increase damping570- Not bouncy enough? Decrease damping or stiffness5713. Test on real devices572573## Debugging574575### Visualize Physics576577```dart578void main() {579timeDilation = 5.0; // Slow down physics580runApp(MyApp());581}582```583584### Print Simulation Values585586```dart587_controller.addListener(() {588print('Position: ${_controller.value}');589});590591_controller.addStatusListener((status) {592print('Status: $status');593});594```595596### Physics Inspector597598Use Flutter DevTools to profile physics animations:5991. Open Performance overlay6002. Look for frame drops during physics simulations6013. Identify expensive calculations602603## Best Practices604605### DO606607- Use physics simulations for natural-feeling motion608- Tune spring parameters for desired feel609- Test on various devices and screen sizes610- Profile performance with DevTools611- Provide fallback animations for physics failures612613### DON'T614615- Use complex physics when simple curves suffice616- Forget to handle simulation completion617- Use overly bouncy animations (distracting)618- Ignore accessibility (respect disable animations preference)619- Assume physics simulation completes instantly620621## Common Physics Patterns622623### Bouncy Button Press624625```dart626_controller.animateWith(627SpringSimulation(628spring: const SpringDescription(629mass: 0.5,630stiffness: 500,631damping: 10,632),633start: 1.0,634end: 0.9,635velocity: 0,636),637);638```639640### Swipeable Card641642```dart643_controller.fling(644velocity: details.velocity.pixelsPerSecond.dx / 1000,645);646```647648### Pull to Refresh649650```dart651RefreshIndicator(652onRefresh: () async {653// Refresh data654},655child: ListView(...),656)657```658659## Accessibility660661- Respect `MediaQuery.disableAnimations` setting662- Provide non-physics alternatives663- Ensure physics animations don't cause motion sickness664- Test with reduced motion settings665666## Platform-Specific Physics667668### Adaptive Scroll Physics669670```dart671class AdaptiveScrollPhysics extends ScrollPhysics {672const AdaptiveScrollPhysics({super.parent});673674@override675AdaptiveScrollPhysics applyTo(ScrollPhysics? ancestor) {676return AdaptiveScrollPhysics(parent: buildParent(ancestor));677}678679@override680SpringDescription get spring => Platform.isIOS681? const SpringDescription(682mass: 0.5,683stiffness: 100,684damping: 10,685)686: const SpringDescription(687mass: 0.8,688stiffness: 150,689damping: 15,690);691}692```693694**Usage:**695```dart696ListView(697physics: const AdaptiveScrollPhysics(),698children: [...],699)700```701