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.
AnimationController
[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:
|
@@ -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:
|
|
15
|
-
width:
|
|
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.
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.
|
@@ -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”:
|
@@ -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:

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.
|
@@ -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) =>
|
|
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

