Getting Started With Staggered Animations in Flutter

Animations in mobile apps are powerful tools to attract users’ attention. They make transitions between screens and states smoother and more appealing for the user. In this tutorial, you’ll learn how to implement animations in Flutter. By Sébastien Bel.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Using Explicit Animations

Staggered animations are animations that follow or overlap each other. You’ll use explicit animations to implement them.

AnimationController allows you to control animations. You can forward(), reverse(), repeat(), reset() and stop() animations linked to it. Check the doc for more details about AnimationController.

You must use a mixin to create instances of AnimationController. Which mixin you should use depends on the number of AnimationControllers:

  • Use a SingleTickerProviderStateMixin if you have one AnimationController.
  • Use a TickerProviderStateMixin if you have two or more AnimationControllers.

Edit _HomePageState located at lib/ui/home_page.dart:

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
// …
}

Because you’ll use only one AnimationController, you added SingleTickerProviderStateMixin.

Now, initialize AnimationController in initState() by adding the following above didChangeDependencies():

late AnimationController _animationController;

@override
void initState() {
  super.initState();
  _animationController = AnimationController(
      vsync: this, 
      duration: const Duration(milliseconds: 3000),
  );
}

With that, you added AnimationController, which is three seconds in duration. Its vsync parameter needs TickerProvider. Here’s where you use the mixin.

_animationController needs one more thing. Copy and paste the following in the same class:

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

Now, you dispose _animationController with dispose() when you don’t need it anymore.

Finally, replace the contents of _switchTheme() with the following:

_animationController.reset();
_animationController.forward();

This resets _animationController to start again from zero if it had been previously started and starts the animation again.

Now that you can control animations, you need actual animation objects!

Animating With Animation

Animation gives the current status and value of Tween interpolation, as well as letting you listen to their changes. You’ll use that to animate your widgets. You usually use AnimationControllers to control to control Animations.

Start by declaring Animation for the sun and the moon below the _animationController declaration:

late Animation<Offset> _sunMoveAnim;
late Animation<Offset> _moonMoveAnim;

Next, you need to initialize them. The simplest way is to call animate() on your Tween. Do this in a new method:

void _initThemeAnims({required bool dayToNight}) {
  _sunMoveAnim =
      Tween<Offset>(begin: const Offset(0, 0), end: const Offset(-500, 0))
          .animate(_animationController);
  _moonMoveAnim =
      Tween<Offset>(begin: const Offset(500, 0), end: const Offset(0, 0))
          .animate(_animationController);
}

animate() takes Animation as a parameter, but here you use _animationController instead. You can do this because AnimationController inherits from Animation. The newly created method takes dayToNight as an argument; you’ll get back to this in a moment.

Since you use animate using _animationController, the animation duration will be the same as _animationController‘s duration. You defined that value when you initialized your _animationController: 3,000 milliseconds.

With the above code, _animationController animates both _sunMoveAnim and _moonMoveAnim at the same time, each with its own Tween.

Note: AnimationController animation value goes from the lowest to the highest value, which are 0.0 and 1.0 if you didn’t override them. You can also use AnimationController as Animation directly instead of creating another Animation.

Next, add the following at the bottom of didChangeDependencies():

_isDayTheme = Theme.of(context).brightness == Brightness.light;
_initThemeAnims(dayToNight: _isDayTheme);

Here, you initialize the theme based on the device’s theme brightness. Then, you use it to initialize your Animations.

Animation is listenable; in other words, you can attach listeners to them. Animation notifies the status listener when its AnimationStatus changes by using addStatusListener():

exampleAnim.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    print('completed');
  }
});

This example prints a message when the Animation completes.

On the other hand, addListener() listens to all value changes. Here’s how to use it:

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

This code snippet prints the current, updated value each time exampleAnim.value changes. You could also call setState() from there to update your widgets, but that’s not the recommended way.

AnimatedBuilder

You usually use StatefulWidget and setState() to update your widget tree. Yet, AnimatedBuilder simplifies this process for Animations.

It has three parameters:

  • child: The non-moving part of the animation. AnimatedBuilder builds it once instead of rebuilding it each time the animation changes.
  • animation: Listenable you’re listening to.
  • builder: The part that changes with the animation.

Replace the contents of _sunOrMoon() with the following:

return Stack(
  children: [
    // 1
    AnimatedBuilder(
      // 2
      child: const SunWidget(),
      // 3 
      animation: _sunMoveAnim,
      // 4
      builder: (ctx, child) {
        return Transform.translate(
          // 5
          offset: _sunMoveAnim.value,
          child: child,
        );
      },
    ),
    // 6
    AnimatedBuilder(
      child: const MoonWidget(),
      animation: _moonMoveAnim,
      builder: (ctx, child) {
        return Transform.translate(
          offset: _moonMoveAnim.value,
          child: child,
        );
      },
    ),
  ],
);

Here’s what’s happening in the code above:

  1. Use an AnimatedBuilder instead of TweenAnimationBuilder.
  2. Set the non-moving part in child.
  3. Define which Animation AnimatedBuilder will listen to. Here, it’s _sunMoveAnim.
  4. Set builder to perform the actual animation. This one translates child. AnimatedBuilder calls builder each time the animation updates.
  5. Get Animation‘s current value.
  6. Do the same for the moon.

Hot restart and click SWITCH THEMES.

Animation using AnimatedBuilder without curve

You now have your first explicit animation, but it plays linearly. Unlike when you use TweenAnimationBuilder, you have to handle the curve yourself.

Replace _initThemeAnims() content with the following:

_sunMoveAnim =
    Tween<Offset>(begin: const Offset(0, 0), end: const Offset(-400, 0))
        .animate(
  CurvedAnimation(parent: _animationController, curve: Curves.easeIn),
);
_moonMoveAnim =
    Tween<Offset>(begin: const Offset(400, 0), end: const Offset(0, 0))
        .animate(
  CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
);

The _sunMoveAnim and _moonMoveAnim initializations apply easeIn and easeOut curves, respectively, thanks to CurvedAnimation.

Hot restart and click the SWITCH THEMES button.

Animation using AnimatedBuilder with curve

Since your animations run simultaneously, the sun and the moon can be visible at the same time. You’ll change that next.

Coordinating Animations With Intervals

You’ll use Interval to define when you want each animation to start and end.

Replace the contents of _initThemeAnims() with:

_sunMoveAnim =
    Tween<Offset>(begin: const Offset(0, 0), end: const Offset(-400, 0))
        .animate(
  CurvedAnimation(
    parent: _animationController,
    curve: const Interval(0, 0.5, curve: Curves.easeIn),
  ),
);
_moonMoveAnim =
    Tween<Offset>(begin: const Offset(400, 0), end: const Offset(0, 0))
        .animate(
  CurvedAnimation(
    parent: _animationController,
    curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
  ),
);

In the code above, you used Interval as CurvedAnimation curve. Note that Interval can also have a curve.

The resulting animations will play for a fraction of _animationController‘s total time. _sunMoveAnim will take the first half (0.0 to 0.5), while _moonMoveAnim will play during the second half (0.5 to 1.0). Each will take 1,500 milliseconds since _animationController is 3,000 milliseconds long.

Hot restart and launch the animation.

Animation using AnimatedBuilder with Interval

Sébastien Bel

Contributors

Sébastien Bel

Author

Vid Palčar

Tech Editor

Adriana Kutenko

Illustrator

Brian Moakley

Team Lead

Over 300 content creators. Join our team.