Tutorial de animaciones

Este tutorial te muestra como construir animaciones en Flutter. Después de introducir algunos conceptos esenciales, clases y métodos, de la biblioteca de animaciones, te conduce a través de 5 ejemplos de animación. Los ejemplos se basan unos en otros, introduciéndote en diferentes aspectos de la biblioteca de animaciones.

El SDK de Flutter también proporciona animaciones de transición, como son [FadeTransition][], [SizeTransition][], y [SlideTransition][]. Estas animaciones simples son ejecutadas definiendo un punto de inicio y de fin. Son más simples que las animaciones explícitas, que describimos aquí.

Conceptos y clases esenciales de animaciones

El sistema de animaciones en Flutter está basado en objetos [Animation][] tipados. Los widgets pueden incorporar estos objetos animation en sus funciones build directamente al leer su valor actual y escuchar sus cambios de estado, o pueden usarlos como la base de animaciones más elaboradas que pasan a través de otros widgets.

Animation<double>

En Flutter, un objeto Animation no sabe nada sobre que hay en la pantalla. Un objeto Animation es una clase abstracta que entiende su valor actual y su estado (completado o rechazado). Uno de los tipos de animation más comúnmente usados es Animation<double>.

Un objeto Animation en Flutter es una clase que genera secuencialmente números interpolándolos entre dos valores durante una cierta duración. La salida de un objeto Animation puede ser lineal, una curva, una función por pasos, o cualquier otro mapeado que puedas idear. Dependiendo de como el objeto Animation se controle, podría ejecutarse en modo inverso, o incluso cambiar la dirección en el medio.

Los objetos Animation pueden también interpolar otros tipos diferentes a double, como Animation<Color> o Animation<Size>.

El objeto Animation tiene estado. El valor actual siempre esta disponible en la propiedad .value.

Un objeto Animation no conoce nada sobre renderizado o funciones build().

CurvedAnimation

Un objeto [CurvedAnimation][] define el progreso de una animación como una curva no lineal.

animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);

CurvedAnimation y AnimationController (descrito en la siguiente sección), son ambas de tipo Animation<double>, puedes pasarlas de forma intercambiable. El objeto CurvedAnimation envuelve el objeto que está modificando—no puedes hacer una subclase de AnimationController para implementar una curva.

Animation­Controller

[AnimationController][] es un objeto Animation especial que genera un nuevo valor cada vez que el hardware esta preparado para un nuevo frame. Por defecto, un AnimationController produce linealmente números desde 0.0 a 1.0 durante una duración dada. Por ejemplo, este código crea un objeto Animation, pero no comienza su ejecución:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);

AnimationController deriva de Animation<double>, por esto puede ser usado donde quiera que se necesite un objeto Animation. Sin embargo, AnimationController tiene métodos adicionales para controlar la animación. Por ejemplo, inicias una animación con el método .forward(). La generación de números está vinculada al refresco de la pantalla, normalmente son generados 60 numeros por segundo. Después de que cada número es generado, cada objeto Animation llama a sus objetos Listener asociados. Para crear una lista personalizada para cada hijo, mira [RepaintBoundary][].

Cuando creas un AnimationController, le pasas un argumento vsync. La presencia de vsync previene animaciones fuera de pantalla que consuman recursos innecesarios. Puedes usar tu objeto stateful como vsync añadiendo SingleTickerProviderStateMixin a la definición de la clase. Puedes ver un ejemplo de esto en animate1 en GitHub.

Tween

Por defecto, el objeto AnimationController tiene rangos entre 0.0 y 1.0. Si necesitas un rango diferente o un tipo de datos diferente, puedes usar Tween para configurar un objeto animation que interpole un rango o tipo de dato diferente. Por ejemplo, el siguiente Tween va desde -200.0 a 0.0:

tween = Tween<double>(begin: -200, end: 0);

Un Tween es un objeto stateless que solo toma las propiedades begin y end. El único trabajo de un Tween es definir un mapeado entre un rango de entrada y un rango de salida. El rango de entrada en normalment 0.0 a 1.0, pero esto no es un requisito.

Un Tween hereda de Animatable<T>, no de Animation<T>. Un Animatable, como un Animation, no tiene porque tener una salida de tipo double. Por ejemplo, ColorTween especifica una progresión entre dos colores.

colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

Un objeto Tween no almacena ningun estado. En cambio, provee el método evaluate(Animation<double> animation) que aplica la función de mapeado al valor actual del objeto Animation. El valor actual del objeto Animation puede ser encontrado en el método .value. La función evaluate function también realiza algunas labores de limpieza, como asegurar que se devuelva begin y end cuando los valores del objeto animation sean 0.0 y 1.0, respectivamente.

Tween.animate

Para usar el objeto Tween, llama a animate() en Tween, pasado en el objeto controller. Por ejemplo, el siguiente código genera los valores enteros entre 0 y 255 en el trascurso de 500 ms.

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

El siguiente ejemplo muestra un controller, un curve, y un Tween:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

Notificaciones de Animation

Un objeto [Animation][] puede tener Listeners y StatusListeners, definidos con addListener() y addStatusListener(). Un Listener es llamado cada vez que el valor del objeto animation cambia. El comportamiento mas habitual de un Listener es llamar a setState() para provocar un rebuild. Un StatusListener es llamado cuando una animación empieza, finaliza, se mueve hacia delante, o se mueve hacia atrás, como es definido por AnimationStatus. La nueva sección tiene un ejemplo del método addListener(), y Monitoriza el progreso de la animación monstrando un ejemplo de addStatusListener().


Ejemplo de animaciones

Esta sección te conduce a través de 5 ejemplos de animaciones. Cada sección proporciona un enlace al código fuente del ejemplo.

Rendering animations

Hasta ahora has aprendido como generar una secuencia de números en el trascurso del un tiempo. Nada se ha renderizado en la pantalla. Para renderizar con un objeto Animation;, guarda el objeto Animation como un miembro de tu Widget, entonces usa su valor para decidir que dibujar.

Considera la siguiente aplicación que dibuja el logo de Flutter sin animación:

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        child: FlutterLogo(),
      ),
    );
  }
}

Lo siguiente muestra el mismo código modificado para animar el logo para crecer de nada al tamaño completo. Cuando defines un AnimationController, debes pasarlo en un objeto vsync. El parámetro vsync es descrito en la sección AnimationController.

Los cambios desde el ejemplo no animado están resaltados:

{animate0 → animate1}/lib/main.dart
@@ -1,3 +1,4 @@
1
+ import 'package:flutter/animation.dart';
1
2
import 'package:flutter/material.dart';
2
3
void main() => runApp(LogoApp());
@@ -6,16 +7,39 @@
6
7
_LogoAppState createState() => _LogoAppState();
7
8
}
8
- class _LogoAppState extends State<LogoApp> {
9
+ class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
10
+ Animation<double> animation;
11
+ AnimationController controller;
12
+
13
+ @override
14
+ void initState() {
15
+ super.initState();
16
+ controller =
17
+ AnimationController(duration: const Duration(seconds: 2), vsync: this);
18
+ animation = Tween<double>(begin: 0, end: 300).animate(controller)
19
+ ..addListener(() {
20
+ setState(() {
21
+ // The state that has changed here is the animation object’s value.
22
+ });
23
+ });
24
+ controller.forward();
25
+ }
26
+
9
27
@override
10
28
Widget build(BuildContext context) {
11
29
return Center(
12
30
child: Container(
13
31
margin: EdgeInsets.symmetric(vertical: 10),
14
- height: 300,
15
- width: 300,
32
+ height: animation.value,
33
+ width: animation.value,
16
34
child: FlutterLogo(),
17
35
),
18
36
);
19
37
}
38
+
39
+ @override
40
+ void dispose() {
41
+ controller.dispose();
42
+ super.dispose();
43
+ }
20
44
}

La función addListener() llama a setState(), cada vez que el objeto Animation genera un nuevo número, el frame actual es marcado como dirty, lo caul fuerza al método build() a ser llamado de nuevo. En la función build(), el container cambia su tamaño porque su altura y anchura ahora usan animation.value en lugar de un valor fijo. Deseche con el método dispose el controlador cuando la animación haya terminado para prevenir memory leaks.

Con estos pocos cambioss, habrás creado, ¡tu primera animación en Flutter! Puedes encontrar el código fuente para este ejemplo en, animate1.

Simplificando con AnimatedWidget

La clase AnimatedWidget te permte separar el código del widger del código de la animación en la llamada a setState(). AnimatedWidget no neceita mantener un objeto State para sostener la animación.

lib/main.dart (AnimatedLogo)
class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

LogoApp pasa el objeto Animation a la clase base y usa animation.value para fijar el alto y el ancho del container, funcionando entonces exactamente igual que antes.

{animate1 → animate2}/lib/main.dart
@@ -10,2 +27,2 @@
10
27
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
11
28
Animation<double> animation;
@@ -13,32 +30,18 @@
13
30
@override
14
31
void initState() {
15
32
super.initState();
16
33
controller =
17
34
AnimationController(duration: const Duration(seconds: 2), vsync: this);
18
- animation = Tween<double>(begin: 0, end: 300).animate(controller)
35
+ animation = Tween<double>(begin: 0, end: 300).animate(controller);
19
- ..addListener(() {
20
- setState(() {
21
- // The state that has changed here is the animation object’s value.
22
- });
23
- });
24
36
controller.forward();
25
37
}
26
38
@override
27
- Widget build(BuildContext context) {
39
+ Widget build(BuildContext context) => AnimatedLogo(animation: animation);
28
- return Center(
29
- child: Container(
30
- margin: EdgeInsets.symmetric(vertical: 10),
31
- height: animation.value,
32
- width: animation.value,
33
- child: FlutterLogo(),
34
- ),
35
- );
36
- }
37
40
@override
38
41
void dispose() {
39
42
controller.dispose();
40
43
super.dispose();
41
44
}

App source: animate2

Monitorzando el progreso de la animación

A menudo es útil saber cuando una animación cambia su estado, como cuando finaliza, avanza hacia delante, o hacia atrás. Puedes obtener notificaciones de esto con addStatusListener(). El siguiente códgo modifica el ejemplo previo para que escuche los cambios de estado e imprima una actualización. Las líneas resaltadas muestran los cambios:

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  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((state) => print('$state'));
    controller.forward();
  }
  // ...
}

Ejecutar este código produce líneas como las siguientes:

AnimationStatus.forward
AnimationStatus.completed

A continuación, usa addStatusListener() para invertir la animación en el principio o en el final. Esto crea un efecto “respiración”:

{animate2 → animate3}/lib/main.dart
@@ -32,7 +32,15 @@
32
32
void initState() {
33
33
super.initState();
34
34
controller =
35
35
AnimationController(duration: const Duration(seconds: 2), vsync: this);
36
- animation = Tween<double>(begin: 0, end: 300).animate(controller);
36
+ animation = Tween<double>(begin: 0, end: 300).animate(controller)
37
+ ..addStatusListener((status) {
38
+ if (status == AnimationStatus.completed) {
39
+ controller.reverse();
40
+ } else if (status == AnimationStatus.dismissed) {
41
+ controller.forward();
42
+ }
43
+ })
44
+ ..addStatusListener((state) => print('$state'));
37
45
controller.forward();
38
46
}

App source: animate3

Refactorizando con AnimatedBuilder

Un problema con el código en el ejemplo animate3 , es que cambiar la animación requiere cambiar el widget que renderiza el logo. Una mejor solución es separar las responsabilidades en dos clases diferentes:

  • Renderizar el logo
  • Definir el objeto Animation
  • Renderizar la transición

Puedes conseguir esta separación con la ayuda de la clase AnimatedBuilder. Un AnimatedBuilder es una clase separada en el árbol de renderizado. Como AnimatedWidget, AnimatedBuilder automáticamente escucha las notificaciones del objeto Animation, y marca el árbol de widgets como dirty cuando sea necesario, entonces no necesitas llamar a addListener().

El árbol de widgets para el ejemplo animate5 se ve como esto:

AnimatedBuilder widget tree

Empezando por el fondo del árbol de widget, el código para renderizar el logo es sencillo:

class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  Widget build(BuildContext context) => Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        child: FlutterLogo(),
      );
}

Los tres bloques centrales en el diagrama son todos creados en el método build() en GrowTransition. El widget GrowTransition en sí mismo es stateless y soporta el conjunto final de variable necesarias para definir la animación de transición. La función build() crea y devuelve el AnimatedBuilder, que toma el método (constructor anónimo) y el objeto LogoWidget como parámetros. El trabajo de renderizar la transición actualmente ocure en el método (construcor anónimo), que crea un Container del tamaño apropiado para forzar a LogoWidget a ajustarse para llenarlo.

Un punto complicado en el código más abajo, es que la propiedad child se ve como si se hubiera definido dos veces. Lo que está ocurriendo es que la referencia externa del hijo esta siendo pasada al AnimatedBuilder, el cual pase este a la función anónima, que usa este objeto como su hijo. La red resulta en que AnimatedBuilder es insertado entre los dos widgets en el árbol de renderizado.

class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
        child: AnimatedBuilder(
            animation: animation,
            builder: (context, child) => Container(
                  height: animation.value,
                  width: animation.value,
                  child: child,
                ),
            child: child),
      );
}

Finalmente, el código para iniciar la animación se ve muy similar al primer ejemplo, animate1. El método initState() crea un AnimationController y un Tween, entonces vincula estos con animate(). La mágia ocurre en el método build(), que devuelve un objeto GrowTransition con un LogoWidget como hijo, un objeto animation para dirigir la transición. Estos son los tres elementos listados en los puntos más arriba.

{animate2 → animate4}/lib/main.dart
@@ -27,22 +36,25 @@
27
36
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
28
37
Animation<double> animation;
29
38
AnimationController controller;
30
39
@override
31
40
void initState() {
32
41
super.initState();
33
42
controller =
34
43
AnimationController(duration: const Duration(seconds: 2), vsync: this);
35
44
animation = Tween<double>(begin: 0, end: 300).animate(controller);
36
45
controller.forward();
37
46
}
38
47
@override
39
- Widget build(BuildContext context) => AnimatedLogo(animation: animation);
48
+ Widget build(BuildContext context) => GrowTransition(
49
+ child: LogoWidget(),
50
+ animation: animation,
51
+ );
40
52
@override
41
53
void dispose() {
42
54
controller.dispose();
43
55
super.dispose();
44
56
}
45
57
}

App source: animate4

Animaciones simultáneas

En esta sección, construirás el ejemplo de monitorizando el progreso de la animación (animate3), que usa AnimatedWidget para animarlo dentro y fuera continuamente. Considera el caso en que queras animar adentro y afuera mientras que animas la opacidad de transparente a opaco.

Cada tween administra 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 obtber el tamaño con sizeAnimation.value y la opacidad con opacityAnimation.value, pero el construcor para AnimatedWidget solo toma un único objeto Animation. Para resolver este problema, el ejemplo crea su propio objeto Tween y calcula los valores explícitamente.

El widget AnimatedLogo fue cambiado para encapsular sus propios objetos Tween. Su método build llama a la función Tween.evaluate() del Tween en el objeto animation padre para calcular el tamaño requerido y los valores de opacidad.

El siguiente código muestra los cambios con resaltado:

class AnimatedLogo extends AnimatedWidget {
  // 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);

  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          margin: EdgeInsets.symmetric(vertical: 10),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: FlutterLogo(),
        ),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  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();
  }
}

App source: animate5

Siguientes pasos

Este tutorial te da una base para crear animaciones en Flutter usando Tweens, pero hay muchas otras clases a explorar. Puedes investigar las clases especializadas Tween, animaciones específicas de Material Design, ReverseAnimation, elementos compartidos en transiciones (también conocidas como animaciones Hero), simulaciones físicas y métodos fling(). Mira la página animaciones para los últimos documentos y ejemplos disponibles.

AnimatedWidget]: https://api.flutter.dev/flutter/widgets/AnimatedWidget-class.html [Animatable]: https://api.flutter.dev/flutter/animation/Animatable-class.html [Animation]: https://api.flutter.dev/flutter/animation/Animation-class.html [AnimatedBuilder]: https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html [AnimationController]: https://api.flutter.dev/flutter/animation/AnimationController-class.html [Curves]: https://api.flutter.dev/flutter/animation/Curves-class.html [CurvedAnimation]: https://api.flutter.dev/flutter/animation/CurvedAnimation-class.html [FadeTransition]: https://api.flutter.dev/flutter/widgets/FadeTransition-class.html [RepaintBoundary]: https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html [SlideTransition]: https://api.flutter.dev/flutter/widgets/SlideTransition-class.html [SizeTransition]: https://api.flutter.dev/flutter/widgets/SizeTransition-class.html [Tween]: https://api.flutter.dev/flutter/animation/Tween-class.html