[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-animazioni-fisiche-in-flutter-spring-attrito-e-simulazioni-realistiche":3,"comments-article-animazioni-fisiche-in-flutter-spring-attrito-e-simulazioni-realistiche":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},139,"Animazioni fisiche in Flutter: spring, attrito e simulazioni realistiche","animazioni-fisiche-in-flutter-spring-attrito-e-simulazioni-realistiche","Le curve predefinite non bastano quando l'utente trascina, lancia o rilascia un elemento. Scopriamo le Simulation di Flutter — SpringSimulation, FrictionSimulation, GravitySimulation — per creare animazioni che reagiscono alla velocità del gesto.","## Perché le curve non bastano\n\nLa maggior parte delle animazioni in Flutter si costruisce con una `Duration` e una `Curve`: `easeInOut`, `fastOutSlowIn`, `elasticOut`. Funziona benissimo quando l'animazione parte da uno stato noto e arriva a un altro stato noto, senza che l'utente interagisca durante il movimento.\n\nIl discorso cambia quando l'animazione nasce da un gesto. Se trascino una card e la rilascio con un colpo secco, mi aspetto che continui a muoversi e poi rallenti; se la rilascio piano, mi aspetto che torni al suo posto con un movimento morbido. Una curva a durata fissa non ha modo di sapere *quanto velocemente* stava andando il dito nell'istante del rilascio: il risultato è quella sensazione di \"animazione finta\" che distingue subito un'app curata da una abbozzata.\n\nLa risposta di Flutter si chiama `Simulation`, una classe del package `dart:ui`\u002F`physics` che descrive la posizione di un oggetto **in funzione del tempo**, partendo da una posizione e da una velocità iniziali.\n\n## L'API Simulation in due minuti\n\nUna `Simulation` espone tre metodi fondamentali:\n\n```dart\nabstract class Simulation {\n  double x(double time);        \u002F\u002F posizione all'istante time (in secondi)\n  double dx(double time);       \u002F\u002F velocità all'istante time\n  bool isDone(double time);     \u002F\u002F la simulazione è terminata?\n}\n```\n\nL'`AnimationController` sa consumarla con `animateWith()`: invece di interpolare da 0 a 1 in N millisecondi, a ogni frame chiede alla simulazione \"dove ti trovi adesso?\" e si ferma quando `isDone` restituisce `true`.\n\nFlutter include già le simulazioni più utili:\n\n- **`SpringSimulation`** — una molla smorzata: perfetta per \"torna al suo posto\", snap, rimbalzi.\n- **`FrictionSimulation`** — decelerazione esponenziale: il classico *fling*\u002Finerzia.\n- **`GravitySimulation`** — accelerazione costante verso un bordo: caduta libera.\n- **`ClampingScrollSimulation`** e **`BouncingScrollSimulation`** — quelle usate internamente dallo scroll Android e iOS.\n\nUn dettaglio importante: quando usi `animateWith` conviene quasi sempre un controller **unbounded**, altrimenti i valori vengono clampati tra `lowerBound` e `upperBound` e una molla che oltrepassa il target perde l'effetto di rimbalzo.\n\n```dart\nlate final AnimationController _controller =\n    AnimationController.unbounded(vsync: this);\n```\n\n## Molle: SpringDescription e SpringSimulation\n\nUna molla in Flutter si descrive con tre parametri:\n\n```dart\nconst spring = SpringDescription(\n  mass: 1,        \u002F\u002F massa dell'oggetto: più alta = movimento più \"pesante\"\n  stiffness: 180, \u002F\u002F rigidità: più alta = ritorno più rapido\n  damping: 20,    \u002F\u002F smorzamento: più alto = meno oscillazioni\n);\n```\n\nRagionare direttamente su `damping` è poco intuitivo. Molto più pratico è il costruttore che lavora sul **damping ratio**:\n\n```dart\nfinal spring = SpringDescription.withDampingRatio(\n  mass: 1,\n  stiffness: 200,\n  ratio: 0.7, \u002F\u002F \u003C 1 sotto-smorzata (rimbalza), 1 critica, > 1 sovra-smorzata\n);\n```\n\n- `ratio: 1.0` → la molla arriva al target nel minor tempo possibile **senza** superarlo. È la scelta giusta per snap sobri (bottom sheet, snap di una colonna).\n- `ratio: 0.6–0.8` → un piccolo overshoot, percepito come \"vivo\". Ottimo per card, like, badge.\n- `ratio: 0.3` → molto elastico. Usalo con parsimonia: su schermate dense stanca.\n\nNelle versioni recenti di Flutter esiste anche `SpringDescription.withDurationAndBounce`, che permette di descrivere la molla in termini di durata percepita e quantità di rimbalzo (lo stesso modello mentale delle spring di SwiftUI). Verifica che sia disponibile nella versione dell'SDK che stai usando prima di adottarlo.\n\n## Esempio completo: card trascinabile che torna al suo posto\n\nQuesto è il caso d'uso canonico. L'utente trascina la card, e al rilascio la molla la riporta al centro tenendo conto della velocità del gesto.\n\n```dart\nclass SpringCard extends StatefulWidget {\n  const SpringCard({super.key});\n\n  @override\n  State\u003CSpringCard> createState() => _SpringCardState();\n}\n\nclass _SpringCardState extends State\u003CSpringCard>\n    with SingleTickerProviderStateMixin {\n  late final AnimationController _controller =\n      AnimationController.unbounded(vsync: this);\n\n  Alignment _alignment = Alignment.center;\n  Animation\u003CAlignment> _animation = const AlwaysStoppedAnimation(Alignment.center);\n\n  @override\n  void initState() {\n    super.initState();\n    _controller.addListener(() {\n      setState(() => _alignment = _animation.value);\n    });\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  void _runSpring(Offset pixelsPerSecond, Size size) {\n    _animation = _controller.drive(\n      AlignmentTween(begin: _alignment, end: Alignment.center),\n    );\n\n    \u002F\u002F Normalizziamo la velocità del gesto nello spazio 0..1 della simulazione.\n    final unitsPerSecond = Offset(\n      pixelsPerSecond.dx \u002F size.width,\n      pixelsPerSecond.dy \u002F size.height,\n    );\n    final unitVelocity = unitsPerSecond.distance;\n\n    final spring = SpringDescription.withDampingRatio(\n      mass: 1,\n      stiffness: 220,\n      ratio: 0.75,\n    );\n\n    \u002F\u002F Da 1 (posizione attuale) a 0 (target), con velocità iniziale negativa.\n    final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);\n    _controller.animateWith(simulation);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final size = MediaQuery.sizeOf(context);\n\n    return GestureDetector(\n      onPanDown: (_) => _controller.stop(),\n      onPanUpdate: (details) {\n        setState(() {\n          _alignment += Alignment(\n            details.delta.dx \u002F (size.width \u002F 2),\n            details.delta.dy \u002F (size.height \u002F 2),\n          );\n        });\n      },\n      onPanEnd: (details) => _runSpring(details.velocity.pixelsPerSecond, size),\n      child: Align(\n        alignment: _alignment,\n        child: const Card(\n          child: SizedBox(width: 160, height: 160, child: Center(child: Text('Trascinami'))),\n        ),\n      ),\n    );\n  }\n}\n```\n\nTre dettagli che fanno la differenza:\n\n1. **`onPanDown: (_) => _controller.stop()`** — se l'utente riafferra la card mentre sta tornando indietro, l'animazione deve cedere immediatamente il controllo. È la regola d'oro delle UI \"interrompibili\".\n2. La velocità viene **normalizzata** nello stesso spazio della simulazione (0..1), altrimenti i numeri non hanno senso fisico.\n3. Il segno della velocità è negativo perché ci muoviamo da 1 verso 0.\n\n## Fling e inerzia con FrictionSimulation\n\nQuando l'elemento non deve tornare indietro ma continuare per inerzia, la simulazione giusta è `FrictionSimulation`:\n\n```dart\nvoid _fling(double velocityPx) {\n  final simulation = FrictionSimulation(\n    0.135,        \u002F\u002F coefficiente di attrito: più basso = scivola più a lungo\n    _position,    \u002F\u002F posizione iniziale in pixel\n    velocityPx,   \u002F\u002F velocità iniziale in pixel\u002Fsecondo\n  );\n  _controller.animateWith(simulation);\n}\n```\n\nSe hai bisogno che il fling si fermi esattamente su un punto (ad esempio lo snap di un carosello), esiste il costruttore `FrictionSimulation.through(startPosition, endPosition, startVelocity, endVelocity)`, che calcola l'attrito necessario per passare per quel punto.\n\nPer il caso semplice, l'`AnimationController` offre già una scorciatoia:\n\n```dart\n_controller.fling(velocity: 2.0); \u002F\u002F usa una spring di default verso 0 o 1\n```\n\n`fling()` è perfetto per toggle e pannelli a due stati (aperto\u002Fchiuso): decide la direzione in base al segno della velocità e anima con una molla predefinita.\n\n## Caduta e rimbalzo con GravitySimulation\n\n`GravitySimulation(acceleration, startPosition, endPosition, startVelocity)` modella un corpo che accelera costantemente fino a una posizione finale. Combinandola in sequenza con sé stessa si ottiene un rimbalzo credibile:\n\n```dart\nvoid _drop(double fromY, double floorY, double velocity) {\n  final sim = GravitySimulation(2000, fromY, floorY, velocity);\n  _controller.animateWith(sim).whenComplete(() {\n    final impact = _controller.velocity.abs() * 0.55; \u002F\u002F coefficiente di restituzione\n    if (impact \u003C 50) return; \u002F\u002F sotto soglia ci fermiamo\n    _controller.animateWith(\n      GravitySimulation(2000, floorY, floorY, -impact),\n    );\n  });\n}\n```\n\nÈ il tipo di effetto che funziona bene per micro-interazioni giocose (gamification, badge conquistati), meno per UI di produttività.\n\n## Scrivere una Simulation personalizzata\n\nQuando nessuna delle simulazioni incluse descrive il comportamento che vuoi, implementarne una è sorprendentemente semplice: basta fornire la formula del moto.\n\n```dart\nclass PulseSimulation extends Simulation {\n  PulseSimulation({this.amplitude = 0.08, this.frequency = 3, this.decay = 2.5});\n\n  final double amplitude;\n  final double frequency;\n  final double decay;\n\n  @override\n  double x(double time) =>\n      1 + amplitude * math.sin(frequency * 2 * math.pi * time) * math.exp(-decay * time);\n\n  @override\n  double dx(double time) => (x(time + 0.001) - x(time)) \u002F 0.001;\n\n  @override\n  bool isDone(double time) => time > 1.5;\n}\n```\n\nIl valore può essere usato direttamente come fattore di scala:\n\n```dart\nAnimatedBuilder(\n  animation: _controller,\n  builder: (context, child) =>\n      Transform.scale(scale: _controller.value, child: child),\n  child: const Icon(Icons.favorite, size: 48),\n)\n```\n\nNota il `child` passato fuori dalla `builder`: il sottoalbero non viene ricostruito a ogni frame, si ricostruisce solo il `Transform`. È la regola base per animazioni che non fanno scendere gli fps.\n\n## Dove si incastra lo scroll\n\nLo stesso meccanismo governa lo scrolling: una `ScrollPhysics` non fa altro che restituire una `Simulation` dal metodo `createBallisticSimulation`. Se ti serve uno scroll con inerzia personalizzata (per esempio uno snap magnetico), puoi estendere `ScrollPhysics` e restituire una `SpringSimulation` verso l'elemento più vicino:\n\n```dart\nclass SnapPhysics extends ScrollPhysics {\n  const SnapPhysics({super.parent, required this.itemExtent});\n\n  final double itemExtent;\n\n  @override\n  SnapPhysics applyTo(ScrollPhysics? ancestor) =>\n      SnapPhysics(parent: buildParent(ancestor), itemExtent: itemExtent);\n\n  @override\n  Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {\n    final target = (position.pixels \u002F itemExtent).roundToDouble() * itemExtent;\n    if ((target - position.pixels).abs() \u003C tolerance.distance) return null;\n    return ScrollSpringSimulation(spring, position.pixels, target, velocity,\n        tolerance: tolerance);\n  }\n}\n```\n\n## Buone pratiche\n\n- **Tolleranza**: una molla matematicamente non si ferma mai. `Tolerance` definisce quando considerarla conclusa; nelle simulazioni di scroll passa sempre la `tolerance` fornita dalla physics, altrimenti rischi animazioni che non terminano.\n- **Attenzione a `pumpAndSettle`**: nei widget test, un controller alimentato da una simulazione che non termina manda in timeout il test. Verifica `isDone` o usa `pump` con durate esplicite.\n- **Rispetta le preferenze di accessibilità**: se `MediaQuery.disableAnimationsOf(context)` è `true`, salta la simulazione e applica direttamente lo stato finale.\n- **Un solo `Ticker` quando possibile**: `SingleTickerProviderStateMixin` è più leggero di `TickerProviderStateMixin`, e ricordati sempre di fare `dispose()` del controller.\n- **Non esagerare con i parametri**: molle troppo elastiche su elementi grandi danno una sensazione di instabilità. Per le UI \"serie\" resta su `ratio` tra 0.8 e 1.\n\n## Conclusione\n\nLe simulazioni fisiche sono uno degli angoli meno frequentati di Flutter, eppure sono ciò che separa un'interfaccia che *risponde* da una che semplicemente *si anima*. Il trucco concettuale è semplice: smettere di pensare in termini di \"quanto dura\" e iniziare a pensare in termini di \"con che velocità entra nel movimento\". Parti dalla `SpringSimulation` per gli snap, aggiungi `FrictionSimulation` per i fling, e tieni presente che `AnimationController.animateWith` accetta qualsiasi classe tu voglia scrivere: da lì in poi il limite è la tua fantasia (e un po' di buon senso sul design).","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F6eb098a1-fa1b-427b-a1ca-3ead57557053.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1685906357233-42a7699acc28?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTI5ODczMDd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"HI! ESTUDIO","https:\u002F\u002Funsplash.com\u002F@hiestudio","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-blue-ball-sitting-on-top-of-a-red-box-SXpFZ6plYzM",null,"published","2026-10-26T04:01:48+00:00","Animazioni fisiche in Flutter: spring e simulazioni","Guida pratica alle Simulation di Flutter: SpringSimulation, FrictionSimulation e GravitySimulation per animazioni che reagiscono alla velocità del gesto.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1793098102509]