Animations & Motion Design

Introduction to Flutter Animations

16 min Lesson 1 of 13

Introduction to Flutter Animations

Animations elevate a Flutter app from functional to polished. A well-timed fade, a smooth slide, or a spring-based bounce communicates structure, provides feedback, and delights users. Flutter ships with a rich, layered animation system — from one-line implicit widgets all the way down to low-level Animation objects and physics simulations. Understanding the taxonomy lets you choose the right tool for each situation without reaching for a sledgehammer when a screwdriver will do.

The Three-Tier Animation Taxonomy

Flutter's animation system is best understood as three tiers, each offering more control at the cost of more code:

  • Implicit animations — widgets that animate automatically when a property changes. Zero boilerplate: set a new value, Flutter interpolates to it.
  • Explicit animations — animations you drive manually with an AnimationController. You decide when they start, stop, repeat, and reverse.
  • Physics-based animations — simulations (spring, friction, gravity) that produce motion which feels natural because it obeys real-world physics rather than a fixed duration.
Note: These tiers are not mutually exclusive. A sophisticated UI often uses all three: implicit widgets for simple state-driven transitions, explicit controllers for coordinated multi-step sequences, and physics simulations for drag-release or fling interactions.

Implicit Animations — Animate Without a Controller

The AnimatedFoo family of widgets (e.g. AnimatedOpacity, AnimatedContainer, AnimatedPadding, AnimatedAlign) wraps a regular widget and transitions any property listed in the Flutter docs automatically when its value changes. You supply a duration and optionally a curve; Flutter does the interpolation.

When to choose implicit: The animation is triggered by a state change (a boolean flip, a new data value), runs once per trigger, has no dependency on other animations, and does not need to be paused, reversed on demand, or sequenced.

Example 1 — AnimatedOpacity

import 'package:flutter/material.dart';

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

  @override
  State<FadeToggle> createState() => _FadeToggleState();
}

class _FadeToggleState extends State<FadeToggle> {
  bool _visible = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedOpacity(
          opacity: _visible ? 1.0 : 0.0,
          duration: const Duration(milliseconds: 400),
          curve: Curves.easeInOut,
          child: const FlutterLogo(size: 100),
        ),
        const SizedBox(height: 24),
        ElevatedButton(
          onPressed: () => setState(() => _visible = !_visible),
          child: Text(_visible ? 'Hide' : 'Show'),
        ),
      ],
    );
  }
}

Tapping the button flips _visible, which changes opacity, and AnimatedOpacity smoothly interpolates the opacity value over 400 ms — no AnimationController required.

Explicit Animations — Full Manual Control

When you need precise control — looping, reversing on a gesture, sequencing multiple values — you graduate to AnimationController combined with Tween and AnimatedBuilder (or the mixin shortcuts SingleTickerProviderStateMixin / TickerProviderStateMixin).

When to choose explicit: The animation must start or stop in response to user input, needs to loop indefinitely (e.g. a loading spinner), must synchronise with another animation, or drives a custom painted value.

Example 2 — AnimationController with Tween

import 'package:flutter/material.dart';

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

  @override
  State<PulsingDot> createState() => _PulsingDotState();
}

class _PulsingDotState extends State<PulsingDot>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    )..repeat(reverse: true); // loops back and forth

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

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _scale,
      builder: (context, child) => Transform.scale(
        scale: _scale.value,
        child: child,
      ),
      child: Container(
        width: 60,
        height: 60,
        decoration: const BoxDecoration(
          color: Colors.deepPurple,
          shape: BoxShape.circle,
        ),
      ),
    );
  }
}

Physics-Based Animations — Natural Motion

Physics simulations replace fixed-duration curves with force-based models. The most common is SpringSimulation, surfaced through SpringDescription and AnimationController.animateWith(), or through the higher-level PhysicsBasedAnimationWidget in newer Flutter versions. FrictionSimulation models deceleration (ideal for scroll flings), while GravitySimulation produces falling objects.

When to choose physics: Motion is initiated by a gesture with a velocity (drag-and-release, fling), or you want the widget to "settle" naturally rather than ending abruptly at a fixed time. The animation's end-point may not be fully known at start time.

Tip: The spring_description parameters — mass, stiffness, and damping — map directly to physical intuition. A higher stiffness = snappier spring; lower damping = more oscillation (bouncy). Start with SpringDescription.withDampingRatio(mass: 1, stiffness: 200, ratio: 0.8) for a subtle, professional-feeling bounce.

Decision Guide: Which Tier to Use?

  • State change triggers a single transition? → Implicit (AnimatedOpacity, AnimatedContainer, TweenAnimationBuilder).
  • Need looping, sequencing, or gesture-driven control? → Explicit (AnimationController + Tween + AnimatedBuilder).
  • Initiated by a fling/drag with real velocity? → Physics-based (SpringSimulation, FrictionSimulation).
  • Predefined route/page transition?PageRouteBuilder with a custom transitionsBuilder (explicit under the hood).
Warning: Always dispose of AnimationController in dispose(). A leaked controller keeps a Ticker alive, burning CPU and battery every frame even when the widget is no longer visible. This is one of the most common performance bugs in Flutter apps.

The Animation Pipeline Under the Hood

Every Flutter animation ultimately drives a double value (the animation value) through time using a Ticker — a callback registered with the engine's VSYNC signal. The pipeline is: Ticker → AnimationController (0.0–1.0) → Tween (maps to typed range) → Curve (shapes the easing) → widget rebuild via AnimatedBuilder or listener. Implicit widgets wrap all of this internally; explicit animations expose each step.

Summary

Flutter animations fall into three tiers: implicit (automatic, state-driven), explicit (manual, controller-driven), and physics-based (simulation-driven, velocity-aware). Start with implicit widgets like AnimatedOpacity for simple transitions, reach for AnimationController when you need control, and use physics simulations for gesture-initiated motion that must feel natural. In the lessons ahead you will master each tier with progressively more complex, real-world examples.