1. Introducción
Las animaciones son una excelente manera de mejorar la experiencia del usuario de tu app, comunicar información importante al usuario y hacer que tu app sea más pulida y agradable de usar.
Descripción general del framework de animación de Flutter
Flutter muestra efectos de animación reconstruyendo una parte del árbol de widgets en cada fotograma. Proporciona efectos de animación prediseñados y otras APIs para facilitar la creación y composición de animaciones.
- Las animaciones implícitas son efectos de animación prediseñados que ejecutan toda la animación automáticamente. Cuando cambia el valor target de la animación, se ejecuta la animación desde el valor actual hasta el valor objetivo y se muestra cada valor intermedio para que el widget se anime sin problemas. Algunos ejemplos de animaciones implícitas son
AnimatedSize,AnimatedScaleyAnimatedPositioned. - Las animaciones explícitas también son efectos de animación prediseñados, pero requieren un objeto
Animationpara funcionar. Los ejemplos incluyenSizeTransition,ScaleTransitionoPositionedTransition. - Animation es una clase que representa una animación en ejecución o detenida, y se compone de un valor que representa el valor objetivo al que se dirige la animación y el estado, que representa el valor actual que la animación muestra en la pantalla en cualquier momento. Es una subclase de
Listenabley notifica a sus listeners cuando cambia el estado mientras se ejecuta la animación. - AnimationController es una forma de crear una animación y controlar su estado. Sus métodos, como
forward(),reset(),stop()yrepeat(), se pueden usar para controlar la animación sin necesidad de definir el efecto de animación que se muestra, como la escala, el tamaño o la posición. - Los tweens se usan para interpolar valores entre un valor inicial y un valor final, y pueden representar cualquier tipo, como un valor doble,
OffsetoColor. - Las curvas se usan para ajustar la tasa de cambio de un parámetro a lo largo del tiempo. Cuando se ejecuta una animación, es común aplicar una curva de aceleración para que la tasa de cambio sea más rápida o más lenta al principio o al final de la animación. Las curvas toman un valor de entrada entre 0.0 y 1.0, y devuelven un valor de salida entre 0.0 y 1.0.
Qué compilarás
En este codelab, compilarás un juego de preguntas de opción múltiple que incluye varios efectos y técnicas de animación.

Verás cómo hacer lo siguiente:
- Crea un widget que anime su tamaño y color
- Crea un efecto de giro de tarjeta en 3D
- Usar efectos de animación prediseñados sofisticados del paquete de animaciones
- Se agregó compatibilidad con el gesto atrás predictivo disponible en la versión más reciente de Android.
Qué aprenderás
En este codelab, aprenderás lo siguiente:
- Cómo usar los efectos animados de forma implícita para lograr animaciones atractivas sin necesidad de escribir mucho código
- Cómo usar efectos animados de forma explícita para configurar tus propios efectos con widgets animados prediseñados, como
AnimatedSwitchero unAnimationController - Cómo usar
AnimationControllerpara definir tu propio widget que muestre un efecto 3D - Cómo usar el paquete
animationspara mostrar efectos de animación sofisticados con una configuración mínima
Requisitos
- El SDK de Flutter
- Un IDE, como VSCode o Android Studio / IntelliJ
2. Configura tu entorno de desarrollo de Flutter
Para completar este lab, necesitas dos tipos de software: el SDK de Flutter y un editor.
Puedes ejecutar el codelab con cualquiera de estos dispositivos o modalidades:
- Un dispositivo físico Android (recomendado para implementar el gesto de atrás predictivo en el paso 7) o iOS conectado a tu computadora y configurado en el modo de desarrollador
- El simulador de iOS (requiere la instalación de herramientas de Xcode).
- Android Emulator (requiere configuración en Android Studio).
- Un navegador (se requiere Chrome para la depuración)
- Una computadora de escritorio con Windows, Linux o macOS Debes desarrollar contenido en la plataforma donde tengas pensado realizar la implementación. por lo tanto, si quieres desarrollar una app de escritorio para Windows, debes desarrollarla en ese SO a fin de obtener acceso a la cadena de compilación correcta; En docs.flutter.dev/desktop, puede encontrar detalles sobre los requisitos específicos del sistema operativo.
Verifica tu instalación
Para verificar que tu SDK de Flutter esté configurado correctamente y que tengas instalada al menos una de las plataformas de destino anteriores, usa la herramienta Flutter Doctor:
$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale
en)
[✓] Android toolchain - develop for Android devices
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] IntelliJ IDEA Ultimate Edition
[✓] VS Code
[✓] Connected device (4 available)
[✓] Network resources
• No issues found!
3. Ejecuta la app de partida
Descarga la app de inicio
Usa git para clonar la app de inicio desde el repositorio flutter/samples en GitHub.
git clone https://github.com/flutter/codelabs.git cd codelabs/animations/step_01/
También tienes la opción de descargar el código fuente como archivo ZIP.
Ejecuta la app
Para ejecutar la app, usa el comando flutter run y especifica un dispositivo de destino, como android, ios o chrome. Para obtener la lista completa de las plataformas compatibles, consulta la página Plataformas compatibles.
flutter run -d android
También puedes ejecutar y depurar la app con el IDE que prefieras. Consulta la documentación oficial de Flutter para obtener más información.
Explora el código
La app de partida es un juego de preguntas de opción múltiple que consta de dos pantallas que siguen el patrón de diseño de modelo-vista-vista-modelo, o MVVM. La QuestionScreen (vista) usa la clase QuizViewModel (modelo de vistas) para hacerle al usuario preguntas de opción múltiple de la clase QuestionBank (modelo).
- home_screen.dart: Muestra una pantalla con un botón New Game.
- main.dart: Configura
MaterialApppara usar Material 3 y mostrar la pantalla principal. - model.dart: Define las clases principales que se usan en toda la app.
- question_screen.dart: Muestra la IU del juego de preguntas.
- view_model.dart: Almacena el estado y la lógica del juego de preguntas, que muestra el
QuestionScreen.

Por el momento, la app no admite ningún efecto animado, excepto la transición de vista predeterminada que muestra la clase Navigator de Flutter cuando el usuario presiona el botón New Game.
4. Cómo usar efectos de animación implícitos
Las animaciones implícitas son una excelente opción en muchas situaciones, ya que no requieren ninguna configuración especial. En esta sección, actualizarás el widget StatusBar para que muestre un marcador animado. Para encontrar efectos de animación implícita comunes, explora la documentación de la API de ImplicitlyAnimatedWidget.

Crea el widget del marcador sin animación
Crea un archivo nuevo, lib/scoreboard.dart, con el siguiente código:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color: score < i + 1
? Colors.grey.shade400
: Colors.yellow.shade700,
),
],
),
);
}
}
Luego, agrega el widget Scoreboard en los elementos secundarios del widget StatusBar, reemplazando los widgets Text que antes mostraban la puntuación y el recuento total de preguntas. El editor debería agregar automáticamente el import "scoreboard.dart" requerido en la parte superior del archivo.
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
Este widget muestra un ícono de estrella para cada pregunta. Cuando se responde correctamente una pregunta, se enciende otra estrella al instante sin ninguna animación. En los siguientes pasos, ayudarás a informar al usuario que su puntuación cambió animando su tamaño y color.
Cómo usar un efecto de animación implícito
Crea un nuevo widget llamado AnimatedStar que use un widget AnimatedScale para cambiar la cantidad de scale de 0.5 a 1.0 cuando la estrella se active:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar(isActive: score > i), // Edit this line.
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
Ahora, cuando el usuario responde una pregunta correctamente, el widget AnimatedStar actualiza su tamaño con una animación implícita. El Icon de color no se anima aquí, solo el scale, que se realiza con el widget AnimatedScale.

Cómo usar un Tween para interpolar entre dos valores
Observa que el color del widget AnimatedStar cambia inmediatamente después de que el campo isActive cambia a verdadero.
Para lograr un efecto de color animado, puedes intentar usar un widget AnimatedContainer (que es otra subclase de ImplicitlyAnimatedWidget), ya que puede animar automáticamente todos sus atributos, incluido el color. Lamentablemente, nuestro widget necesita mostrar un ícono, no un contenedor.
También puedes probar AnimatedIcon, que implementa efectos de transición entre las formas de los íconos. Sin embargo, no hay una implementación predeterminada de un ícono de estrella en la clase AnimatedIcons.
En su lugar, usaremos otra subclase de ImplicitlyAnimatedWidget llamada TweenAnimationBuilder, que toma un Tween como parámetro. Un tween es una clase que toma dos valores (begin y end) y calcula los valores intermedios para que una animación pueda mostrarlos. En este ejemplo, usaremos un ColorTween, que satisface la interfaz Tween necesaria para compilar nuestro efecto de animación.
Selecciona el widget Icon y usa la acción rápida "Wrap with Builder" en tu IDE. Luego, cambia el nombre a TweenAnimationBuilder. Luego, proporciona la duración y ColorTween.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: TweenAnimationBuilder( // Add from here...
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) { // To here.
return Icon(Icons.star, size: 50, color: value); // And modify this line.
},
),
);
}
}
Ahora, recarga la app en caliente para ver la nueva animación.

Ten en cuenta que el valor end de nuestro ColorTween cambia según el valor del parámetro isActive. Esto se debe a que TweenAnimationBuilder vuelve a ejecutar su animación cada vez que cambia el valor de Tween.end. Cuando esto sucede, la nueva animación se ejecuta desde el valor de animación actual hasta el nuevo valor final, lo que te permite cambiar el color en cualquier momento (incluso mientras se ejecuta la animación) y mostrar un efecto de animación fluido con los valores intermedios correctos.
Cómo aplicar una curva
Ambos efectos de animación se ejecutan a una velocidad constante, pero las animaciones suelen ser más interesantes y útiles visualmente cuando se aceleran o ralentizan.
Un Curve aplica una función de aceleración, que define la tasa de cambio de un parámetro a lo largo del tiempo. Flutter incluye una colección de curvas de aceleración prediseñadas en la clase Curves, como easeIn o easeOut.


Estos diagramas (disponibles en la página de documentación de la API de Curves) brindan una idea de cómo funcionan las curvas. Las curvas convierten un valor de entrada entre 0.0 y 1.0 (que se muestra en el eje X) en un valor de salida entre 0.0 y 1.0 (que se muestra en el eje Y). Estos diagramas también muestran una vista previa de cómo se ven los diferentes efectos de animación cuando usan una curva de aceleración.
Crea un campo nuevo en AnimatedStar llamado _curve y pásalo como parámetro a los widgets AnimatedScale y TweenAnimationBuilder.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(Icons.star, size: 50, color: value);
},
),
);
}
}
En este ejemplo, la curva elasticOut proporciona un efecto de resorte exagerado que comienza con un movimiento de resorte y se equilibra hacia el final.

Recarga la app en caliente para ver esta curva aplicada a AnimatedSize y TweenAnimationBuilder.

Cómo usar Herramientas para desarrolladores para habilitar animaciones lentas
Para depurar cualquier efecto de animación, Flutter DevTools proporciona una forma de ralentizar todas las animaciones de tu app, de modo que puedas ver la animación con mayor claridad.
Para abrir DevTools, asegúrate de que la app se esté ejecutando en modo de depuración y abre el Inspector de widgets seleccionándolo en la barra de herramientas de depuración en VSCode o seleccionando el botón Abrir Flutter DevTools en la ventana de herramientas de depuración en IntelliJ o Android Studio.


Una vez que se abra el inspector de widgets, haz clic en el botón Animaciones lentas en la barra de herramientas.

5. Usa efectos de animación explícitos
Al igual que las animaciones implícitas, las animaciones explícitas son efectos de animación prediseñados, pero, en lugar de tomar un valor de destino, toman un objeto Animation como parámetro. Esto las hace útiles en situaciones en las que la animación ya está definida por una transición de navegación, AnimatedSwitcher o AnimationController, por ejemplo.
Usar un efecto de animación explícito
Para comenzar con un efecto de animación explícito, une el widget Card con un AnimatedSwitcher.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
AnimatedSwitcher usa un efecto de fundido cruzado de forma predeterminada, pero puedes anularlo con el parámetro transitionBuilder. El compilador de transición proporciona el widget secundario que se pasó a AnimatedSwitcher y un objeto Animation. Esta es una excelente oportunidad para usar una animación explícita.
En este codelab, la primera animación explícita que usaremos es SlideTransition, que toma un Animation<Offset> que define el desplazamiento inicial y final entre los que se moverán los widgets entrantes y salientes.
Los tweens tienen una función auxiliar, animate(), que convierte cualquier Animation en otro Animation con el tween aplicado. Esto significa que se puede usar un Tween para convertir el Animation proporcionado por el AnimatedSwitcher en un Animation, que se proporcionará al widget SlideTransition.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) { // Add from here...
final curveAnimation = CurveTween(
curve: Curves.easeInCubic,
).animate(animation);
final offsetAnimation = Tween<Offset>(
begin: Offset(-0.1, 0.0),
end: Offset.zero,
).animate(curveAnimation);
return SlideTransition(position: offsetAnimation, child: child);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Ten en cuenta que esto usa Tween.animate para aplicar un Curve al Animation y, luego, convertirlo de un Tween que varía de 0.0 a 1.0 a un Tween que realiza la transición de -0.1 a 0.0 en el eje X.
Como alternativa, la clase Animation tiene una función drive() que toma cualquier Tween (o Animatable) y lo convierte en un nuevo Animation. Esto permite que las interpolaciones se "encadenen", lo que hace que el código resultante sea más conciso:
lib/question_screen.dart
transitionBuilder: (child, animation) {
var offsetAnimation = animation
.drive(CurveTween(curve: Curves.easeInCubic))
.drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
return SlideTransition(position: offsetAnimation, child: child);
},
Otra ventaja de usar animaciones explícitas es que se pueden componer juntas. Agrega otra animación explícita, FadeTransition, que use la misma animación curva envolviendo el widget SlideTransition.
lib/question_screen.dart
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
final curveAnimation = CurveTween(
curve: Curves.easeInCubic,
).animate(animation);
final offsetAnimation = Tween<Offset>(
begin: Offset(-0.1, 0.0),
end: Offset.zero,
).animate(curveAnimation);
final fadeInAnimation = curveAnimation; // NEW
return FadeTransition( // NEW
opacity: fadeInAnimation, // NEW
child: SlideTransition(position: offsetAnimation, child: child), // NEW
); // NEW
},
Personaliza el diseño de layoutBuilder
Es posible que notes un pequeño problema con el AnimationSwitcher. Cuando un QuestionCard cambia a una pregunta nueva, la organiza en el centro del espacio disponible mientras se ejecuta la animación, pero cuando se detiene la animación, el widget se ajusta a la parte superior de la pantalla. Esto provoca una animación inestable porque la posición final de la tarjeta de pregunta no coincide con la posición mientras se ejecuta la animación.

Para solucionar este problema, el AnimatedSwitcher también tiene un parámetro layoutBuilder, que se puede usar para definir el diseño. Usa esta función para configurar el compilador de diseño de modo que alinee la tarjeta con la parte superior de la pantalla:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
Este código es una versión modificada de defaultLayoutBuilder de la clase AnimatedSwitcher, pero usa Alignment.topCenter en lugar de Alignment.center.
Resumen
- Las animaciones explícitas son efectos de animación que toman un objeto
Animation(a diferencia deImplicitlyAnimatedWidgets, que toma un objetivovalueyduration). - La clase
Animationrepresenta una animación en ejecución, pero no define un efecto específico. - Usa
Tween().animateoAnimation.drive()para aplicarTweensyCurves(conCurveTween) a una animación. - Usa el parámetro
layoutBuilderdeAnimatedSwitcherpara ajustar la forma en que organiza sus elementos secundarios.
6. Cómo controlar el estado de una animación
Hasta ahora, el framework ejecutó automáticamente todas las animaciones. Las animaciones implícitas se ejecutan automáticamente, y los efectos de animación explícitos requieren un Animation para funcionar correctamente. En esta sección, aprenderás a crear tus propios objetos Animation con un AnimationController y a usar un TweenSequence para combinar objetos Tween.
Cómo ejecutar una animación con un AnimationController
Para crear una animación con un AnimationController, deberás seguir estos pasos:
- Cómo crear un
StatefulWidget - Usa el mixin
SingleTickerProviderStateMixinen tu claseStatepara proporcionar unTickera tuAnimationController. - Inicializa
AnimationControlleren el método de ciclo de vidainitState, proporcionando el objetoStateactual al parámetrovsync(TickerProvider). - Asegúrate de que tu widget se vuelva a compilar cada vez que
AnimationControllernotifique a sus listeners, ya sea conAnimatedBuildero llamando alisten()ysetStatede forma manual.
Crea un archivo nuevo, flip_effect.dart, y copia y pega el siguiente código:
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.duration,
);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
Esta clase configura un AnimationController y vuelve a ejecutar la animación cada vez que el framework llama a didUpdateWidget para notificarle que la configuración del widget cambió y que podría haber un nuevo widget secundario.
El AnimatedBuilder garantiza que el árbol de widgets se vuelva a compilar cada vez que el AnimationController notifica a sus listeners, y el widget Transform se usa para aplicar un efecto de rotación en 3D para simular que se da vuelta una tarjeta.
Para usar este widget, incluye cada tarjeta de respuesta en un widget CardFlipEffect. Asegúrate de proporcionar un key al widget Card:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
Ahora vuelve a cargar la app en caliente para ver cómo se dan vuelta las tarjetas de respuesta con el widget CardFlipEffect.

Es posible que notes que esta clase se parece mucho a un efecto de animación explícito. De hecho, suele ser una buena idea extender la clase AnimatedWidget directamente para implementar tu propia versión. Lamentablemente, como esta clase necesita almacenar el widget anterior en su State, debe usar un StatefulWidget. Para obtener más información sobre cómo crear tus propios efectos de animación explícitos, consulta la documentación de la API de AnimatedWidget.
Cómo agregar un retraso con TweenSequence
En esta sección, agregarás una demora al widget CardFlipEffect para que cada tarjeta se dé vuelta de a una. Para comenzar, agrega un campo nuevo llamado delayAmount.
lib/flip_effect.dart
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
final double delayAmount; // NEW
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
required this.delayAmount, // NEW
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
Luego, agrega delayAmount al método de compilación AnswerCards.
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
Luego, en _CardFlipEffectState, crea un nuevo Animation que aplique la demora con un TweenSequence. Ten en cuenta que no usa ninguna utilidad de la biblioteca dart:async, como Future.delayed. Esto se debe a que la demora forma parte de la animación y no es algo que el widget controle de forma explícita cuando usa AnimationController. Esto facilita la depuración del efecto de animación cuando se habilitan animaciones lentas en Herramientas para desarrolladores, ya que usa el mismo TickerProvider.
Para usar un TweenSequence, crea dos TweenSequenceItems, uno que contenga un ConstantTween que mantenga la animación en 0 durante una duración relativa y un Tween normal que vaya de 0.0 a 1.0.
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.duration * (widget.delayAmount + 1),
);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // Add from here...
if (widget.delayAmount > 0)
TweenSequenceItem(
tween: ConstantTween<double>(0.0),
weight: widget.delayAmount,
),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
]).animate(_animationController); // To here.
}
Por último, reemplaza la animación de AnimationController por la nueva animación retrasada en el método build.
lib/flip_effect.dart
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationWithDelay, // Modify this line
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationWithDelay.value * math.pi), // And this line
child: _animationController.isAnimating
? _animationWithDelay.value < 0.5 // And this one.
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
Ahora, vuelve a cargar la app en caliente y mira cómo se dan vuelta las tarjetas una por una. Para un desafío, experimenta con cambiar la perspectiva del efecto 3D que proporciona el widget Transform.

7. Cómo usar transiciones de navegación personalizadas
Hasta ahora, vimos cómo personalizar los efectos en una sola pantalla, pero otra forma de usar las animaciones es para hacer la transición entre pantallas. En esta sección, aprenderás a aplicar efectos de animación a las transiciones de pantalla con efectos de animación integrados y efectos de animación prediseñados sofisticados que proporciona el paquete oficial animations en pub.dev.
Cómo animar una transición de navegación
La clase PageRouteBuilder es un Route que te permite personalizar la animación de transición. Te permite anular su devolución de llamada transitionBuilder, que proporciona dos objetos Animation que representan la animación entrante y saliente que ejecuta Navigator.
Para personalizar la animación de transición, reemplaza MaterialPageRoute por PageRouteBuilder y, para personalizar la animación de transición cuando el usuario navega de HomeScreen a QuestionScreen. Usa un FadeTransition (un widget animado de forma explícita) para que la pantalla nueva aparezca con un efecto de atenuación sobre la pantalla anterior.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // Add from here...
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
), // To here.
);
},
child: Text('New Game'),
),
El paquete de animaciones proporciona efectos de animación prediseñados sofisticados, como FadeThroughTransition. Importa el paquete de animaciones y reemplaza FadeTransition por el widget FadeThroughTransition:
lib/home_screen.dart
import 'package;animations/animations.dart';
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition( // Add from here...
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
); // To here.
},
),
);
},
child: Text('New Game'),
),
Cómo personalizar la animación de atrás predictivo

El gesto atrás predictivo es una nueva función de Android que permite al usuario echar un vistazo detrás de la ruta o la app actual para ver qué hay detrás antes de navegar. La animación de vista previa se basa en la ubicación del dedo del usuario mientras lo arrastra hacia atrás por la pantalla.
Flutter admite el gesto atrás predictivo del sistema habilitando la función a nivel del sistema cuando Flutter no tiene rutas para descartar en su pila de navegación o, en otras palabras, cuando un gesto atrás saldría de la app. El sistema controla esta animación, no Flutter.
Flutter también admite el gesto de atrás predictivo cuando se navega entre rutas dentro de una app de Flutter. Un PageTransitionsBuilder especial llamado PredictiveBackPageTransitionsBuilder detecta los gestos de atrás predictivo del sistema y controla la transición de página con el progreso del gesto.
El gesto atrás predictivo solo se admite en Android U y versiones posteriores, pero Flutter recurrirá de forma correcta al comportamiento original del gesto atrás y a ZoomPageTransitionBuilder. Consulta nuestra entrada de blog para obtener más información, incluida una sección sobre cómo configurarlo en tu propia app.
En la configuración de ThemeData de tu app, configura PageTransitionsTheme para que use PredictiveBack en Android y el efecto de transición de desvanecimiento del paquete de animaciones en otras plataformas:
lib/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
Ahora puedes cambiar la devolución de llamada Navigator.push() a MaterialPageRoute.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute( // Add from here...
builder: (context) {
return const QuestionScreen();
},
), // To here.
);
},
child: Text('New Game'),
),
Usa FadeThroughTransition para cambiar la pregunta actual
El widget AnimatedSwitcher solo proporciona un Animation en su devolución de llamada del compilador. Para solucionar este problema, el paquete animations proporciona un PageTransitionSwitcher.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher( // Add from here...
layoutBuilder: (entries) {
return Stack(alignment: Alignment.topCenter, children: entries);
},
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Usa OpenContainer

El widget OpenContainer del paquete animations proporciona un efecto de animación de transformación de contenedores que se expande para crear una conexión visual entre dos widgets.
El widget que devuelve closedBuilder se muestra inicialmente y se expande al widget que devuelve openBuilder cuando se presiona el contenedor o cuando se llama a la devolución de llamada de openContainer.
Para conectar la devolución de llamada openContainer al modelo de vista, agrega un nuevo paso viewModel al widget QuestionCard y almacena una devolución de llamada que se usará para mostrar la pantalla "Game Over":
lib/question_screen.dart
class QuestionScreen extends StatefulWidget {
const QuestionScreen({super.key});
@override
State<QuestionScreen> createState() => _QuestionScreenState();
}
class _QuestionScreenState extends State<QuestionScreen> {
late final QuizViewModel viewModel = QuizViewModel(
onGameOver: _handleGameOver,
);
VoidCallback? _showGameOverScreen; // NEW
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed:
viewModel.hasNextQuestion && viewModel.didAnswerQuestion
? () {
viewModel.getNextQuestion();
}
: null,
child: const Text('Next'),
),
],
),
body: Center(
child: Column(
children: [
QuestionCard( // NEW
onChangeOpenContainer: _handleChangeOpenContainer, // NEW
question: viewModel.currentQuestion?.question, // NEW
viewModel: viewModel, // NEW
), // NEW
Spacer(),
AnswerCards(
onTapped: (index) {
viewModel.checkAnswer(index);
},
answers: viewModel.currentQuestion?.possibleAnswers ?? [],
correctAnswer: viewModel.didAnswerQuestion
? viewModel.currentQuestion?.correctAnswer
: null,
),
StatusBar(viewModel: viewModel),
],
),
),
);
},
);
}
void _handleChangeOpenContainer(VoidCallback openContainer) { // NEW
_showGameOverScreen = openContainer; // NEW
} // NEW
void _handleGameOver() { // NEW
if (_showGameOverScreen != null) { // NEW
_showGameOverScreen!(); // NEW
} // NEW
} // NEW
}
Agrega un widget nuevo, GameOverScreen:
lib/question_screen.dart
class GameOverScreen extends StatelessWidget {
final QuizViewModel viewModel;
const GameOverScreen({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(automaticallyImplyLeading: false),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Scoreboard(
score: viewModel.score,
totalQuestions: viewModel.totalQuestions,
),
Text('You Win!', style: Theme.of(context).textTheme.displayLarge),
Text(
'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
style: Theme.of(context).textTheme.displaySmall,
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
),
);
}
}
En el widget QuestionCard, reemplaza el Card por un widget OpenContainer del paquete animations y agrega dos campos nuevos para la devolución de llamada de viewModel y el contenedor abierto:
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.onChangeOpenContainer,
required this.question,
required this.viewModel,
super.key,
});
final ValueChanged<VoidCallback> onChangeOpenContainer;
final QuizViewModel viewModel;
static const _backgroundColor = Color(0xfff2f3fa);
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: OpenContainer( // NEW
key: ValueKey(question), // NEW
tappable: false, // NEW
closedColor: _backgroundColor, // NEW
closedShape: const RoundedRectangleBorder( // NEW
borderRadius: BorderRadius.all(Radius.circular(12.0)), // NEW
), // NEW
closedElevation: 4, // NEW
closedBuilder: (context, openContainer) { // NEW
onChangeOpenContainer(openContainer); // NEW
return ColoredBox( // NEW
color: _backgroundColor, // NEW
child: Padding( // NEW
padding: const EdgeInsets.all(16.0), // NEW
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
);
},
openBuilder: (context, closeContainer) { // NEW
return GameOverScreen(viewModel: viewModel); // NEW
}, // NEW
),
);
}
}

8. Felicitaciones
¡Felicitaciones! Agregaste correctamente efectos de animación a una app de Flutter y aprendiste sobre los componentes principales del sistema de animación de Flutter. Específicamente, aprendiste lo siguiente:
- Cómo usar un
ImplicitlyAnimatedWidget - Cómo usar un
ExplicitlyAnimatedWidget - Cómo aplicar
CurvesyTweensa una animación - Cómo usar widgets de transición prediseñados, como
AnimatedSwitcheroPageRouteBuilder - Cómo usar efectos de animación prediseñados sofisticados del paquete
animations, comoFadeThroughTransitionyOpenContainer - Cómo personalizar la animación de transición predeterminada, incluida la adición de compatibilidad con el gesto de atrás predictivo en Android

Próximos pasos
Consulta algunos de estos codelabs:
- Cómo compilar un diseño de app responsivo y animado con Material 3
- Cómo compilar transiciones atractivas con el sistema de movimiento de Material para Flutter
- Cómo hacer que tu app de Flutter pase de aburrida a atractiva
También puedes descargar la app de ejemplo de animaciones, que muestra varias técnicas de animación.
Lecturas adicionales
Puedes encontrar más recursos de animación en flutter.dev:
- Introducción a las animaciones
- Instructivo sobre animaciones (instructivo)
- Animaciones implícitas (tutorial)
- Cómo animar las propiedades de un contenedor (recetario)
- Cómo hacer que un widget aparezca y desaparezca gradualmente (recetario)
- Animaciones de héroes
- Cómo animar una transición de ruta de página (recetario)
- Cómo animar un widget con una simulación física (recetario)
- Animaciones escalonadas
- Widgets de animación y movimiento (catálogo de widgets)
También puedes consultar estos artículos en Medium:
- Análisis detallado de la animación
- Animaciones implícitas personalizadas en Flutter
- Administración de animaciones con Flutter y Flux / Redux
- How to Choose Which Flutter Animation Widget is Right for You? (Cómo elegir el widget de animación de Flutter adecuado para ti)
- Animaciones direccionales con animaciones explícitas integradas
- Conceptos básicos de la animación en Flutter con animaciones implícitas
- ¿Cuándo debo usar AnimatedBuilder o AnimatedWidget?