Dart Tutorial

Flutter Lesson 55 of 83 4 min read

Explicit Animations in Flutter: AnimationController and Tween

Control animations precisely in Flutter with AnimationController, Tween, CurvedAnimation, AnimatedBuilder and staggered sequences.

On this page

An explicit animation gives you the controls: play, stop, reverse, repeat, jump to a position. You need one when an animation loops, responds to a drag, or coordinates several properties.

The pieces #

PieceRole
AnimationControllerThe engine. Produces a value from 0.0 to 1.0 over a duration, and has forward(), reverse(), repeat() and stop()
Ticker (via a mixin)Calls the controller once per frame, and pauses when the widget is not visible
TweenMaps 0.0 to 1.0 onto the range you want: sizes, colours, offsets
CurvedAnimationApplies a curve
A transition or AnimatedBuilderRebuilds the UI on every tick

A pulsing heart #

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: Scaffold(body: Center(child: PulsingHeart()))));

class PulsingHeart extends StatefulWidget {
  const PulsingHeart({super.key});

  @override
  State<PulsingHeart> createState() => _PulsingHeartState();
}

class _PulsingHeartState extends State<PulsingHeart>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 700),
    )..repeat(reverse: true);

    _scale = Tween<double>(begin: 0.85, end: 1.15).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: _scale,
      child: const Icon(Icons.favorite, color: Colors.red, size: 96),
    );
  }
}

Three things you must always do:

  1. Add with SingleTickerProviderStateMixin to the state class. It supplies vsync: this.
  2. Create the controller in initState.
  3. Dispose it in dispose. A forgotten controller keeps ticking for ever.

If one state needs several controllers, use TickerProviderStateMixin.

Controlling playback #

_controller.forward();              // 0 to 1
_controller.reverse();              // 1 to 0
_controller.repeat();               // loop
_controller.repeat(reverse: true);  // back and forth
_controller.stop();
_controller.reset();                // jump to 0
_controller.value = 0.5;            // jump anywhere
_controller.animateTo(0.8);         // animate to a point

_controller.addStatusListener((status) {
  if (status == AnimationStatus.completed) debugPrint('Finished');
});

Ready-made transitions #

These take an animation and apply it to a child, rebuilding efficiently.

WidgetAnimatesTakes
FadeTransitionOpacityAnimation<double>
ScaleTransitionScaleAnimation<double>
RotationTransitionRotation, in turnsAnimation<double>
SlideTransitionPosition, as a fraction of its sizeAnimation<Offset>
SizeTransitionClipped sizeAnimation<double>
SlideTransition(
  position: Tween<Offset>(begin: const Offset(0, 1), end: Offset.zero).animate(
    CurvedAnimation(parent: _controller, curve: Curves.easeOut),
  ),
  child: const Card(child: Padding(padding: EdgeInsets.all(24), child: Text('Slides up'))),
)

AnimatedBuilder: anything else #

When no transition fits, AnimatedBuilder rebuilds whatever you return on every tick.

import 'dart:math' as math;

AnimatedBuilder(
  animation: _controller,
  child: const Icon(Icons.settings, size: 64), // built once, not every frame
  builder: (context, child) {
    return Transform.rotate(
      angle: _controller.value * 2 * math.pi,
      child: child,
    );
  },
)

Pass the unchanging part as child. It is built once and handed back to the builder, which avoids rebuilding it sixty times a second.

Playing on a tap #

class _ExpandButtonState extends State<ExpandButton>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 250),
  );

  void _toggle() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return IconButton(
      onPressed: _toggle,
      icon: AnimatedIcon(icon: AnimatedIcons.menu_close, progress: _controller),
    );
  }
}

A late final field with an initializer is created the first time it is used, which is a tidy alternative to initState.

Staggered animations #

One controller can drive several animations that start at different moments. Give each an Interval: a slice of the controller’s 0 to 1 range.

late final Animation<double> _fade = CurvedAnimation(
  parent: _controller,
  curve: const Interval(0.0, 0.5, curve: Curves.easeOut), // first half
);

late final Animation<Offset> _slide = Tween<Offset>(
  begin: const Offset(0, 0.3),
  end: Offset.zero,
).animate(CurvedAnimation(
  parent: _controller,
  curve: const Interval(0.2, 1.0, curve: Curves.easeOut), // overlaps, ends later
));

// In build
FadeTransition(
  opacity: _fade,
  child: SlideTransition(position: _slide, child: const WelcomeCard()),
)

Driving an animation with a gesture #

Because controller.value can be set directly, a drag can control an animation frame by frame.

GestureDetector(
  onHorizontalDragUpdate: (details) {
    _controller.value += details.primaryDelta! / 300; // 300 px for the full range
  },
  onHorizontalDragEnd: (_) {
    _controller.value > 0.5 ? _controller.forward() : _controller.reverse();
  },
  child: ...,
)

Implicit or explicit? #

QuestionIf yes
Does it just go from one value to another when state changes?Implicit
Does it repeat or loop?Explicit
Do you need to pause, reverse or seek?Explicit
Is it tied to a gesture?Explicit
Are several properties sequenced?Explicit, with intervals

For rich pre-designed animations, such as an animated illustration or a character, use the lottie or rive packages, which play files made by designers.

Try it yourself #

Build a loading indicator from three dots that bounce one after another, using one repeating controller and three Intervals. Then build a card that fades and slides in when the screen opens.

Practise in the playground Updated by Santosh Adhikari