Using AnimationController for Fine-Grained Control in Flutter

In Flutter, animations add a layer of polish and engagement to your user interface. Flutter’s animation framework is highly flexible and allows developers to create simple to complex animations. One of the fundamental classes in Flutter’s animation framework is AnimationController. Understanding how to use AnimationController provides you with fine-grained control over your animations, enabling you to build customized and interactive experiences. This blog post dives deep into the usage of AnimationController, including its properties, methods, and best practices.

What is AnimationController?

The AnimationController is a special type of Animation that manages the animation’s progression. It allows you to start, stop, reverse, and reset animations. Essentially, it drives the animation forward (or backward) and is crucial for managing the state of the animation. The AnimationController linearly produces values that range from 0.0 to 1.0, during a given duration. This value can be used to drive various visual properties of your widgets, such as size, opacity, position, etc.

Why Use AnimationController?

  • Fine-Grained Control: Provides precise control over animation behavior, including start, stop, reverse, and reset functionalities.
  • Custom Animations: Enables the creation of complex, tailored animations beyond simple transitions.
  • State Management: Effectively manages the state and progression of an animation, making it predictable and manageable.
  • Integration with Tweens: Works seamlessly with Tween objects to define specific value changes over time.

How to Use AnimationController

Step 1: Initialize the AnimationController

First, you need to create an instance of AnimationController within a StatefulWidget, typically in the initState method. It’s essential to dispose of the controller when the widget is no longer needed to prevent memory leaks. Therefore, the recommended approach is to manage the controller in StatefulWidget. The AnimationController requires a vsync parameter, which is typically the this reference if your State class includes the TickerProviderStateMixin or SingleTickerProviderStateMixin.


import 'package:flutter/material.dart';

class MyAnimatedWidget extends StatefulWidget {
  const MyAnimatedWidget({Key? key}) : super(key: key);

  @override
  _MyAnimatedWidgetState createState() => _MyAnimatedWidgetState();
}

class _MyAnimatedWidgetState extends State
    with SingleTickerProviderStateMixin { // Or TickerProviderStateMixin
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2), // Animation duration
      vsync: this, // The ticker provider
    );
  }

  @override
  void dispose() {
    _controller.dispose(); // Dispose of the controller
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(); // Placeholder, to be replaced
  }
}

Step 2: Using Tweens

AnimationController provides a value from 0.0 to 1.0. To animate a specific property between a specific range, you’ll use a Tween. A Tween defines the beginning and ending values of an animation.


late Animation _animation;

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    duration: const Duration(seconds: 2),
    vsync: this,
  );
  _animation = Tween(begin: 100.0, end: 200.0).animate(_controller)
    ..addListener(() {
      setState(() {
        // The state that has changed here is our "size" - managed by animation value
      });
    });

  _controller.forward(); // Start the animation
}

In the example above:

  • We created a Tween that transitions a value from 100.0 to 200.0.
  • We created an Animation<double> called _animation and attached our animation controller to our tween’s animate() function
  • Added a listener to the Animation so it will redraw as the state has changed. This is extremely important.
  • Then call _controller.forward() to start the animation!

Step 3: Building the Animated Widget

Now you can use the _animation.value in your widget to create an animation. In this example, the height and width are being animated by using animation controller.


import 'package:flutter/material.dart';

class MyAnimatedWidget extends StatefulWidget {
  const MyAnimatedWidget({Key? key}) : super(key: key);

  @override
  _MyAnimatedWidgetState createState() => _MyAnimatedWidgetState();
}

class _MyAnimatedWidgetState extends State
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _animation = Tween(begin: 100.0, end: 200.0).animate(_controller)
      ..addListener(() {
        setState(() {
          // The state that has changed here is our "size" - managed by animation value
        });
      });

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        height: _animation.value,
        width: _animation.value,
        color: Colors.blue,
      ),
    );
  }
}

Step 4: Controlling the Animation

AnimationController comes with useful functions to control your animation such as:

  • forward(): Starts the animation from the beginning.
  • reverse(): Plays the animation backward from the end.
  • repeat(): Repeats the animation a specified number of times or indefinitely.
  • stop(): Stops the animation at its current value.
  • reset(): Resets the animation to its initial value (0.0).

ElevatedButton(
  onPressed: () {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  },
  child: const Text('Animate'),
)

The example above starts the animation or reverses it if it’s already completed.

Advanced Techniques

1. Chained Animations

Animations can be chained together to create complex sequences. This involves listening for the completion of one animation and then starting another.


_controller.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    // Start the next animation or action
    _controller.reverse();
  } else if (status == AnimationStatus.dismissed) {
    _controller.forward();
  }
});

2. Curved Animations

You can apply a curve to the animation to make it look more natural or dramatic. Flutter provides several predefined curves like Curves.easeIn, Curves.easeInOut, etc.


_animation = Tween(begin: 100.0, end: 200.0).animate(
  CurvedAnimation(
    parent: _controller,
    curve: Curves.easeInOut,
  ),
);

3. Custom Tween

Flutter also support to use Custom Tween by extending Tween<T> class. Here an example for creating Custom Color Tween.


class ColorTween extends Tween {
  ColorTween({Color? begin, Color? end}) : super(begin: begin, end: end);

  @override
  Color? lerp(double t) {
    if (begin == null || end == null) {
      return begin ?? end;
    }
    return Color.lerp(begin, end, t);
  }
}

//Usage
late Animation _colorAnimation;
@override
void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller);
     ..addListener(() {
        setState(() {
          // The state that has changed here is our "color" - managed by animation value
        });
      });
}
//Widget
Container(
  height: _animation.value,
  width: _animation.value,
  color: _colorAnimation.value,
),

Best Practices

  • Dispose Controller: Always dispose of the AnimationController in the dispose method to prevent memory leaks.
  • Use Curves: Incorporate curves to make animations look more natural.
  • Optimize Performance: Be mindful of the performance impact of animations, especially complex ones.
  • Use AnimatedWidget or AnimatedBuilder: For complex animations, use AnimatedWidget or AnimatedBuilder to optimize the rebuild process.
  • Combine with Hero: If you want to animate a shared Widget between 2 different screens, the easiest option is by using Hero Widget.

Conclusion

The AnimationController is a pivotal component of Flutter’s animation framework, granting you the ability to manage and fine-tune animations with precision. By understanding and utilizing the various functionalities of AnimationController—such as controlling animation state, chaining animations, and incorporating curves—you can craft visually appealing and highly engaging user interfaces. Properly implemented animations elevate the user experience, making your Flutter applications stand out.