Dart Tutorial

Flutter Lesson 54 of 83 4 min read

Implicit Animations in Flutter: AnimatedContainer and Friends

Add animation to Flutter with no controllers: AnimatedContainer, AnimatedOpacity, AnimatedSwitcher, TweenAnimationBuilder and curves.

On this page

An implicit animation is the easiest kind. You change a value, and the widget animates from the old value to the new one by itself. There is no controller to manage.

AnimatedContainer #

It is a Container that animates any change to its properties.

import 'package:flutter/material.dart';

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

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

  @override
  State<MorphBox> createState() => _MorphBoxState();
}

class _MorphBoxState extends State<MorphBox> {
  bool _big = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _big = !_big),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 400),
        curve: Curves.easeInOut,
        width: _big ? 240 : 120,
        height: _big ? 240 : 120,
        decoration: BoxDecoration(
          color: _big ? Colors.deepPurple : Colors.teal,
          borderRadius: BorderRadius.circular(_big ? 48 : 12),
        ),
        alignment: Alignment.center,
        child: const Text('Tap me', style: TextStyle(color: Colors.white)),
      ),
    );
  }
}

The only animation code is duration and curve. You call setState as usual and Flutter fills in the frames between.

The Animated family #

Most layout and styling widgets have an animated twin.

WidgetAnimates
AnimatedContainerSize, colour, padding, border, radius, alignment
AnimatedOpacityFading in and out
AnimatedPaddingPadding
AnimatedAlignPosition within a parent
AnimatedPositionedPosition inside a Stack
AnimatedScale, AnimatedRotation, AnimatedSlideTransforms
AnimatedDefaultTextStyleText style
AnimatedSizeIts own size, when the child’s size changes
AnimatedCrossFadeA fade between two children
AnimatedSwitcherA transition when the child is replaced

They all work the same way: give a duration, change a property, call setState.

AnimatedOpacity(
  opacity: _visible ? 1 : 0,
  duration: const Duration(milliseconds: 300),
  child: const Text('Now you see me'),
)

Curves #

A curve controls how the speed changes during the animation.

CurveFeel
Curves.linearConstant speed. Mechanical
Curves.easeInOutSlow start and end. A safe default
Curves.easeOutFast start, gentle stop. Good for things entering
Curves.easeInGentle start, fast end. Good for things leaving
Curves.easeOutBackOvershoots slightly, then settles
Curves.elasticOutSpringy
Curves.bounceOutBounces at the end

AnimatedSwitcher #

When the child is replaced by a different widget, AnimatedSwitcher fades between them.

AnimatedSwitcher(
  duration: const Duration(milliseconds: 250),
  transitionBuilder: (child, animation) =>
      ScaleTransition(scale: animation, child: child),
  child: Text(
    '$_count',
    key: ValueKey(_count), // a new key tells the switcher the child changed
    style: const TextStyle(fontSize: 48),
  ),
)

Without the key, the switcher sees “a Text replaced by a Text” and does nothing. It is also ideal for swapping a loading spinner for content.

TweenAnimationBuilder #

When no ready-made widget animates what you need, TweenAnimationBuilder animates any value and lets you build from it.

TweenAnimationBuilder<double>(
  tween: Tween(begin: 0, end: _progress), // animates whenever `end` changes
  duration: const Duration(milliseconds: 600),
  curve: Curves.easeOut,
  builder: (context, value, child) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        SizedBox(
          width: 120,
          height: 120,
          child: CircularProgressIndicator(value: value, strokeWidth: 10),
        ),
        const SizedBox(height: 12),
        Text('${(value * 100).round()}%'),
      ],
    );
  },
)

Use ColorTween, IntTween or Tween<Offset> for other types. A count-up number is one line: animate an IntTween and show its value.

Expanding and collapsing #

AnimatedSize(
  duration: const Duration(milliseconds: 250),
  curve: Curves.easeInOut,
  alignment: Alignment.topCenter,
  child: _expanded
      ? const Text('A long description that appears smoothly when expanded...')
      : const SizedBox(width: double.infinity),
)

How long should an animation be? #

KindDuration
Small feedback: a toggle, a colour change100 to 200 ms
An element entering, leaving or expanding200 to 300 ms
A full-screen transition300 to 400 ms

Longer than half a second feels slow. Users see these animations hundreds of times, so keep them brief.

Respect reduced motion #

Some people turn animations off in their device settings because motion makes them unwell.

final reduceMotion = MediaQuery.disableAnimationsOf(context);

AnimatedContainer(
  duration: reduceMotion ? Duration.zero : const Duration(milliseconds: 300),
  // ...
)

When implicit is not enough #

Implicit animations run once, from one value to another, when you change something. For animations that loop, reverse, are driven by a gesture, or need several properties in sequence, use an explicit animation.

Try it yourself #

Build a “like” button: tapping it animates the heart’s colour and scale, and the count beside it changes with an AnimatedSwitcher. Then build an expandable FAQ item using AnimatedSize and an arrow that turns with AnimatedRotation.

Practise in the playground Updated by Santosh Adhikari