1. Introducción
Las animaciones son una excelente manera de mejorar la experiencia del usuario de tu app, comunicarle información importante y hacer que tu app sea más refinada y agradable de usar.
Descripción general del framework de animación de Flutter
Flutter muestra efectos de animación recompilando 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 precompilados que ejecutan toda la animación automáticamente. Cuando cambia el valor objetivo de la animación, esta se ejecuta desde el valor actual hasta el valor objetivo y muestra cada valor intermedio para que el widget se anime de forma fluida. Algunos ejemplos de animaciones implícitas son
AnimatedSize
,AnimatedScale
yAnimatedPositioned
. - Las animaciones explícitas también son efectos de animación precompilados, pero requieren un objeto
Animation
para funcionar. Los ejemplos incluyenSizeTransition
,ScaleTransition
oPositionedTransition
. - 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 ejecuta la animación y el estado, que representa el valor actual que la animación muestra en la pantalla en un momento determinado. Es una subclase de
Listenable
y notifica a sus objetos de escucha 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 transiciones se usan para interpolar valores entre un valor inicial y uno final, y pueden representar cualquier tipo, como un número doble,
Offset
oColor
. - 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 muestran un valor de salida entre 0.0 y 1.0.
Qué compilarás
En este codelab, crearás un juego de cuestionario de opción múltiple que incluya varios efectos y técnicas de animación.
Verás cómo hacer lo siguiente:
- Compila un widget que anime su tamaño y color
- Crea un efecto de giro de tarjeta 3D
- Usa efectos de animación prediseñados y elegantes del paquete de animaciones
- Agrega 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 efectos animados de forma implícita para lograr animaciones de aspecto atractivo sin necesidad de escribir mucho código
- Cómo usar efectos animados de forma explícita para configurar tus propios efectos con widgets animados precompilados, como
AnimatedSwitcher
oAnimationController
- Cómo usar
AnimationController
para definir tu propio widget que muestre un efecto 3D - Cómo usar el paquete
animations
para 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 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; encuentra detalles sobre los requisitos específicos del sistema operativo en docs.flutter.dev/desktop).
Cómo verificar la instalación
Para verificar que el 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 partida
Usa git
para clonar la app de partida del repositorio flutter/samples en GitHub.
$ git clone https://github.com/flutter/codelabs.git $ cd codelabs/animations/step_01/
También puedes descargar el código fuente como un 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 una 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 elijas. 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 cuestionarios de opción múltiple que consta de dos pantallas que siguen el patrón de diseño modelo-vista-modelo-vista o MVVM. QuestionScreen
(View) usa la clase QuizViewModel
(View-Model) para hacerle al usuario preguntas de opción múltiple de la clase QuestionBank
(Model).
- home_screen.dart: Muestra una pantalla con el botón New Game.
- main.dart: Configura
MaterialApp
para 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 trivia.
- view_model.dart: Almacena el estado y la lógica del juego de cuestionario que muestra
QuestionScreen
.
La app aún 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. Usa 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ícitos comunes, explora la documentación de la API de ImplicitlyAnimatedWidget.
Crea el widget de tabla de clasificación 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 anteriormente mostraban la puntuación y el recuento total de preguntas. El editor debería agregar automáticamente el import "scoreboard.dart"
obligatorio 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 de inmediato 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 widget nuevo llamado AnimatedStar
que use un widget AnimatedScale
para cambiar el importe 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( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
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 color
de Icon
no está animado aquí, solo el scale
, que realiza 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 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 debe 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 interpolación 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<Color>
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. 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, // Modify from here...
);
}, // To here.
),
);
}
}
Ahora, vuelve a cargar 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 suave con los valores intermedios correctos.
Aplica una curva
Ambos efectos de animación se ejecutan a una velocidad constante, pero las animaciones suelen ser más interesantes y informativas a nivel visual cuando se aceleran o ralentizan.
Un Curve
aplica una función de suavización, que define la tasa de cambio de un parámetro a lo largo del tiempo. Flutter se envía con una colección de curvas de suavización precompiladas en la clase Curves
, como easeIn
o easeOut
.
Estos diagramas (disponibles en la página de documentación de la API de Curves
) dan 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 varios efectos de animación cuando se usa una curva de suavizació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.
Vuelve a cargar la app en caliente para ver esta curva aplicada a AnimatedSize
y TweenAnimationBuilder
.
Cómo usar DevTools para habilitar animaciones lentas
Para depurar cualquier efecto de animación, las herramientas para desarrolladores de Flutter proporcionan una forma de ralentizar todas las animaciones de tu app, de modo que puedas verlas 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. Para ello, selecciónalo en la barra de herramientas de depuración de VSCode o selecciona el botón Open Flutter DevTools en la ventana de herramientas de depuración de IntelliJ o Android Studio.
Una vez que se abra el inspector de widgets, haz clic en el botón Animaciones lentas de 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 precompilados, pero, en lugar de tomar un valor objetivo, toman un objeto Animation
como parámetro. Esto los 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.
Cómo 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 gran 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<Offset>
para convertir el Animation<double>
que proporciona AnimatedSwitcher
en un Animation<Offset>
, 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
a Animation
y, luego, convertirlo de un Tween<double>
que varía de 0.0 a 1.0 a un Tween<Offset>
que pasa 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 Animation
nuevo. Esto permite que los interpolación 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 fácilmente. Agrega otra animación explícita, FadeTransition, que usa la misma animación curva uniendo 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
},
Cómo personalizar layoutBuilder
Es posible que notes un pequeño problema con AnimationSwitcher. Cuando una tarjeta de pregunta cambia a una pregunta nueva, la coloca en el centro del espacio disponible mientras se ejecuta la animación, pero cuando se detiene, el widget se ajusta a la parte superior de la pantalla. Esto causa 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, 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 (en contraste con ImplicitlyAnimatedWidgets, que toman un valor y una duración objetivo).
- La clase Animation representa una animación en ejecución, pero no define un efecto específico.
- Usa Tween().animate o Animation.drive() para aplicar Tweens y Curves (con CurveTween) a una animación.
- Usa el parámetro layoutBuilder de AnimatedSwitcher para ajustar la forma en que se organizan 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 una animación para funcionar correctamente. En esta sección, aprenderás a crear tus propios objetos de animación con un AnimationController y a usar una TweenSequence para combinar Tweens.
Cómo ejecutar una animación con un AnimationController
Para crear una animación con un AnimationController, deberás seguir estos pasos:
- Crea un StatefulWidget
- Usa el mixin SingleTickerProviderStateMixin en tu clase de estado para proporcionar un Ticker a tu AnimationController.
- Inicializa AnimationController en el método de ciclo de vida initState y proporciona el objeto State actual al parámetro
vsync
(TickerProvider). - Asegúrate de que tu widget se vuelva a compilar cada vez que AnimationController notifique a sus objetos de escucha, ya sea con AnimatedBuilder o llamando a listen() y setState de 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 cambió la configuración del widget y que puede haber un widget secundario nuevo.
AnimatedBuilder se asegura de que el árbol de widgets se vuelva a compilar cada vez que AnimationController notifica a sus objetos de escucha, y el widget Transform se usa para aplicar un efecto de rotación en 3D para simular que se voltea una tarjeta.
Para usar este widget, une cada tarjeta de respuesta con un widget CardFlipEffect. Asegúrate de proporcionar un key
al widget de tarjeta:
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 voltean 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, a menudo es 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 estado, 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 un retraso al widget CardFlipEffect para que cada tarjeta se voltee de a una a la vez. 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 una nueva animación que aplique la demora con un TweenSequence
. Ten en cuenta que esto no usa ninguna utilidad de la biblioteca dart:async
, como Future.delayed
. Esto se debe a que la demora es 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 DevTools, ya que usa el mismo TickerProvider.
Para usar un TweenSequence
, crea dos TweenSequenceItem
, 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>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
Por último, reemplaza la animación de AnimationController por la nueva animación retrasada en el método de compilación.
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 observa cómo se voltean las tarjetas una por una. Para un desafío, intenta experimentar 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 que proporciona el paquete oficial de animaciones 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 el navegador.
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 una FadeTransition (un widget animado de forma explícita) para que la pantalla nueva se desvanezca sobre la pantalla anterior.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
El paquete de animaciones proporciona efectos de animación prediseñados, 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( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
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 ver detrás de la ruta o la app actuales para ver qué hay antes de navegar. La animación de vista previa se controla según la ubicación del dedo del usuario mientras lo arrastra hacia atrás en 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 mostrar 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 atrás predictivo cuando se navega entre rutas dentro de una app de Flutter. Un PageTransitionsBuilder especial llamado PredictiveBackPageTransitionsBuilder
detecta los gestos atrás predictivos del sistema y controla su transición de página con el progreso del gesto.
El gesto atrás predictivo solo es compatible con Android U y versiones posteriores, pero Flutter recurrirá de forma fluida 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 usar PredictiveBack en Android y el efecto de transición de atenuación 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),
useMaterial3: true,
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 de Navigator.push() a una MaterialPageRoute.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
Cómo usar FadeThroughTransition para cambiar la pregunta actual
El widget AnimatedSwitcher solo proporciona una animación en su devolución de llamada del compilador. Para abordar 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( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
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 contenedor que se expande para crear una conexión visual entre dos widgets.
El widget que muestra closedBuilder
se muestra inicialmente y se expande al widget que muestra 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 de openContainer
al modelo de vista, agrega un nuevo pase del modelo de vista 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 la tarjeta por un widget OpenContainer del paquete de animaciones y agrega dos campos nuevos para la devolución de llamada de viewModel y Open Container:
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 curvas y interpolación a una animación
- Cómo usar widgets de transición precompilados, como AnimatedSwitcher o PageRouteBuilder
- Cómo usar efectos de animación precompilados sofisticados del paquete
animations
, como FadeThroughTransition y OpenContainer - Cómo personalizar la animación de transición predeterminada, incluida la compatibilidad con el gesto de atrás predictivo en Android
¿Qué sigue?
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 muestra 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 de animaciones (instructivo)
- Animaciones implícitas (instructivo)
- Anima las propiedades de un contenedor (libro de recetas)
- Cómo desvanecer un widget (libro de recetas)
- Animaciones de héroe
- Cómo animar una transición de ruta de la página (libro de recetas)
- Cómo animar un widget con una simulación de física (libro de recetas)
- 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
- ¿Cómo elegir el widget de animación de Flutter adecuado para ti?
- Animaciones direccionales con animaciones explícitas integradas
- Aspectos básicos de la animación de Flutter con animaciones implícitas
- ¿Cuándo debo usar AnimatedBuilder o AnimatedWidget?