Home Flutter & Dart Tutorials

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.

Version

  • Dart 2.17, Flutter 3.0

If you want your app to stand out from the crowd, you might be surprised at what a difference just a few animations make. Not only can animations bring your app to life, but they’re also a very simple way to explain what’s happening on the screen.

Luckily, Flutter makes the process of adding animations very easy. You can use implicit animations for simple cases, or you can choose explicit animations if you need more control.

Implicit animations are really easy to use, but they only allow you to control the duration and the curve of your animations. For example, use AnimatedFoo instead of Foo and your widget will animate automatically when its values change — and Container becomes AnimatedContainer. Find the full list for ImplicitlyAnimatedWidget in the Flutter documentation.

When you need more control over the lifecycle of your animation, though, such as the ability to pause it or launch it on demand, you have to use explicit animations. They’re especially useful when there are several elements to coordinate.

In this tutorial, you’ll build a weather app called Good Night Moon Hello Sun. With the help of staggered animations, you’ll create a custom day-to-night animation using:

  • AnimationController
  • Tween
  • Tweens with Intervals
  • AnimatedBuilder and AnimatedWidget to render the animations

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial and open it in your favorite IDE.

Note: This tutorial uses Android Studio, but Visual Studio Code works fine, too. Similarly, this tutorial uses Chrome as the test device, but you could also run it on mobile.

Starter project directories and files

The code is in the lib folder, and the images are in assets/icons. Open main.dart and take a look at the home widget given to MaterialApp:

LayoutBuilder(builder: (context, constraints) {
  return HomePage(width: constraints.maxWidth);
})

HomePage uses LayoutBuilder constraints to know its size and be more responsive than with hard-coded values.

Speaking of which, open home_page.dart.

Since you’re building a weather app, you need to give it some data. SampleDataGenerator generates weather mock data using classes you’ll find in model:

  • WeatherData: Weather for a day with a list of its details.
  • WeatherDataDetails: Weather, temperature and wind at a given time of the day.

You usually change the theme of an app by changing MaterialApp‘s theme, but MaterialApp includes a default implicit animation that’s not customizable. So instead, you’ll use the Theme widget — which is not animated by default — to fully control the animation between the day and night themes.

In home_page.dart, build() only contains Theme, which depends on _isDayTheme because _content() returns the rest of the widget tree. You’ll use _animationButton() to launch the animation.

The rest of the widgets are pretty common, except for these custom ones:

  • SunWidget
  • MoonWidget
  • CloudyWidget
  • TodayDetailsWidget
  • BottomCard

Take a look at sun_widget.dart. This is your sun, which you’ll animate later. Note, again, the presence of LayoutBuilder to set its size.

Next, open moon_widget.dart. You’ll see that MoonWidget uses an image instead of just a Container.

cloudy_widget.dart shows clouds on top of SunWidget or MoonWidget, depending on the theme.

today_details_widget.dart is where you display the temperature, wind and weather of the day.

Finally, open bottom_card.dart. BottomCard displays both the hourly weather and the forecast for the next five days.

Now that you’re familiar with the project’s code, build and run.

Test switching the theme by clicking SWITCH THEMES; you’ll notice you have two different themes for your app, but no animations yet.

The weather app without animations

Animating Goodnightmoonhellosun

The transition from day to night includes several elements.

Planned animations

The scheme above illustrates the transition. First, the sun disappears by moving away. Then, the moon replaces it by moving in.

Other elements animate in the middle of this transition:

  • Theme: Changes from the day theme to the night theme with a fade in/fade out transition of its colors.
  • TodayDetailsWidget: Moves and fades at the same time.

The night-to-day transition is the same, but you swap the sun and moon while the theme transition goes from the night theme to the day theme. Some animations run one after the other, while others animate in parallel.

This is how the animation will look when you finish this tutorial:

Final animation

Using Implicit Animations

You’ll start by animating the sun and the moon during the day-to-night transition. The sun moves to the left, and the moon appears from the right.

You have several options to do this using implicitly animated widgets, such as AnimatedSlide and TweenAnimationBuilder. Here, you’ll use the latter, which needs a Tween.

Applying Tween Animations

Tween defines the starting and ending points of your animations, as its definition suggests:

Tween<T>(begin: T, end: T)

To interpolate between 0.0 and 1.0, for instance, you’d write:

Tween<double>(begin: 0.0, end: 1.0)

It works with many objects, and you can even try it with Colors!

Tween interpolates between two values using the operators +, and * in its lerp method. This means you can make Tween between custom objects if you implement these. It also means some objects can’t interpolate well since they don’t do it or their operators don’t fit. For example, the operator * in the int class returns num instead of int, which is the reason why Tween can’t interpolate it.

There are a few prebuilt Tweens for these cases. For example, you can use IntTween for int or ConstantTween, which is a Tween that stays at the same value. See the implementers of Tween for the full list.

Implementing Curve Animations

Instead of playing your animations linearly, you can apply different curves to them. Curves includes the most common ones, like easeIn and easeOut. See them animated in the docs.

There are several ways to apply a curve to your animations. One is to apply the curve directly to a Tween by calling chain() on it:

Tween<double>(begin: 0.0, end: 1.0)
    .chain(CurveTween(curve: Curves.slowMiddle));

This Tween would have a slowMiddle curve.

Animating the Sun and the Moon Implicitly

To see how implicit animations work and what their limitations are, you’ll try animating the sun and moon implicitly first.

Replace the contents of _sunOrMoon() in lib/ui/home_page.dart with the following:

return Stack(children: [
  // TweenAnimationBuilder for the sun
  TweenAnimationBuilder<Offset>(
    duration: const Duration(seconds: 2),
    curve: Curves.bounceIn,
    tween: Tween<Offset>(
        begin: const Offset(0, 0), end: const Offset(-500, 0)),
    child: const SunWidget(),
    builder: (context, offset, child) {
      return Transform.translate(offset: offset, child: child);
    },
  ),
  // TweenAnimationBuilder for the moon
  TweenAnimationBuilder<Offset>(
    duration: const Duration(seconds: 2),
    curve: Curves.bounceOut,
    tween:
        Tween<Offset>(begin: const Offset(500, 0), end: const Offset(0, 0)),
    child: const MoonWidget(),
    builder: (context, offset, child) {
      return Transform.translate(offset: offset, child: child);
    },
  )
]);

Here, you use two TweenAnimationBuilders to animate SunWidget and MoonWidget implicitly.

Your TweenAnimationBuilder has several arguments:

  • duration: Your animation duration.
  • curve: Here, you make SunWidget bounceIn and MoonWidget bounceOut.
  • tween: You used Tween to make the translation animation.
  • child: The non-moving part of your widget. In this case, the children are SunWidget and MoonWidget.
  • builder: Where you decide how to animate. Here, you used Transform.translate(). TweenAnimationBuilder calls it several times with an updated offset interpolated from your Tween.

Note that you display both the sun and the moon in a Stack because you haven’t handled the transition between the themes yet.

Hot reload to see the result.

Animation using TweenAnimationBuilder

Though you get an animation, it only animates on startup. It’s not easy to start it on a button press or launch the moon animation after the sun animation finishes. This shows that implicit animations are not the best tool here.

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

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.

Animating the TodayDetails Widget

This widget is composed of two parts: The left one displays temperature, and the right one displays wind and weather type. The animation will take part in three phases:

  • Move away from the original position and become transparent.
  • Stay transparent away for a while.
  • Move back to the original position and become visible.

In this case, you’ll need to use TweenSequence, which allows you to separate a Tween in several parts. Here’s an example:

TweenSequence<double>([
  TweenSequenceItem(tween: Tween<double>(begin: 0.0, end: 0.5), weight: 4),
  TweenSequenceItem(tween: ConstantTween<double>(0.5), weight: 2),
  TweenSequenceItem(tween: Tween<double>(begin: 0.5, end: 1.0), weight: 4),
]);

Each TweenSequenceItem has a weight used to determine its duration. For example, here you have a total weight of 10 with the following repartition:

  • Go from 0.0 to 0.5 with a weight of 4, which is 4/10 of the total TweenSequence time.
  • Stay at 0.5 with a weight of 2, which is 2/10 of the total TweenSequence time.
  • Go from 0.5 to 1.0 with a weight of 4, which is 4/10 of the total TweenSequence time.

Interpolating a Custom Object

You need to animate two properties at once: Offset for the movement and double for the opacity. One way of doing it is to make a class that implements the operators used by Tween for the interpolation. These are *, + and -.

Create a new file, fade_away.dart, in the model directory:

import 'dart:ui';

class FadeAway {
  final Offset offset;
  final double opacity;

  const FadeAway(this.offset, this.opacity);

  FadeAway operator *(double multiplier) =>
      FadeAway(offset * multiplier, opacity * multiplier);

  FadeAway operator +(FadeAway other) =>
      FadeAway(offset + other.offset, opacity + other.opacity);

  FadeAway operator -(FadeAway other) =>
      FadeAway(offset - other.offset, opacity - other.opacity);
}

FadeAway implements all the mentioned operators to be able to animate it. You simply use Offset and double operators.

Go back to home_page.dart, import FadeAway and add these new variables:

late TweenSequence<FadeAway> _temperatureAnim;
late TweenSequence<FadeAway> _weatherDetailsAnim;

You’ll use TweenSequence since three phases make up the animation.

Init the TweenSequences at the end of _initThemeAnims():

_temperatureAnim = TweenSequence<FadeAway>([
  TweenSequenceItem(
    tween: Tween<FadeAway>(
      begin: const FadeAway(Offset(0, 0), 1.0),
      end: const FadeAway(Offset(-100, 0), 0.0),
    ).chain(CurveTween(curve: Curves.easeInOut)),
    weight: 40,
  ),
  TweenSequenceItem(
    tween: ConstantTween<FadeAway>(const FadeAway(Offset(-100, 0), 0.0)),
    weight: 20,
  ),
  TweenSequenceItem(
    tween: Tween<FadeAway>(
      begin: const FadeAway(Offset(-100, 0), 0.0),
      end: const FadeAway(Offset(0, 0), 1.0),
    ).chain(CurveTween(curve: Curves.easeInOut)),
    weight: 40,
  ),
]);

_weatherDetailsAnim = TweenSequence<FadeAway>([
  TweenSequenceItem(
    tween: Tween<FadeAway>(
      begin: const FadeAway(Offset(0, 0), 1.0),
      end: const FadeAway(Offset(100, 0), 0.0),
    ).chain(CurveTween(curve: Curves.easeInOut)),
    weight: 40,
  ),
  TweenSequenceItem(
    tween: ConstantTween<FadeAway>(const FadeAway(Offset(100, 0), 0.0)),
    weight: 20,
  ),
  TweenSequenceItem(
    tween: Tween<FadeAway>(
      begin: const FadeAway(Offset(100, 0), 0.0),
      end: const FadeAway(Offset(0, 0), 1.0),
    ).chain(CurveTween(curve: Curves.easeInOut)),
    weight: 40,
  ),
]);

Each TweenSequence has a different target position: Offset(-100, 0) for _temperatureAnim and Offset(100, 0) for _temperatureAnim. They move toward it, pause with ConstantTween and finally come back to their origin.

Next, update the call to TodayDetailsWidget() with:

TodayDetailsWidget(
  weatherData: todayWeather,
  progress: _animationController,
  temperatureTween: _temperatureAnim,
  detailsTween: _weatherDetailsAnim,
)

Just like SunWidget and MoonWidget, you’ll transform TodayDetailsWidget into an AnimatedWidget. Then, you’ll use your TweenSequence and _animationController to animate it.

Update TodayDetailsWidget:

// ...
import '../model/fade_away.dart';

class TodayDetailsWidget extends AnimatedWidget {
  final WeatherData weatherData;
  final Animatable<FadeAway> temperatureTween;
  final Animatable<FadeAway> detailsTween;

  const TodayDetailsWidget({Key? key,
    required this.weatherData,
    required Animation<double> progress,
    required this.temperatureTween,
    required this.detailsTween})
      : super(key: key, listenable: progress);

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

  @override
  Widget build(BuildContext context) {
    // 1
    final temperatureCurrentValue = temperatureTween.evaluate(_animation);
    final detailsCurrentValue = detailsTween.evaluate(_animation);

    final now = DateTime.now();
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // 2
        Transform.translate(
          offset: temperatureCurrentValue.offset,
          child: Opacity(
            child: _temperature(context, now),
            opacity: temperatureCurrentValue.opacity,
          ),
        ),
        const SizedBox(
          width: 16,
        ),
        // 3
        Transform.translate(
          offset: detailsCurrentValue.offset,
          child: Opacity(
            child: _windAndWeatherText(context, now),
            opacity: detailsCurrentValue.opacity,
          ),
        ),
      ],
    );
  }

TodayDetailsWidget is a bit different from your previous AnimatedWidgets, especially in build():

  1. You use _animation‘s progress to evaluate each Tween-interpolated FadeAway object instead of directly using the _animation value in your widgets.
  2. You move and fade the temperature using temperatureCurrentValue.
  3. You do the same with detailsCurrentValue.

Also, notice that you declare temperatureTween and detailsTween as Animatable instead of TweenSequence. Both Tween and TweenSequence are Animatable, so you can use any of them without any impact on TodayDetailsWidget.

Remember that Tweens are not animated — only _animation changes over time. Here, you get their interpolated value thanks to evaluate().

Hot restart and play the animation.

Final animation

Congratulations! You’ve completed all the planned animations.

Adding More Animations

Use the next exercises to test your new skills.

Add an Animation to BottomCard

Try to animate BottomCard by letting it rotate, bounce or scale when the theme changes, for instance.

Make a Startup Animation

You might need two AnimationController for this: one for the day/night transition, one for the startup animation. In this case, don’t forget to change SingleTickerProviderStateMixin to TickerProviderStateMixin.

You may add the following in your initState():

WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
  // Do something on startup
});

It will launch at the startup of your widget, which is useful for a startup animation.

Improve CloudyWidget

Try to add some movements to the clouds, like you did for the sun and the moon. Here, consider using .repeat() instead of forward() on your AnimationController. TweenSequence is also a good fit for this animation.

Make It Rain or Snow

Since you know how to animate widgets, you can also simulate rain and snow. Make rectangles for the rain falling and white circles for the snow using Container. Then, animate them with Tween or TweenSequence that you’ll repeat().

Solution

A complete solution is available in the challenges project of this tutorial’s resources.

Build and run, then watch the animations take place. :]

Challenge animations

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

This tutorial introduced you to staggered animations. After reviewing their main components, you can now chain many animations or make them overlap each other.

Check out the tutorial Flutter Canvas API: Getting Started to make even more custom animations. It teaches how to draw custom shapes, and even animate them!

You can also achieve great results with implicit animations. Learn how to do a radial menu in Implicit Animations in Flutter: Getting Started.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Contributors

Comments

Reviews

More like this