skills/flutter-animations/references/explicit.md

11 KiB

Explicit Animations Reference

Explicit animations provide full control using AnimationController, Tween, and animation status listeners.

Core Components

AnimationController

The heart of explicit animations. Manages animation lifecycle, value, and timing.

late AnimationController _controller;

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    duration: const Duration(seconds: 2),
    vsync: this,  // Required TickerProvider
    lowerBound: 0.0,  // Optional: default 0.0
    upperBound: 1.0,  // Optional: default 1.0
  );
}

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

Lifecycle methods:

// Start animation forward
_controller.forward();

// Start animation backward
_controller.reverse();

// Stop animation
_controller.stop();

// Repeat animation
_controller.repeat();

// Animate to specific value
await _controller.animateTo(0.5);

// Reset to beginning
_controller.reset();

// Animate with physics simulation
_controller.fling(velocity: 2.0);

// Animate with custom simulation
_controller.animateWith(SpringSimulation(...));

Listening to changes:

_controller.addListener(() {
  // Value changed - rebuild needed
  setState(() {});
});

_controller.addStatusListener((status) {
  // Status changed
  switch (status) {
    case AnimationStatus.dismissed:
      print('Animation dismissed (at 0)');
      break;
    case AnimationStatus.forward:
      print('Animation moving forward');
      break;
    case AnimationStatus.reverse:
      print('Animation moving backward');
      break;
    case AnimationStatus.completed:
      print('Animation completed (at 1)');
      break;
  }
});

Tween

Interpolates between begin and end values.

// Simple tween
animation = Tween<double>(begin: 0, end: 300).animate(_controller);

// Color tween
animation = ColorTween(
  begin: Colors.red,
  end: Colors.blue,
).animate(_controller);

// Border radius tween
animation = BorderRadiusTween(
  begin: BorderRadius.circular(4),
  end: BorderRadius.circular(75),
).animate(_controller);

// Rect tween
animation = RectTween(
  begin: Rect.fromLTWH(0, 0, 100, 100),
  end: Rect.fromLTWH(100, 100, 200, 200),
).animate(_controller);

Common Tweens:

  • Tween<T> - Generic tween (use for most types)
  • ColorTween - Color interpolation
  • RectTween - Rectangle interpolation
  • IntTween - Integer interpolation
  • SizeTween - Size interpolation
  • OffsetTween - Offset interpolation
  • BorderRadiusTween - Border radius interpolation

CurvedAnimation

Applies a curve to transform animation values.

animation = CurvedAnimation(
  parent: _controller,
  curve: Curves.easeInOut,
  reverseCurve: Curves.easeIn,  // Optional: different curve for reverse
);

With Interval:

animation = CurvedAnimation(
  parent: _controller,
  curve: const Interval(
    0.25,  // Start at 25% of controller
    0.75,  // End at 75% of controller
    curve: Curves.easeInOut,
  ),
);

ReverseAnimation

Reverses the parent animation.

final forwardAnimation = Tween<double>(begin: 0, end: 1).animate(_controller);
final reverseAnimation = ReverseAnimation(forwardAnimation);

Patterns

AnimatedWidget Pattern

Best for reusable animated widgets. Automatically handles rebuilds.

class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
    : super(listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}

// Usage
@override
Widget build(BuildContext context) {
  return AnimatedLogo(animation: _animation);
}

AnimatedBuilder Pattern

Best for complex widgets. Separates animation logic from widget logic.

class GrowTransition extends StatelessWidget {
  const GrowTransition({
    required this.child,
    required this.animation,
    super.key,
  });

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

// Usage
@override
Widget build(BuildContext context) {
  return GrowTransition(
    animation: _animation,
    child: const LogoWidget(),
  );
}

Why pass child twice? The child parameter to AnimatedBuilder is passed to the builder function, allowing the child to be built once and reused, rather than rebuilt every animation frame.

Multiple Animations Pattern

Animate multiple properties from single controller.

class MultiPropertyAnimation extends AnimatedWidget {
  const MultiPropertyAnimation({super.key, required Animation<double> animation})
    : super(listenable: animation);

  static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
  static final _sizeTween = Tween<double>(begin: 0, end: 300);
  static final _colorTween = ColorTween(
    begin: Colors.red,
    end: Colors.blue,
  );

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          decoration: BoxDecoration(
            color: _colorTween.evaluate(animation),
            borderRadius: BorderRadius.circular(12),
          ),
          child: const FlutterLogo(),
        ),
      ),
    );
  }
}

Built-in Transitions

Flutter provides pre-built transitions for common use cases.

FadeTransition

FadeTransition(
  opacity: _animation,
  child: const FlutterLogo(),
)

ScaleTransition

ScaleTransition(
  scale: _animation,
  child: const FlutterLogo(),
)

SlideTransition

SlideTransition(
  position: Tween<Offset>(
    begin: Offset(0, 1),
    end: Offset.zero,
  ).animate(_animation),
  child: const FlutterLogo(),
)

SizeTransition

SizeTransition(
  sizeFactor: _animation,
  axis: Axis.vertical,
  child: const FlutterLogo(),
)

RotationTransition

RotationTransition(
  turns: _animation,
  child: const FlutterLogo(),
)

DecoratedBoxTransition

DecoratedBoxTransition(
  decoration: DecorationTween(
    begin: BoxDecoration(color: Colors.red),
    end: BoxDecoration(color: Colors.blue),
  ).animate(_animation),
  child: const FlutterLogo(),
)

PositionedTransition

Stack(
  children: [
    PositionedTransition(
      rect: RelativeRectTween(
        begin: RelativeRect.fill(calculateRect(context, startPosition)),
        end: RelativeRect.fill(calculateRect(context, endPosition)),
      ).animate(_animation),
      child: const FlutterLogo(),
    ),
  ],
)

Animation Status Handling

Basic Status Loop

_animation.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    _controller.reverse();
  } else if (status == AnimationStatus.dismissed) {
    _controller.forward();
  }
});

One-way Animation

_animation.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    _controller.stop();  // Or navigate, show dialog, etc.
  }
});

Reset and Repeat

_animation.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    _controller.reset();
    _controller.forward();
  }
});

Performance Best Practices

DO

  • Always dispose AnimationController
  • Use AnimatedWidget or AnimatedBuilder instead of setState() in listeners
  • Minimize widget rebuilds during animation
  • Use static Tweens when possible (avoid recreating)
  • Profile with Flutter DevTools
  • Use timeDilation for debugging

DON'T

  • Forget to dispose controllers (memory leak)
  • Call setState() in animation listeners unnecessarily
  • Create complex widget trees inside animation builds
  • Animate too many widgets simultaneously
  • Use animation values directly in expensive operations

Performance Pattern: RepaintBoundary

RepaintBoundary(
  child: AnimatedBuilder(
    animation: _controller,
    builder: (context, child) {
      return ExpensiveWidget();
    },
  ),
)

Debugging

Slow Animations

void main() {
  timeDilation = 10.0;  // 10x slower
  runApp(MyApp());
}

Print Animation Values

_controller.addListener(() {
  print('Animation value: ${_controller.value}');
});

_animation.addStatusListener((status) {
  print('Animation status: $status');
});

Flutter DevTools Performance Overlay

  1. Run app in debug mode
  2. Press 'p' to toggle performance overlay
  3. Look for "GPU UI" and "GPU Raster" graphs
  4. Aim for 60fps (16.67ms per frame)

Common Patterns

Fade In Then Slide Up

_animation = Tween<double>(begin: 0, end: 1).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
  ),
);

_slideAnimation = Tween<Offset>(
  begin: Offset(0, 0.5),
  end: Offset.zero,
).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.3, 1.0, curve: Curves.easeOut),
  ),
);

Bounce Effect

_animation = CurvedAnimation(
  parent: _controller,
  curve: Curves.elasticOut,
);

Shake Effect

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return sin(t * pi * 2);
  }
}

_animation = CurvedAnimation(
  parent: _controller,
  curve: ShakeCurve(),
);

Parallax Scrolling

class ParallaxWidget extends StatelessWidget {
  const ParallaxWidget({
    required this.animation,
    required this.child,
    super.key,
  });

  final Animation<double> animation;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context, child) {
        return Transform.translate(
          offset: Offset(0, animation.value * 50),
          child: child,
        );
      },
      child: child,
    );
  }
}

Comparison: Implicit vs Explicit

Feature Implicit Explicit
Controller None needed AnimationController required
Setup Simple More complex
Control Limited (duration, curve) Full (lifecycle, status, physics)
Multiple Properties AnimatedContainer only Unlimited
Performance Good Excellent (with patterns)
Reusability Widget-based Component-based
State Monitoring Limited (onEnd) Full (status listeners)

When to Use Explicit Animations

Use explicit animations when:

  • Need full control over animation lifecycle
  • Animating multiple properties
  • Need to react to animation state changes
  • Creating reusable animation components
  • Need complex timing or sequencing
  • Performance is critical
  • Want physics-based or custom animations

Use implicit animations when:

  • Simple, one-off animations
  • Animation triggered by state change
  • No need for fine-grained control
  • Want simple, declarative code