Kodokon kodokon.com

Animaciones: implícitas y explícitas por dentro

Comprende la mecánica interna de las animaciones implícitas y controla con precisión tu propio AnimationController.

10 min · 3 preguntas

Abrir esta lección en Kodokon

AnimatedContainer pertenece a la familia de las animaciones implícitas: cambias un valor objetivo y el framework anima la transición. Por dentro, cada ImplicitlyAnimatedWidget guarda un AnimationController oculto en su estado. Cuando llega una nueva configuración, didUpdateWidget recorre las propiedades mediante forEachTween y reapunta cada tween: el límite begin se convierte en el valor interpolado actual, el límite end en el nuevo objetivo, y luego el controlador reinicia desde cero. Una consecuencia valiosa: cambiar el objetivo a mitad de animación no provoca ningún salto visual: la animación retoma exactamente donde estaba.

DART
import 'package:flutter/material.dart';

class SelectableCard extends StatefulWidget {
  const SelectableCard({super.key});

  @override
  State<SelectableCard> createState() =>
      _SelectableCardState();
}

class _SelectableCardState extends State<SelectableCard> {
  bool _selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () =>
          setState(() => _selected = !_selected),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOutCubic,
        width: _selected ? 220 : 160,
        height: 96,
        decoration: BoxDecoration(
          color: _selected
              ? Colors.indigo
              : Colors.indigo.shade200,
          borderRadius: BorderRadius.circular(
            _selected ? 28 : 12,
          ),
        ),
      ),
    );
  }
}
Tamaño, color y radio animados desde un solo setState.

Las implícitas cubren las propiedades que el framework anticipa, y TweenAnimationBuilder generaliza el proceso a cualquier valor interpolable. En cuanto necesites controlar la animación (pausar, invertir, repetir, reaccionar al estado), pasa al AnimationController explícito. El parámetro vsync espera un TickerProvider: el mixin crea un Ticker, un callback invocado en cada frame por el SchedulerBinding. Una sutileza poco conocida: TickerMode silencia los tickers de un subárbol oculto (por ejemplo, bajo una ruta tapada). El tiempo sigue corriendo, pero ya no se ejecuta ningún callback: cero CPU desperdiciada en animaciones invisibles.

DART
import 'package:flutter/material.dart';

class RevealPanel extends StatefulWidget {
  const RevealPanel({super.key, required this.child});

  final Widget child;

  @override
  State<RevealPanel> createState() => _RevealPanelState();
}

class _RevealPanelState extends State<RevealPanel>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller =
      AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 400),
    reverseDuration: const Duration(milliseconds: 200),
  );

  late final Animation<double> _opacity = _controller
      .drive(CurveTween(curve: Curves.easeOutCubic));

  void toggle() {
    switch (_controller.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        _controller.reverse();
      case AnimationStatus.reverse:
      case AnimationStatus.dismissed:
        _controller.forward();
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: toggle,
      child: FadeTransition(
        opacity: _opacity,
        child: widget.child,
      ),
    );
  }
}
reverseDuration: la ida y la vuelta tienen cada una su propio tempo.

Un controlador no es más que una Animation<double> que va de 0 a 1: compónla en lugar de consumirla en crudo. controller.drive(CurveTween(...)) produce una animación derivada sin estado adicional. Para aplicar el valor al árbol, prefiere los widgets Transition (FadeTransition, RotationTransition): escuchan la animación directamente a nivel del render object, sin reconstruir ni un solo widget por frame. Cuando la transformación es arbitraria, AnimatedBuilder cumple la función, siempre que pases el subárbol estático a través de child: se construye una sola vez y luego se reinyecta en builder en cada frame. Esa es la optimización más rentable para las animaciones explícitas.

DART
import 'dart:math' as math;

import 'package:flutter/material.dart';

class Orbit extends StatefulWidget {
  const Orbit({super.key});

  @override
  State<Orbit> createState() => _OrbitState();
}

class _OrbitState extends State<Orbit>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller =
      AnimationController(
    vsync: this,
    duration: const Duration(seconds: 4),
  )..repeat();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      child: const Icon(Icons.satellite_alt, size: 32),
      builder: (context, child) {
        final angle = _controller.value * 2 * math.pi;
        return Transform.translate(
          offset: Offset(
            math.cos(angle) * 80,
            math.sin(angle) * 80,
          ),
          child: child,
        );
      },
    );
  }
}
El icono pasado como child se construye una sola vez.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. Un AnimatedContainer recibe un nuevo color objetivo cuando la animación anterior va por la mitad. ¿Qué ocurre?
    • El tween se reapunta: reinicia desde el valor actual hacia el nuevo objetivo, sin ningún salto visual
    • La animación en curso termina primero, y luego empieza la nueva
    • El widget salta de inmediato al nuevo color
  2. ¿Para qué sirve exactamente el parámetro vsync de un AnimationController?
    • Proporciona un Ticker sincronizado con los frames, que TickerMode puede silenciar cuando el subárbol está oculto
    • Sincroniza el controlador con las demás animaciones de la app
    • Limita la animación a 30 frames por segundo
    • Activa la aceleración por hardware de la GPU
  3. ¿Por qué pasar el subárbol estático a través del parámetro child de AnimatedBuilder?
    • Se construye una vez y luego se reinyecta en builder, en lugar de reconstruirse en cada frame
    • Es obligatorio: builder no puede devolver nuevos widgets
    • Libera automáticamente el controlador al final