Tutorial de animaciones
Un tutorial que muestra cómo construir animaciones explícitas en Flutter.
Este tutorial te muestra cómo construir animaciones explícitas en Flutter. Los ejemplos se construyen unos sobre otros, introduciéndote a diferentes aspectos de la biblioteca de animaciones. El tutorial se basa en conceptos, clases y métodos esenciales de la biblioteca de animaciones de los que puedes aprender en Introducción a las animaciones.
El SDK de Flutter también proporciona animaciones explícitas integradas,
como FadeTransition,
SizeTransition,
y SlideTransition. Estas animaciones simples se
activan estableciendo un punto inicial y un punto final.
Son más simples de implementar
que las animaciones explícitas personalizadas, que se describen aquí.
Las siguientes secciones te guían a través de varios ejemplos de animación. Cada sección proporciona un enlace al código fuente de ese ejemplo.
Renderizar animaciones
#Hasta ahora has aprendido cómo generar una secuencia de números a lo largo del tiempo.
Nada se ha renderizado en la pantalla. Para renderizar con un
objeto Animation, almacena el objeto Animation como un
miembro de tu widget, luego usa su valor para decidir cómo dibujar.
Considera la siguiente app que dibuja el logo de Flutter sin animación:
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
class LogoApp extends StatefulWidget {
const LogoApp({super.key});
@override
State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
child: const FlutterLogo(),
),
);
}
}
App source: animate0
A continuación se muestra el mismo código modificado para animar el
logo para que crezca desde cero hasta su tamaño completo.
Al definir un AnimationController, debes pasar un
objeto vsync. El parámetro vsync se describe en la
sección de AnimationController.
Los cambios con respecto al ejemplo no animado están resaltados:
class _LogoAppState extends State<LogoApp> {
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {
// The state that has changed here is the animation object's value.
});
});
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
App source: animate1
La función addListener() llama a setState(),
por lo que cada vez que la Animation genera un nuevo número,
el fotograma actual se marca como dirty, lo que fuerza a que
build() sea llamado de nuevo. En build(),
el contenedor cambia de tamaño porque su altura y
ancho ahora usan animation.value en lugar de un valor hardcodeado.
Desecha el controlador cuando el objeto State sea
descartado para evitar fugas de memoria.
¡Con estos pocos cambios, has creado tu primera animación en Flutter!
Simplificando con AnimatedWidget
#La clase base AnimatedWidget te permite separar el
código del widget principal del código de animación.
AnimatedWidget no necesita mantener un objeto State
para contener la animación. Agrega la siguiente clase AnimatedLogo:
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
AnimatedLogo usa el valor actual de la animation
al dibujarse a sí mismo.
LogoApp todavía gestiona el AnimationController y el Tween,
y pasa el objeto Animation a AnimatedLogo:
void main() => runApp(const LogoApp());
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
class LogoApp extends StatefulWidget {
// ...
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {
// The state that has changed here is the animation object's value.
});
});
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
// ...
}
App source: animate2
Monitorear el progreso de la animación
#A menudo es útil saber cuándo una animación cambia de estado,
como finalizar, avanzarse o invertirse.
Puedes recibir notificaciones de esto con addStatusListener().
El siguiente código modifica el ejemplo anterior para que
escuche un cambio de estado e imprima una actualización.
La línea resaltada muestra el cambio:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) => print('$status'));
controller.forward();
}
// ...
}
Ejecutar este código produce esta salida:
AnimationStatus.forward
AnimationStatus.completed
A continuación, usa addStatusListener() para invertir la animación
al principio o al final. Esto crea un efecto de "respiración":
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
})
..addStatusListener((status) => print('$status'));
controller.forward();
}
App source: animate3
Refactorizar con AnimatedBuilder
#Un problema con el código del ejemplo animate3, es que cambiar la animación requería cambiar el widget que renderiza el logo. Una mejor solución es separar las responsabilidades en diferentes clases:
- Renderizar el logo
- Definir el objeto
Animation - Renderizar la transición
Puedes lograr esta separación con la ayuda de la clase
AnimatedBuilder. Un AnimatedBuilder es una
clase independiente en el árbol de renderizado. Al igual que AnimatedWidget,
AnimatedBuilder escucha automáticamente las notificaciones
del objeto Animation, y marca el árbol de widgets como
dirty según sea necesario, por lo que no necesitas llamar a addListener().
El árbol de widgets para el ejemplo animate4 se ve así:

Comenzando desde la parte inferior del árbol de widgets, el código para renderizar el logo es sencillo:
class LogoWidget extends StatelessWidget {
const LogoWidget({super.key});
// Leave out the height and width so it fills the animating parent.
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10),
child: const FlutterLogo(),
);
}
}
Los tres bloques centrales del diagrama se crean en el
método build() en GrowTransition, mostrado a continuación.
El widget GrowTransition en sí es stateless y contiene
el conjunto de variables finales necesarias para definir la animación de transición.
La función build() crea y devuelve el AnimatedBuilder,
que toma el método (builder anónimo) y el
objeto LogoWidget como parámetros. El trabajo de renderizar la
transición realmente ocurre en el método (builder anónimo),
que crea un Container del tamaño adecuado
para forzar al LogoWidget a encogerse para ajustarse.
Un punto complicado en el código siguiente es que el child parece
estar especificado dos veces. Lo que está sucediendo es que la
referencia externa de child se pasa a AnimatedBuilder,
que la pasa a la clausura anónima, que luego usa
ese objeto como su child. El resultado neto es que el
AnimatedBuilder se inserta entre los dos widgets
en el árbol de renderizado.
class GrowTransition extends StatelessWidget {
const GrowTransition({
required this.child,
required this.animation,
super.key,
});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
Finalmente, el código para inicializar la animación se ve muy
similar al ejemplo animate2. El método initState()
crea un AnimationController y un Tween,
y luego los vincula con animate(). La magia sucede en
el método build(), que devuelve un objeto GrowTransition
con un LogoWidget como child, y un objeto de animación para
impulsar la transición. Estos son los tres elementos enumerados
en las viñetas anteriores.
void main() => runApp(const LogoApp());
class LogoWidget extends StatelessWidget {
const LogoWidget({super.key});
// Leave out the height and width so it fills the animating parent.
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10),
child: const FlutterLogo(),
);
}
}
class GrowTransition extends StatelessWidget {
const GrowTransition({
required this.child,
required this.animation,
super.key,
});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
class LogoApp extends StatefulWidget {
// ...
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
Widget build(BuildContext context) {
return GrowTransition(
animation: animation,
child: const LogoWidget(),
);
}
// ...
}
App source: animate4
Animaciones simultáneas
#En esta sección, te basarás en el ejemplo de
monitorear el progreso de la animación
(animate3), que usó
AnimatedWidget
para animarse hacia adentro y hacia afuera continuamente. Considera el caso
en el que deseas animar hacia adentro y hacia afuera mientras la
opacidad se anima de transparente a opaca.
Cada tween gestiona un aspecto de la animación. Por ejemplo:
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);
Puedes obtener el tamaño con sizeAnimation.value y la opacidad
con opacityAnimation.value, pero el constructor de AnimatedWidget
solo toma un único objeto Animation. Para resolver este problema,
el ejemplo crea sus propios objetos Tween y calcula explícitamente los
valores.
Cambia AnimatedLogo para encapsular sus propios objetos Tween,
y su método build() llama a Tween.evaluate()
en el objeto de animación del padre para calcular
los valores de tamaño y opacidad requeridos.
El siguiente código muestra los cambios resaltados:
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
// Make the Tweens static because they don't change.
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: const FlutterLogo(),
),
),
);
}
}
class LogoApp extends StatefulWidget {
const LogoApp({super.key});
@override
State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
Código fuente de la app: El objeto animate5 conoce el estado actual de una animación (por ejemplo, si se ha iniciado, detenido, o si se mueve hacia adelante o en reversa), pero no sabe nada sobre lo que aparece en pantalla.
- Un
AnimationControllergestiona laAnimation. - Una
CurvedAnimationdefine la progresión como una curva no lineal. - Un
Tweeninterpola entre un valor inicial y final para una propiedad que se está animando.
Próximos pasos
#Este tutorial te da una base para crear animaciones en
Flutter usando Tweens, pero hay muchas otras clases para explorar.
Podrías investigar las clases Tween especializadas,
animaciones específicas para tu tipo de sistema de diseño, ReverseAnimation,
transiciones de elementos compartidos (también conocidas como animaciones Hero),
simulaciones de física y métodos fling().
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.