Animar un Widget usando una simulación física
Cómo implementar una animación física.
Las simulaciones físicas pueden hacer que las interacciones de la aplicación se sientan realistas e interactivas. Por ejemplo, es posible que desees animar un Widget para que actúe como si estuviera sujeto a un resorte o cayendo por gravedad.
Esta receta demuestra cómo mover un Widget desde un punto arrastrado de vuelta al centro usando una simulación de resorte.
Esta receta utiliza estos pasos:
- Configurar un controlador de animación
- Mover el Widget usando gestos
- Animar el Widget
- Calcular la velocidad para simular un movimiento de resorte
Paso 1: Configurar un controlador de animación
#Comienza con un Widget con estado (stateful widget) llamado DraggableCard:
import 'package:flutter/material.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)),
);
}
}
class DraggableCard extends StatefulWidget {
const DraggableCard({required this.child, super.key});
final Widget child;
@override
State<DraggableCard> createState() => _DraggableCardState();
}
class _DraggableCardState extends State<DraggableCard> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Align(child: Card(child: widget.child));
}
}
Haz que la clase _DraggableCardState extienda de
SingleTickerProviderStateMixin.
Luego construye un AnimationController
en
initState y establece vsync como this.
class _DraggableCardState extends State<DraggableCard> {
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Paso 2: Mover el Widget usando gestos
#Haz que el Widget se mueva cuando se arrastre, y añade un campo Alignment
a la clase
_DraggableCardState:
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Alignment _dragAlignment = Alignment.center;
Añade un GestureDetector
que maneje las retrollamadas onPanDown, onPanUpdate y
onPanEnd. Para ajustar la alineación, usa un MediaQuery
para obtener el
tamaño del Widget y divídelo por 2. (Esto convierte las unidades de "píxeles arrastrados" en
coordenadas que utiliza Align). Luego, establece la alineación (alignment) del Widget Align en
_dragAlignment:
@override
Widget build(BuildContext context) {
return Align(
child: Card(
child: widget.child,
var size = MediaQuery.of(context).size;
return GestureDetector(
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,
),
),
);
}
Paso 3: Animar el Widget
#Cuando se suelta el Widget, debería volver al centro como un resorte.
Añade un campo Animation<Alignment> y un método _runAnimation. Este
método define un Tween que interpola entre el punto al que se arrastró el Widget
y el punto en el centro.
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Alignment> _animation;
Alignment _dragAlignment = Alignment.center;
void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(begin: _dragAlignment, end: Alignment.center),
);
_controller.reset();
_controller.forward();
}
A continuación, actualiza _dragAlignment cuando el AnimationController produzca un
valor:
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
Luego, haz que el Widget Align use el campo _dragAlignment:
child: Align(
alignment: _dragAlignment,
child: Card(child: widget.child),
),
Finalmente, actualiza el GestureDetector para gestionar el controlador de animación:
return GestureDetector(
onPanDown: (details) {},
onPanDown: (details) {
_controller.stop();
},
onPanUpdate: (details) {
// ...
},
onPanEnd: (details) {},
onPanEnd: (details) {
_runAnimation();
},
child: Align(
Paso 4: Calcular la velocidad para simular un movimiento de resorte
#El último paso consiste en hacer un poco de matemáticas para calcular la velocidad del Widget
después de que se haya terminado de arrastrar. Esto es para que el Widget continúe de forma realista
a esa velocidad antes de volver a su posición original. (El método _runAnimation
ya establece la dirección configurando la alineación de inicio y fin de la animación).
Primero, importa el paquete physics:
import 'package:flutter/physics.dart';
La retrollamada onPanEnd proporciona un objeto DragEndDetails. Este objeto
proporciona la velocidad del puntero cuando dejó de estar en contacto con la pantalla. La
velocidad está en píxeles por segundo, pero el Widget Align no utiliza píxeles. Utiliza
valores de coordenadas entre [-1.0, -1.0] y [1.0, 1.0], donde [0.0, 0.0]
representa el centro. El tamaño (size) calculado en el paso 2 se utiliza para convertir píxeles
en valores de coordenadas en este rango.
Finalmente, AnimationController tiene un método animateWith() al que se le puede pasar una
SpringSimulation:
/// 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);
}
No olvides llamar a _runAnimation() con la velocidad y el tamaño:
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},
Ejemplo interactivo
#A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.