Saltar al contenido principal

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:

  1. Configurar un controlador de animación
  2. Mover el Widget usando gestos
  3. Animar el Widget
  4. 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:

dart
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.

dart
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:

dart
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:

dart
@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.

dart
class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Alignment> _animation;
  Alignment _dragAlignment = Alignment.center;
dart
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:

dart
@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:

dart
child: Align(
  alignment: _dragAlignment,
  child: Card(child: widget.child),
),

Finalmente, actualiza el GestureDetector para gestionar el controlador de animación:

dart
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:

dart
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:

dart
/// 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:

dart
onPanEnd: (details) {
  _runAnimation(details.velocity.pixelsPerSecond, size);
},

Ejemplo interactivo

#