Back to Posts

Introduction to Flutter Animations

11 min read

Animations are a crucial part of modern mobile applications, making them more engaging and providing visual feedback to users. Flutter provides a rich set of animation tools that make it easy to create beautiful and performant animations. This guide will introduce you to the fundamentals of animations in Flutter.

Types of Animations

1. Implicit Animations

Implicit animations are the simplest way to add animations to your Flutter app. They automatically animate changes to widget properties.

class AnimatedContainerExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: Duration(seconds: 1),
      width: 200,
      height: 200,
      color: Colors.blue,
      curve: Curves.easeInOut,
      child: Center(
        child: Text('Tap to animate'),
      ),
    );
  }
}

Common implicit animation widgets:

  • AnimatedContainer
  • AnimatedOpacity
  • AnimatedPadding
  • AnimatedPositioned
  • AnimatedAlign

2. Explicit Animations

Explicit animations give you more control over the animation process using AnimationController.

class ExplicitAnimationExample extends StatefulWidget {
  @override
  _ExplicitAnimationExampleState createState() => _ExplicitAnimationExampleState();
}

class _ExplicitAnimationExampleState extends State<ExplicitAnimationExample>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    
    _animation = Tween<double>(begin: 0, end: 1).animate(_controller)
      ..addListener(() {
        setState(() {});
      });
    
    _controller.repeat(reverse: true);
  }

  @override
  Widget build(BuildContext context) {
    return Transform.scale(
      scale: _animation.value,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.red,
      ),
    );
  }

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

Animation Controllers

Animation controllers manage the animation's state and timing.

class AnimationControllerExample extends StatefulWidget {
  @override
  _AnimationControllerExampleState createState() => _AnimationControllerExampleState();
}

class _AnimationControllerExampleState extends State<AnimationControllerExample>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
    
    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    );
  }

  void _startAnimation() {
    _controller.forward();
  }

  void _reverseAnimation() {
    _controller.reverse();
  }

  void _stopAnimation() {
    _controller.stop();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        FadeTransition(
          opacity: _animation,
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            ElevatedButton(
              onPressed: _startAnimation,
              child: Text('Start'),
            ),
            ElevatedButton(
              onPressed: _reverseAnimation,
              child: Text('Reverse'),
            ),
            ElevatedButton(
              onPressed: _stopAnimation,
              child: Text('Stop'),
            ),
          ],
        ),
      ],
    );
  }
}

Animation Curves

Curves define how an animation progresses over time.

class AnimationCurvesExample extends StatelessWidget {
  final List<Curve> curves = [
    Curves.linear,
    Curves.easeIn,
    Curves.easeOut,
    Curves.easeInOut,
    Curves.bounceIn,
    Curves.bounceOut,
    Curves.elasticIn,
    Curves.elasticOut,
  ];

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: curves.length,
      itemBuilder: (context, index) {
        return TweenAnimationBuilder<double>(
          duration: Duration(seconds: 2),
          curve: curves[index],
          tween: Tween(begin: 0.0, end: 1.0),
          builder: (context, value, child) {
            return Container(
              margin: EdgeInsets.all(8),
              height: 50,
              width: value * 300,
              color: Colors.blue,
              child: Center(
                child: Text(curves[index].toString()),
              ),
            );
          },
        );
      },
    );
  }
}

Custom Animations

1. Using Tween

class CustomTweenAnimation extends StatefulWidget {
  @override
  _CustomTweenAnimationState createState() => _CustomTweenAnimationState();
}

class _CustomTweenAnimationState extends State<CustomTweenAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _sizeAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _colorAnimation = ColorTween(
      begin: Colors.red,
      end: Colors.blue,
    ).animate(_controller);

    _sizeAnimation = Tween<double>(
      begin: 50,
      end: 200,
    ).animate(_controller);

    _controller.repeat(reverse: true);
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Container(
          width: _sizeAnimation.value,
          height: _sizeAnimation.value,
          color: _colorAnimation.value,
        );
      },
    );
  }
}

2. Using CustomPainter

class CustomPainterAnimation extends StatefulWidget {
  @override
  _CustomPainterAnimationState createState() => _CustomPainterAnimationState();
}

class _CustomPainterAnimationState extends State<CustomPainterAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _animation = Tween<double>(
      begin: 0,
      end: 2 * math.pi,
    ).animate(_controller);

    _controller.repeat();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: CirclePainter(_animation.value),
      size: Size(200, 200),
    );
  }
}

class CirclePainter extends CustomPainter {
  final double angle;

  CirclePainter(this.angle);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 4;

    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 3;

    canvas.drawCircle(center, radius, paint);

    final x = center.dx + radius * math.cos(angle);
    final y = center.dy + radius * math.sin(angle);
    canvas.drawCircle(Offset(x, y), 10, paint);
  }

  @override
  bool shouldRepaint(CirclePainter oldDelegate) => oldDelegate.angle != angle;
}

Best Practices

1. Performance Optimization

class OptimizedAnimation extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: AnimatedContainer(
        duration: Duration(milliseconds: 300),
        // Use const constructor for child widgets
        child: const Text('Optimized Animation'),
      ),
    );
  }
}

2. Memory Management

class MemoryManagedAnimation extends StatefulWidget {
  @override
  _MemoryManagedAnimationState createState() => _MemoryManagedAnimationState();
}

class _MemoryManagedAnimationState extends State<MemoryManagedAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );
  }

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

Common Animation Patterns

1. Page Transitions

class CustomPageRoute extends PageRouteBuilder {
  final Widget page;

  CustomPageRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return FadeTransition(
              opacity: animation,
              child: child,
            );
          },
        );
}

2. Hero Animations

class HeroAnimationExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => DetailPage(),
          ),
        );
      },
      child: Hero(
        tag: 'imageHero',
        child: Image.network('https://example.com/image.jpg'),
      ),
    );
  }
}

Conclusion

Flutter provides a powerful and flexible animation system that allows you to:

  • Create simple implicit animations
  • Build complex explicit animations
  • Customize animation curves
  • Optimize performance
  • Manage memory efficiently

Remember to:

  • Choose the right type of animation for your use case
  • Use animation controllers responsibly
  • Dispose of controllers when they're no longer needed
  • Optimize animations for performance
  • Test animations on different devices

With these fundamentals, you can create engaging and performant animations in your Flutter applications!