How To Use Tween | Learn All About Flutter Animations – Part 2

Sandro Maglione

Sandro Maglione

Animations

In this tutorial series, we are going to learn all about Flutter Animations, starting from what is and how to use an AnimationController, understanding how Tween and Curves work in Flutter, then moving to improve our code with AnimatedBuilder, and finishing by integrating Flutter packages to simplify and reduce the boilerplate in our code (simple_animations and animator).

In the previous article, AnimationController and setState | Learn All About Flutter Animations – Part 1, we learned what Animations really are in Flutter and how they work. We created our first animation by using a StatefulWidget and an AnimationController.

We then completed our setup by adding a listener to the animation controller which calls setState() to rebuild the UI and run the animation. I really suggest you to check out the first part of the series if you haven't because we are going to continue building new animations based on that foundation.

In this part 2 of the Flutter animations series, we are going to learn how to use the Tween widget to further customize our animations.


What are the limitations of using only an AnimationController?

In the previous article, we completed our first animation using an AnimationController and calling setState() on its listener.

import 'package:flutter/material.dart';
 
class FlutterAnimations extends StatefulWidget {
  @override
  _FlutterAnimationsState createState() => _FlutterAnimationsState();
}
 
class _FlutterAnimationsState extends State<FlutterAnimations>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
 
  @override
  void initState() {
    super.initState();
 
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(
        milliseconds: 4000,
      ),
    );
 
    _controller.addListener(() {
      setState(() {});
    });
 
    _controller.forward();
  }
 
  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Opacity(
          opacity: _controller.value,
          child: Container(
            height: 150,
            width: 150,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }
}

This code creates a fadeIn animation on the Container. The _controller value goes from 0.0 to 1.0 in 4000 milliseconds (4 seconds) using the default linear curve.

The first limitation of using only an AnimationController is that its value will always be only between 0.0 and 1.0. That was exactly what we needed in the example above because the Opacity range itself goes from 0.0 to 1.0. But what happens when we need to move the container from 0.0 to 100.0? (Hint: Use a Tween!)

The second limitation is that, by just adopting the code above, the animation uses the default Curves.linear of Flutter. We would like to customize the curve to make it look and feel better.

Learn How to use Tweens

The initial build setup contains a centered Transform.translate widget with our basic blue Container as a child.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Transform.translate(
        offset: Offset(0, 0),
        child: Container(
          height: 50,
          width: 50,
          color: Colors.blue,
        ),
      ),
    ),
  );
}
Tween animation tutorial, initial build container

In our new example, we would like to move our container by 100 pixels to the bottom using a bounceIn animation. In order to change the container's position, we are going to use Transform.translate, which takes an Offset that defines how much to move the widget on the X and Y axes.

We introduce a new Animation<double> variable in our StatefulWidget.

class _FlutterAnimationsState extends State<FlutterAnimations>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;
 
  @override
  void initState() {
    super.initState();
    ...

We use the _animation variable to change the range of the _controller from 0.0-1.0 to 0.0-100.0 using a Tween. Inside initState(), we assign a new Tween<double> to the _animation variable. The tween requires two parameters: a begin double, the starting value of our new animation (0.0 in our example), and a end double, the ending value of the animation (100.0 in our example).

Note: We are not limited to use double values, check out the Tween API for more details and examples.

In order to assign the _animation variable, we need to call animate() on our newly created tween, passing the controller to it. By doing so, we chain together the tween with our previously initialized _controller.

_animation = Tween<double>(
  begin: 0.0,
  end: 100.0,
).animate(_controller);

Finally, we add the listener with the setState() call to the _animation variable and we start the animation using the controller (which still controls the animation status, it is a controller after all) by calling _controller.forward().

@override
void initState() {
  super.initState();
 
  _controller = AnimationController(
    vsync: this,
    duration: const Duration(
      milliseconds: 4000,
    ),
  );
 
  _animation = Tween<double>(
    begin: 0.0,
    end: 100.0,
  ).animate(_controller);
  
  _animation.addListener(() {
      setState(() {});
    });
 
  _controller.forward();
}

Using the tween value

The last step to start the animation is to use its value inside the build function. Since we want to move the container to the bottom, we assign the Y value of the Offset widget to _animation.value.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Transform.translate(
        offset: Offset(0, _animation.value),
        child: Container(
          height: 50,
          width: 50,
          color: Colors.blue,
        ),
      ),
    ),
  );
}

We are done! Run the app and you will admire our small container moving slowly by 100 pixels to the bottom. You just learned how to use the Tween, making another huge step into the world of Flutter Animations!

How the Tween works

Wait, what just happened? We chained together the tween with the controller and magically we transformed the range of our animation to 0.0-100.0 (begin-end value of the tween). Kind of.

What the tween actually does is mapping the 0.0-1.0 default range of the controller to another pair of values. If we print side by side the controller value (_controller.value) and the animation value (_animation.value), the situation will start to look much more clear.

I/flutter (19705): Controller: 0.0 -> Tween: 0.0
I/flutter (19705): Controller: 0.0 -> Tween: 0.0
I/flutter (19705): Controller: 0.049169333333333336 -> Tween: 4.916933333333334
I/flutter (19705): Controller: 0.09832666666666667 -> Tween: 9.832666666666668
I/flutter (19705): Controller: 0.122884 -> Tween: 12.2884
I/flutter (19705): Controller: 0.14606133333333335 -> Tween: 14.606133333333334
I/flutter (19705): Controller: 0.19394533333333333 -> Tween: 19.39453333333333
I/flutter (19705): Controller: 0.21852133333333335 -> Tween: 21.852133333333335
I/flutter (19705): Controller: 0.29272266666666663 -> Tween: 29.272266666666663
I/flutter (19705): Controller: 0.341628 -> Tween: 34.1628
I/flutter (19705): Controller: 0.39036133333333334 -> Tween: 39.03613333333333
I/flutter (19705): Controller: 0.4414186666666667 -> Tween: 44.141866666666665
I/flutter (19705): Controller: 0.46575066666666665 -> Tween: 46.575066666666665
I/flutter (19705): Controller: 0.49123333333333336 -> Tween: 49.123333333333335
I/flutter (19705): Controller: 0.5414826666666667 -> Tween: 54.148266666666665
I/flutter (19705): Controller: 0.565644 -> Tween: 56.564400000000006
I/flutter (19705): Controller: 0.611996 -> Tween: 61.1996
I/flutter (19705): Controller: 0.6367746666666666 -> Tween: 63.67746666666666
I/flutter (19705): Controller: 0.685584 -> Tween: 68.55839999999999
I/flutter (19705): Controller: 0.7116373333333333 -> Tween: 71.16373333333334
I/flutter (19705): Controller: 0.7824026666666667 -> Tween: 78.24026666666667
I/flutter (19705): Controller: 0.8300226666666667 -> Tween: 83.00226666666667
I/flutter (19705): Controller: 0.8834746666666667 -> Tween: 88.34746666666668
I/flutter (19705): Controller: 0.93194 -> Tween: 93.194
I/flutter (19705): Controller: 0.9798346666666666 -> Tween: 97.98346666666666
I/flutter (19705): Controller: 1.0 -> Tween: 100.0

As you can see, in this simple example the tween simply multiplies x100 the value of the controller. In fact, we could achieve the same result by using the controller value and multiplying it x100 manually.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: Transform.translate(
        offset: Offset(0, _controller.value * 100),
        child: Container(
          height: 50,
          width: 50,
          color: Colors.blue,
        ),
      ),
    ),
  );
}

This solution here returns the same exact result as the example above using a Tween<double>

Why using a Tween then? The tween allows us to have more control over the animation value (for example when using curves). It is also much more useful when our animation does not contain a simple double variable. If, for example, we use a ColorTween to animate the color of our container, changing the color value manually would become much more of a hassle.

import 'package:flutter/material.dart';
 
class FlutterAnimations extends StatefulWidget {
  @override
  _FlutterAnimationsState createState() => _FlutterAnimationsState();
}
 
class _FlutterAnimationsState extends State<FlutterAnimations>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<Color> _animation;
 
  @override
  void initState() {
    super.initState();
 
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(
        milliseconds: 4000,
      ),
    );
 
    _animation = ColorTween(
      begin: Colors.red,
      end: Colors.yellowAccent,
    ).animate(_controller);
 
    _animation.addListener(() {
      setState(() {});
    });
 
    _controller.forward();
  }
 
  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          height: 50,
          width: 50,
          color: _animation.value,
        ),
      ),
    );
  }
}
I/flutter (19705): Controller: 0.0 -> Tween: MaterialColor(primary value: Color(0xfff44336))
I/flutter (19705): Controller: 0.0 -> Tween: MaterialColor(primary value: Color(0xfff44336))
I/flutter (19705): Controller: 0.075152 -> Tween: Color(0xfff45131)
I/flutter (19705): Controller: 0.10017866666666668 -> Tween: Color(0xfff55530)
I/flutter (19705): Controller: 0.125228 -> Tween: Color(0xfff55a2f)
I/flutter (19705): Controller: 0.15027333333333334 -> Tween: Color(0xfff55f2d)
I/flutter (19705): Controller: 0.18494933333333333 -> Tween: Color(0xfff6652c)
I/flutter (19705): Controller: 0.20900533333333335 -> Tween: Color(0xfff66a2a)
I/flutter (19705): Controller: 0.280632 -> Tween: Color(0xfff77726)
I/flutter (19705): Controller: 0.305164 -> Tween: Color(0xfff77c25)
I/flutter (19705): Controller: 0.32914933333333335 -> Tween: Color(0xfff78024)
I/flutter (19705): Controller: 0.3531666666666667 -> Tween: Color(0xfff78522)
I/flutter (19705): Controller: 0.377164 -> Tween: Color(0xfff88921)
I/flutter (19705): Controller: 0.41244400000000003 -> Tween: Color(0xfff8901f)
I/flutter (19705): Controller: 0.4364733333333333 -> Tween: Color(0xfff8951e)
I/flutter (19705): Controller: 0.4605626666666667 -> Tween: Color(0xfff9991d)
I/flutter (19705): Controller: 0.485776 -> Tween: Color(0xfff99e1b)
I/flutter (19705): Controller: 0.5110693333333333 -> Tween: Color(0xfff9a31a)
I/flutter (19705): Controller: 0.5368506666666667 -> Tween: Color(0xfff9a719)
I/flutter (19705): Controller: 0.5617253333333333 -> Tween: Color(0xfffaac17)
I/flutter (19705): Controller: 0.6093333333333334 -> Tween: Color(0xfffab515)
I/flutter (19705): Controller: 0.6332826666666667 -> Tween: Color(0xfffaba13)
I/flutter (19705): Controller: 0.6571813333333333 -> Tween: Color(0xfffbbe12)
I/flutter (19705): Controller: 0.6813106666666666 -> Tween: Color(0xfffbc311)
I/flutter (19705): Controller: 0.70508 -> Tween: Color(0xfffbc70f)
I/flutter (19705): Controller: 0.7290293333333334 -> Tween: Color(0xfffccc0e)
I/flutter (19705): Controller: 0.7610373333333333 -> Tween: Color(0xfffcd20c)
I/flutter (19705): Controller: 0.7849693333333333 -> Tween: Color(0xfffcd60b)
I/flutter (19705): Controller: 0.8088173333333333 -> Tween: Color(0xfffcdb0a)
I/flutter (19705): Controller: 0.8328426666666666 -> Tween: Color(0xfffddf09)
I/flutter (19705): Controller: 0.8581426666666667 -> Tween: Color(0xfffde407)
I/flutter (19705): Controller: 0.8813080000000001 -> Tween: Color(0xfffde806)
I/flutter (19705): Controller: 0.904616 -> Tween: Color(0xfffded05)
I/flutter (19705): Controller: 0.9285800000000001 -> Tween: Color(0xfffef103)
I/flutter (19705): Controller: 0.9528706666666666 -> Tween: Color(0xfffef602)
I/flutter (19705): Controller: 0.9751759999999999 -> Tween: Color(0xfffefa01)
I/flutter (19705): Controller: 0.9989586666666667 -> Tween: Color(0xfffefe00)
I/flutter (19705): Controller: 1.0 -> Tween: MaterialAccentColor(primary value: Color(0xffffff00))

There exist many different types of pre-build tweens in Flutter, like SizeTween, TextStyleTween, EdgeInsetsTween (check out the Tween API documentation to learn about all the tween implementations available).

Here below another example using TextStyleTween. It is super easy to use once you understand the concept explained in the tutorial, and you now have unlimited power and potential in your hands to build mind-blowing Flutter animations (more power still to come in the series, but we reached a really great point!).

import 'package:flutter/material.dart';
 
class FlutterAnimations extends StatefulWidget {
  @override
  _FlutterAnimationsState createState() => _FlutterAnimationsState();
}
 
class _FlutterAnimationsState extends State<FlutterAnimations>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<TextStyle> _animation;
 
  @override
  void initState() {
    super.initState();
 
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(
        milliseconds: 4000,
      ),
    );
 
    _animation = TextStyleTween(
      begin: TextStyle(
        fontSize: 20.0,
        color: Colors.black,
        fontWeight: FontWeight.w300,
      ),
      end: TextStyle(
        fontSize: 40.0,
        color: Colors.blue,
        fontWeight: FontWeight.w900,
      ),
    ).animate(_controller);
 
    _animation.addListener(() {
      setState(() {});
    });
 
    _controller.forward();
  }
 
  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          'Animations!',
          style: _animation.value,
        ),
      ),
    );
  }
}

We learned a lot about Tween in this tutorial. Now it's your turn to experiment using Tweens of all forms and shapes to build creative animations in your Flutter app. The tutorial is already full of new concepts, so I leave Curves for the next part (stay tuned!).

Thanks for reading.

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.