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 3 of 4 of this article. Click here to view the first page.

Introducing AnimatedWidgets

Adding many AnimatedBuilders to your build() can make things seem a bit messy. Instead, you can use AnimatedWidget to separate the different parts of your UI.

Make the following changes to SunWidget:

// 1
class SunWidget extends AnimatedWidget {
  // 2
  const SunWidget({Key? key, required Animation<Offset> listenable})
      : super(key: key, listenable: listenable);

  // 3
  Animation<Offset> get _animation => listenable as Animation<Offset>;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      final minSize = min(constraints.maxWidth, constraints.maxHeight);
      final sunSize = minSize / 2.0;

      return Transform.translate(
        // 4
        offset: _animation.value,
        child: Container(
          width: sunSize,
          height: sunSize,
          decoration: BoxDecoration(
              // …
              ),
        ),
      );
    });
  }
}

In the code above, you:

  1. Extend AnimatedWidget instead of StatelessWidget.
  2. Give super constructor your Listenable (Animation).
  3. Cast Listenable as Animation for later use.
  4. Use the current animation value in your build() method. It triggers each time Listenable updates.

Now, do the same for MoonWidget:

class MoonWidget extends AnimatedWidget {
  const MoonWidget({Key? key, required Animation<Offset> listenable})
      : super(key: key, listenable: listenable);

  Animation<Offset> get _animation => listenable as Animation<Offset>;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      final minSize = min(constraints.maxWidth, constraints.maxHeight);
      final moonSize = minSize / 2.0;
      return Transform.translate(
        offset: _animation.value,
        child: Container(
          // Rest of MoonWidget
        ),
      );
    });
  }
}

MoonWidget works in the same way as SunWidget, but the Transform.translate child changes.

Finally, replace _sunOrMoon() in home_page.dart with the following:

return Stack(
  children: [
    SunWidget(listenable: _sunMoveAnim),
    MoonWidget(listenable: _moonMoveAnim),
  ],
);

Instead of using AnimatedBuilders, you directly use your AnimatedWidgets.

Hot reload and launch the animation.

Animation using AnimatedBuilder with Interval

The animation looks the same, but it’s different below the hood. :]

Unlike AnimatedBuilder, AnimatedWidget doesn’t have a child property to optimize its build(). However, you could add it manually by adding an extra child property, as in this example class:

class AnimatedTranslateWidget extends AnimatedWidget {
  const AnimatedTranslateWidget(
      {Key? key,
      required Animation<Offset> translateAnim,
      required Widget child})
      : _child = child,
        super(key: key, listenable: translateAnim);

  // Child optimization
  final Widget _child;

  Animation<Offset> get animation => listenable as Animation<Offset>;

  @override
  Widget build(BuildContext context) {
    return Transform.translate(
      offset: animation.value,
      child: _child,
    );
  }
}

The resulting class is an animated version of Transform.translate(), just like SlideTransition. You use it with your non-animated widgets as argument.

There’s a full list of widgets that have an AnimatedWidget version. You name them according to the format FooTransition, where Foo is the animation’s name. You can use them and still control your animations because you pass them Animation objects.

Implementing AnimatedWidget

If the animation you want to achieve is not very heavy and it’s OK for you to skip child optimization, you may make your widget implement AnimatedWidget. Here, you can use the animated versions of SunWidget and MoonWidget without it, for instance.

However, test it on your low-end target devices to be sure it runs well before putting it in production.

You may also want to achieve a special kind of animation that doesn’t already exist in the framework. In this case, you could create a widget implementing AnimatedWidget to animate the components of your app. For instance, you could combine a fade effect with a translate effect and create a SlideAndFadeTransition widget.

Animating Daytime and Nighttime Transition

Now that you know the theory, you can apply it to make your day/night transition! You already have animations for the sun and the moon, but they don’t depend on the current theme: The sun will always leave and the moon will always enter.

Start by updating _initThemeAnims() to change that:

void _initThemeAnims({required bool dayToNight}) {
  final disappearAnim =
      Tween<Offset>(begin: const Offset(0, 0), end: Offset(-widget.width, 0))
          .animate(CurvedAnimation(
    parent: _animationController,
    curve: const Interval(
      0.0,
      0.3,
      curve: Curves.ease,
    ),
  ));

  final appearAnim =
      Tween<Offset>(begin: Offset(widget.width, 0), end: const Offset(0, 0))
          .animate(CurvedAnimation(
    parent: _animationController,
    curve: const Interval(
      0.7,
      1.0,
      curve: Curves.ease,
    ),
  ));

  _sunMoveAnim = dayToNight ? disappearAnim : appearAnim;
  _moonMoveAnim = dayToNight ? appearAnim : disappearAnim;
}

Instead of using raw values for the offset, you used widget.width here to make sure you have a good animation, no matter what the screen size is. Also, instead of directly defining the sun and moon animations, you set appearAnim and disappearAnim. Then, you assign them to _sunMoveAnim and _moonMoveAnim depending on the animation you need to perform — day to night or night to day.

Note: The Intervals don’t follow each other because you’ll add more animations between them.

Next, replace the contents of _sunOrMoon() with this code:

if (_isDayTheme) {
  return SunWidget(listenable: _sunMoveAnim);
} else {
  return MoonWidget(listenable: _moonMoveAnim);
}

You return only one widget, depending on the current theme.

Now, you need to be able to change the theme. You’ll listen to _animationController for this.

Update switchTheme() and add the necessary methods below:

void _switchTheme() {
  // 1
  if (_isDayTheme) {
    _animationController.removeListener(_nightToDayAnimListener);
    _animationController.addListener(_dayToNightAnimListener);
  } else {
    _animationController.removeListener(_dayToNightAnimListener);
    _animationController.addListener(_nightToDayAnimListener);
  }
  // 2
  _initThemeAnims(dayToNight: _isDayTheme);
  // 3
  setState(() {
    _animationController.reset();
    _animationController.forward();
  });
}

void _dayToNightAnimListener() {
  _animListener(true);
}

void _nightToDayAnimListener() {
  _animListener(false);
}

void _animListener(bool dayToNight) {
  // 4
  if ((_isDayTheme && dayToNight || !_isDayTheme && !dayToNight) &&
      _animationController.value >= 0.5) {
    setState(() {
      _isDayTheme = !dayToNight;
    });
  }
}

Here’s what’s happening above:

  1. Remove the previous listener before adding the new one.
  2. Init again Animation objects with the new _isDayTheme setting.
  3. Refresh state with new Animation objects, then launch the animation from the start.
  4. In the listener, eventually update _isDayTheme based on the current animation value.

Hot reload and click SWITCH THEMES.

Sun and moon animation

Animating the Theme

You can animate the theme transition as well. Start by declaring the following Animation below _moonMoveAnim:

late Animation<ThemeData> _themeAnim;

Next, init it at the end of _initThemeAnims():

_themeAnim = (dayToNight
        ? ThemeDataTween(begin: _dayTheme, end: _nightTheme)
        : ThemeDataTween(begin: _nightTheme, end: _dayTheme))
    .animate(
  CurvedAnimation(
    parent: _animationController,
    curve: const Interval(
      0.3,
      0.7,
      curve: Curves.easeIn,
    ),
  ),
);

The code above interpolates between two ThemeDatas with ThemeDataTweens. It’s another example of objects that need a dedicated Tween class.

Finally, replace the contents of build():

return AnimatedBuilder(
  animation: _themeAnim,
  child: _content(),
  builder: (context, child) {
    return Theme(
      data: _themeAnim.value,
      child: Builder(
        builder: (BuildContext otherContext) {
          return child!;
        },
      ),
    );
  },
);

AnimatedBuilder updates the Theme of your HomePage based on _themeAnim‘s value.

Hot restart and launch the animation.

Sun, moon and theme animation

Now, Theme‘s colors change progressively thanks to the animation.

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.