Animaciones escalonadas

Lo que aprenderás:

  • Una animación escalonada consiste en animaciones secuenciales o superpuestas.
  • Para crear una animación escalonada, utiliza varios objetos de animación.
  • Un AnimationController controla todas las Animaciones.
  • Cada objeto de animación especifica la animación durante un intervalo.
  • Para cada propiedad que se esté animando, crea una interpolación.

Las animaciones escalonadas son un concepto sencillo: los cambios visuales ocurren como una serie de operaciones, en lugar de todas a la vez. La animación puede ser puramente secuencial, con un cambio que ocurre después del siguiente, o puede superponerse parcial o completamente. También podría tener vacíos, donde no ocurren cambios.

Esta guía muestra cómo crear una animación escalonada en Flutter.

El siguiente video muestra la animación realizada por basic_staggered_animation:

En el vídeo, puedes ver la siguiente animación de un único widget, que comienza como un cuadrado azul bordeado con esquinas ligeramente redondeadas. El cuadrado pasa por cambios en el siguiente orden:

  1. Fades in
  2. Se expande
  3. Se hace más alto mientras se mueve hacia arriba
  4. Se transforma en un círculo delimitado
  5. Cambia de color a naranja

Después de correr hacia adelante, la animación se ejecuta en reversa.

Estructura básica de una animación escalonada

¿Qué aprenderás?

  • Todas las animaciones son conducidas por el mismo AnimationController.
  • Independientemente de cuánto dure la animación en tiempo real, los valores del controlador deben estar entre 0.0 y 1.0, inclusive.
  • Cada animación tiene un Interval entre 0.0 y 1.0, inclusive.
  • Para cada propiedad que se anima en un intervalo, crea un Tween. Tween especifica los valores de inicio y final para esa propiedad.
  • El Tween produce un objeto Animation que es manejado por el controlador.

El siguiente diagrama muestra los Intervalos utilizados en el ejemplo basic_staggered_animation. Puedes notar las siguientes características:

  • La opacidad cambia durante el primer 10% de la línea de tiempo.
  • Se produce un pequeño hueco entre el cambio de opacidad y el cambio de anchura.
  • Nada se anima durante el último 25% de la línea de tiempo.
  • Aumentar el relleno hace que el widget parezca subir hacia arriba.
  • Aumentando el radio del borde a 0.5, transforma el cuadrado con esquinas redondeadas en un círculo.
  • Los cambios de padding en el radio y del borde ocurren durante el mismo intervalo exacto, pero no es necesario.

Diagram showing the interval specified for each motion.

Para configurar la animación:

  • Crear un AnimationController que administre todas las Animaciones.
  • Crear un Tween para cada propiedad que se esté animando.
    • El Tween define un rango de valores.
    • El método animated de los Tween requiere el controlador parent, y produce una Animación para esa propiedad.
  • Especifica el intervalo en la propiedad curved de la animación.

Cuando el valor de la animación de control cambia, el valor de la nueva animación cambia, provocando que la interfaz de usuario se actualice.

El siguiente código crea una interpolación para la propiedad width. Construye un CurvedAnimation, especificando una eased curve. ver Curves para otras curvas de animación predefinidas disponibles.

width = Tween<double>(
  begin: 50.0,
  end: 150.0,
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(
      0.125, 0.250,
      curve: Curves.ease,
    ),
  ),
),

Los valores de begin y end no tienen que ser dobles. El siguiente código construye la interpolación para la propiedad borderRadius (que controla la redondez de las esquinas del cuadrado), usando BorderRadius.circular().

borderRadius = BorderRadiusTween(
  begin: BorderRadius.circular(4.0),
  end: BorderRadius.circular(75.0),
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(
      0.375, 0.500,
      curve: Curves.ease,
    ),
  ),
),

Animación completa escalonada

Como todos los widgets interactivos, la animación completa consiste en un par de widgets: un widget sin estado y un widget con estado.

El widget sin estado especifica los Tweens, define los objetos de Animación, y proporciona una función build() responsable de construir la parte animada del árbol de widgets.

El widget de estado crea el controlador, reproduce la animación y construye la parte no animada del árbol de widgets. La animación comienza cuando se detecta un toque en cualquier parte de la pantalla.

Código completo de main.dart para basic_staggered_animation

Widget sin estado: StaggerAnimation

En el widget stateless, StaggerAnimation, la función build() instancia un AnimatedBuilder—un widget de propósito general para construir animaciones. El AnimatedBuilder construye un widget y lo configura usando los valores actuales de los Tweens. El ejemplo crea una función llamada _buildAnimation() (que realiza las actualizaciones de la interfaz de usuario), y la asigna a su propiedad builder. AnimatedBuilder escucha las notificaciones del controlador de animación, marcando el árbol de widgets sucio a medida que cambian los valores. Para cada tick de la animación, los valores se actualizan, resultando en una llamada a _buildAnimation().

class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({ Key key, this.controller }) :

    // Cada animación definida aquí transforma su valor durante el subconjunto
    // de la duración del controlador definido por el intervalo de la animación.
    // Por ejemplo, la animación de opacidad transforma su valor durante
    // el primer 10% de la duración del controlador.

    opacity = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          0.0, 0.100,
          curve: Curves.ease,
        ),
      ),
    ),

    // ... Otras definiciones de tween ...

    super(key: key);

  final Animation<double> controller;
  final Animation<double> opacity;
  final Animation<double> width;
  final Animation<double> height;
  final Animation<EdgeInsets> padding;
  final Animation<BorderRadius> borderRadius;
  final Animation<Color> color;

  // Esta función se llama cada vez que el controlador "ticks" un nuevo frame.
  // Cuando se ejecuta, todos los valores de la animación habrán sido
  // actualizados para reflejar el valor actual del controlador.
  Widget _buildAnimation(BuildContext context, Widget child) {
    return Container(
      padding: padding.value,
      alignment: Alignment.bottomCenter,
      child: Opacity(
        opacity: opacity.value,
        child: Container(
          width: width.value,
          height: height.value,
          decoration: BoxDecoration(
            color: color.value,
            border: Border.all(
              color: Colors.indigo[300],
              width: 3.0,
            ),
            borderRadius: borderRadius.value,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

Widget con estado: StaggerDemo

El widget de estado, StaggerDemo, crea el AnimationController (el que las gobierna a todas), especificando una duración de 2000ms. Reproduce la animación y construye la parte no animada del árbol de widgets. La animación comienza cuando se detecta un tap en la pantalla. La animación se ejecuta hacia adelante y luego hacia atrás.

class StaggerDemo extends StatefulWidget {
  @override
  _StaggerDemoState createState() => _StaggerDemoState();
}

class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this
    );
  }

  // ...Boilerplate...

  Future<Null> _playAnimation() async {
    try {
      await _controller.forward().orCancel;
      await _controller.reverse().orCancel;
    } on TickerCanceled {
      // la animación fue cancelada, probablemente porque la descartamos
    }
  }

  @override
  Widget build(BuildContext context) {
    timeDilation = 10.0; // 1.0 es la velocidad normal.
    return Scaffold(
      appBar: AppBar(
        title: const Text('Staggered Animation'),
      ),
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          _playAnimation();
        },
        child: Center(
          child: Container(
            width: 300.0,
            height: 300.0,
            decoration: BoxDecoration(
              color: Colors.black.withOpacity(0.1),
              border: Border.all(
                color:  Colors.black.withOpacity(0.5),
              ),
            ),
            child: StaggerAnimation(
              controller: _controller.view
            ),
          ),
        ),
      ),
    );
  }
}

Recursos

Los siguientes recursos pueden ayudar a escribir animaciones:

Animaciones de landing page
Enumera la documentación disponible para las animaciones de Flutter. Si las interpolaciones son nuevas para ti, consulta el tutorial de Animaciones.
Documentation de la API de Flutter
Documentación de referencia para todas las bibliotecas de Flutter. En particular, mira la documentación de la librería de animación.
Galeria de Flutter
Demo app que muestra muchos Componentes de Material y otras características de Flutter. La demo Shrine implementa una animación Hero.
Especificación del movimiento Material
Describe el movimiento de las aplicaciones de Material.