Skip to main content

Command Palette

Search for a command to run...

Writing Implicitly Animated Widgets in Flutter

Updated
7 min read
Writing Implicitly Animated Widgets in Flutter

There are many ways to create animations in Flutter. Most of these involve creating an Animation and/or AnimationController and explicitly calling forward/reverse. However, a lot of the time what you want is to be able to provide a state and have the widget figure out how to animate to that state by itself. This is termed an implicit animation in the flutter framework, although that term is misused a fair amount even within their own documentation.

A great example is AnimatedOpacity. This widget animates based on the value passed for opacity, and then fades its child in or out.

(Video showing flutter logo animating in and out)

The code for using this is as follows:

AnimatedOpacity(
  opacity: opacityLevel,
  duration: const Duration(seconds: 3),
  child: const FlutterLogo(),
)

All that needs doing to change the opacity is to pass in a different opacityLevel - no need to know how animations, tweens, or any other advanced concepts work.

This is extremely useful if you want to do something provided by flutter - examples include AnimatedOpacity, AnimatedPositioned, AnimatedSize . There are even more multipurpose widgets such as AnimatedCrossFade and AnimatedWidget that will transition between two widgets or any widget passed in respectively. But what if you want to do something else?


This post is going to assume some knowledge of how the flutter animation framework works, or at least animations in general. Without that knowledge you should be able to get things to work anyways, but it's worth knowing what a Tween and Animation are and the difference between them. I'm not going to explain here as that's already covered in their documentation, but give that a read before continuing if you're unsure.

Writing your ImplicitlyAnimatedWidget subclass

The first thing you'll want to do is create new classes subclassing ImplicitlyAnimatedWidgetand ImplicitlyAnimatedWidgetState. For this example, we're going to create a widget that flips its child vertically.

class AnimatedFlipY extends ImplicitlyAnimatedWidget {
  const AnimatedFlipY({
    super.key,
    required this.child,
    required this.flipped,
    required super.duration,
  });

  final Widget child;
  final bool flipped;

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

class _AnimatedFlipYState extends ImplicitlyAnimatedWidgetState<AnimatedFlipY> {
  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {}

  @override
  void didUpdateTweens() {}

  @override
  Widget build(BuildContext context) {}
}

In the above example, you'll notice that the AnimatedFlipY class overrides the createState method to create an instance of _AnimatedFlipYState, which implements ImplicitlyAnimatedWidgetState<AnimatedFlipY> . If you've written a StatefulWidget subclass, this will be very familiar, but ImplicitlyAnimatedWidgetState introduces forEachTween which needs to be implemented in addition to the build method. We'll also need to override didUpdateTweens.

In the forEachTween method, we need to use the visitor to create a tween object which we will use to perform the animation. This is the implementation:

Tween<double>? _flipY;

@override
void forEachTween(TweenVisitor<dynamic> visitor) {
  _flipY = visitor(
    _flipY,
    widget.flipped ? 1.0 : 0.0,
    (dynamic value) => Tween<double>(begin: value as double),
  ) as Tween<double>;
}

What we're doing here is calling the TweenVisitor with the current tween, the value we want to animate towards, and a builder function which creates a new tween given the input value. The result of this is a tween with the current value as the beginning and the target value (1.0 or 0.0 depending on whether we want the object to be flipped or not) as the end. There's a couple peculiarities in this code that should be mentioned - TweenVisitor's type is dynamic because the Tween we're creating can be of any type, not just double, and might even be used for multiple types; and we have to use an optional for _flipY because it is null the first time this method is called.

Next let's look at the didUpdateTweens method. This gets called after the object is first constructed and after a change to the parameters passed to the AnimatedFlipY widget.

late Animation<double> _flipAnimation;

@override
void didUpdateTweens() {
  _flipAnimation = animation.drive(_flipY!);
}

In this case, we can use a late modifier rather than optional for _flipAnimation as it will never be accessed until after it is set. The animation object is something managed by the ImplicitlyAnimatedWidgetState, and by calling drive on it with _flipY we are creating a new animation that will animated to the correct values.

If we simply used the animation object, it would always animate from 0.0 to 1.0 as that is all the superclass knows about, which is not what we want - let's say that our animation is at 0.6 and then we set flipped to false - we then want our animation to run from 0.6 to 0.0. By creating the new Animation object and assigning it to _flipAnimation we are able to control the output of our animation.

Now take a look at the build method of our class:

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _flipAnimation,
    builder: (context, child) {
      final flipMatrix = Matrix4.rotationX(_flipAnimation.value * pi);
      return Transform(
        transform: flipMatrix,
        alignment: Alignment.center,
        child: child,
      );
    },
    child: widget.child,
  );
}

Whew, that's a big one. Let's take a look at what is going on part by part:

AnimatedBuilder(
  animation: _flipAnimation,
  builder: (context, child) {
    ...
  },
  child: widget.child,
);

This or something like it will need to be done for any implementation of an implicitly animated widget so that the animation is actually listened to. Another way of doing it will be shown at the end of this post.

We are using an AnimatedBuilder and passing the _flipAnimation animation to it. The AnimatedBuilder will listen to the animation and call the builder function every time the animation is changed; this means that the amount of work done in the builder function should be minimized as it is performed many times. If there are any static elements to the animation you're creating, those should be passed into the child parameter - in our case, the widget to be flipped. This child will then be passed into the builder function to be used there.

Finally, here is the code for doing the actual flip:

    final flipMatrix = Matrix4.rotationX(_flipAnimation.value * pi);
    return Transform(
    transform: flipMatrix,
      origin: Offset(0, flipAxis),
      alignment: Alignment.center,
      child: child,
    );

This uses a matrix calculation to calculate the amount of rotation around X axis and sets the alignment of the transform to the center so that the axis of rotation is in the middle of the object.

All of this results in the following:

class AnimatedFlipY extends ImplicitlyAnimatedWidget {
  const AnimatedFlipY({
    super.key,
    required this.child,
    required this.flipped,
    required super.duration,
  });

  final Widget child;
  final bool flipped;

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

class _AnimatedFlipYState extends ImplicitlyAnimatedWidgetState<AnimatedFlipY> {
  Tween<double>? _flipY;

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _flipY =
        visitor(
              _flipY,
              widget.flipped ? 1.0 : 0.0,
              (dynamic value) => Tween<double>(begin: value as double),
            )
            as Tween<double>;
  }

  late Animation<double> _flipAnimation;

  @override
  void didUpdateTweens() {
    _flipAnimation = animation.drive(_flipY!);
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _flipAnimation,
      builder: (context, child) {
        final flipMatrix = Matrix4.rotationX(_flipAnimation.value * pi);
        return Transform(
          transform: flipMatrix,
          alignment: Alignment.center,
          child: child,
        );
      },
      child: widget.child,
    );
  }
}

With that, you should be able to write your own implicitly animated widgets!

See in dartpad here.


Bonus:

I mentioned that there is an alternative way to use the animation. If you're going to be using the vertical flip (or whatever you're writing) in other places, you might want to make this into its own widget. That way you can re-use the logic elsewhere, like in a page transition for example. Yay for reusability!

The convention within the flutter codebase seems to be to name these types of objects ...Transition, so we're going to call our widget FlipYTransition. This would be used in the above example instead of the AnimatedBuilder, and does essentially the same thing.

class FlipYTransition extends AnimatedWidget {
  const FlipYTransition({
    super.key,
    required Animation<double> flip,
    this.child,
  }) : super(listenable: flip);

  final Widget? child;

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

  @override
  Widget build(BuildContext context) {
    final flipMatrix = Matrix4.rotationX(flip.value * pi);
    return Transform(
      transform: flipMatrix,
      alignment: Alignment.center,
      child: child,
    );
  }
}

Everything done here should be relatively easy to follow from the previous example with the exception of the flip getter - this is not really necessary but provides a cleaner way to access the flip animation which was passed in, as the AnimatedWidget widget only saves the animation as a Listenable.