Skip to content

Flutter - Animation

Animation enhances user experience by adding motion and visual feedback, making applications feel more interactive and polished. While potentially complex, Flutter provides a powerful yet intuitive animation framework.

Animation is essentially the illusion of movement created by displaying a sequence of frames over a specific duration. Key aspects include:

  • Start and End Values: Animation interpolates between a defined starting value and an ending value (e.g., animating opacity from 1.0 (fully visible) to 0.0 (invisible)).
  • Interpolation (Tweening): The process of calculating intermediate values between the start and end. This can be linear (constant speed) or non-linear (using Curves for acceleration, deceleration, bouncing, etc.).
  • Duration: The total time the animation takes to complete, affecting its perceived speed.
  • Control: The ability to start, stop, reverse, or repeat the animation.
  • Ticker: Provides a signal (a ‘tick’) for each frame, synchronizing the animation with the screen refresh rate.
  • Animation Object: In Flutter, the animation system primarily generates changing values (like numbers, colors, sizes) over time via Animation objects. It doesn’t directly render; widgets use these values to update their properties during the build process.

Flutter’s animation system revolves around a few core classes:

Represents a value of type T that changes over time. It knows its current value and status (e.g., running, completed, dismissed). Common types include Animation<double>, Animation<Color>, Animation<Size>.

A special Animation<double> that manages the animation process. It generates values from 0.0 to 1.0 over a given duration and provides methods to control the animation (e.g., forward(), reverse(), stop(), repeat()). It requires a TickerProvider (vsync).

// Inside a State class that mixes in TickerProviderStateMixin
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this, // 'this' provides the Ticker
);
}
@override
void dispose() {
_controller.dispose(); // IMPORTANT: Dispose the controller!
super.dispose();
}

vsync prevents offscreen animations from consuming resources. It’s typically provided by mixing TickerProviderStateMixin (for single controller) or SingleTickerProviderStateMixin (for multiple controllers) into your State class.

Defines the mapping between the AnimationController’s 0.0-1.0 range and the actual range of values needed for your animation (e.g., mapping 0.0-1.0 to 0-300 pixels, or to specific colors). It’s short for ‘in-between’.

// Creates an animation that goes from 50.0 to 200.0
late Animation<double> _sizeAnimation;
// In initState, after controller is initialized:
_sizeAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(_controller);

Applies a non-linear Curve (like Curves.easeIn, Curves.bounceOut) to an existing animation (usually an AnimationController). This modifies the rate of change, not the value range.

// Apply an ease-in curve to the controller
late Animation<double> _curvedAnimation;
// In initState, after controller is initialized:
_curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
// You can then chain a Tween with the CurvedAnimation:
_sizeAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(_curvedAnimation);

The typical workflow for creating explicit animations involves:

  1. Setup (in State):
    • Mixin TickerProviderStateMixin or SingleTickerProviderStateMixin.
    • Create an AnimationController in initState, providing duration and vsync.
    • Create Tweens to define value ranges.
    • Create Animation objects by connecting Tweens (and optionally CurvedAnimation) to the AnimationController using .animate().
    • Important: Dispose the AnimationController in the dispose method.
  2. Listening/Building: Choose one of these methods to rebuild the UI when the animation value changes:
    • AnimatedBuilder: (Recommended) A widget that listens to an animation and rebuilds its builder function whenever the animation value changes. It optimizes rebuilding by only affecting its subtree.
    • AnimatedWidget: A base class for creating reusable animation widgets (like FadeTransition, SlideTransition). You extend it and use the animation value directly in its build method.
    • addListener + setState: Manually add a listener to the Animation or AnimationController. Inside the listener callback, call setState(() {}) to trigger a rebuild of the entire widget. This is less efficient than AnimatedBuilder or AnimatedWidget.
  3. Using the Value: In the build method (within AnimatedBuilder’s builder, or an AnimatedWidget, or the main build if using addListener), access the animation’s current value using animation.value and apply it to widget properties (like width, height, opacity, color, transform).
  4. Starting the Animation: Call methods on the AnimationController like _controller.forward() or _controller.repeat().

Working Example: Fading and Growing Widget

Section titled “Working Example: Fading and Growing Widget”

Let’s create an application where a logo fades in and grows.

  1. Create a new Flutter project: flutter create animation_example.
  2. Replace lib/main.dart with the following code:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Animation Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const AnimationDemoPage(),
);
}
}
class AnimationDemoPage extends StatefulWidget {
const AnimationDemoPage({Key? key}) : super(key: key);
@override
_AnimationDemoPageState createState() => _AnimationDemoPageState();
}
// Use SingleTickerProviderStateMixin since we have one AnimationController
class _AnimationDemoPageState extends State<AnimationDemoPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacityAnimation;
late Animation<double> _sizeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
);
// Opacity goes from 0.0 (invisible) to 1.0 (visible)
// Use a CurvedAnimation for a non-linear fade (e.g., ease-in)
final curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
);
_opacityAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(curvedAnimation);
// Size goes from 50 to 200
// You can animate the controller directly or use the curved animation
_sizeAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(curvedAnimation);
// Start the animation automatically
_controller.forward();
/* Optional: Add listeners to track status
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// Maybe reverse it?
// _controller.reverse();
} else if (status == AnimationStatus.dismissed) {
// Maybe start it again?
// _controller.forward();
}
});
*/
}
@override
void dispose() {
// IMPORTANT: Dispose the controller when the widget is removed
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Fade & Grow Animation'),
),
body: Center(
// Use AnimatedBuilder for efficient rebuilding
child: AnimatedBuilder(
animation: _controller, // Listen to the controller
builder: (BuildContext context, Widget? child) {
// This builder function runs every time the animation ticks
return Opacity(
opacity: _opacityAnimation.value, // Use the opacity value
child: Container(
width: _sizeAnimation.value, // Use the size value
height: _sizeAnimation.value,
child: child, // Pass the constant child down
),
);
},
// Provide a constant child that doesn't need rebuilding itself
child: const FlutterLogo(),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Example: Control the animation
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: const Icon(Icons.play_arrow),
),
);
}
}

Run this application. You will see the Flutter logo start small and transparent, then grow and fade in over 3 seconds. The Floating Action Button can be used to replay or reverse the animation.

Flutter also offers Implicit Animations (widgets like AnimatedContainer, AnimatedOpacity) which automatically animate changes to their properties over a duration, simplifying common animation tasks without needing controllers or tweens explicitly.