Flutter - Animation
Flutter - Animation Fundamentals
Section titled “Flutter - Animation Fundamentals”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.
Core Concepts
Section titled “Core Concepts”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
Animationobjects. It doesn’t directly render; widgets use these values to update their properties during the build process.
Key Animation Classes
Section titled “Key Animation Classes”Flutter’s animation system revolves around a few core classes:
Animation
Section titled “Animation”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>.
AnimationController
Section titled “AnimationController”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 TickerProviderStateMixinlate AnimationController _controller;
@overridevoid initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, // 'this' provides the Ticker );}
@overridevoid 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.0late Animation<double> _sizeAnimation;
// In initState, after controller is initialized:_sizeAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(_controller);CurvedAnimation
Section titled “CurvedAnimation”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 controllerlate 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);Animation Workflow
Section titled “Animation Workflow”The typical workflow for creating explicit animations involves:
- Setup (in
State): -
- Mixin
TickerProviderStateMixinorSingleTickerProviderStateMixin.
- Mixin
-
- Create an
AnimationControllerininitState, providingdurationandvsync.
- Create an
-
- Create
Tweens to define value ranges.
- Create
-
- Create
Animationobjects by connectingTweens (and optionallyCurvedAnimation) to theAnimationControllerusing.animate().
- Create
-
- Important: Dispose the
AnimationControllerin thedisposemethod.
- Important: Dispose the
- 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 itsbuilderfunction whenever the animation value changes. It optimizes rebuilding by only affecting its subtree.
-
AnimatedWidget: A base class for creating reusable animation widgets (likeFadeTransition,SlideTransition). You extend it and use the animation value directly in itsbuildmethod.
-
addListener+setState: Manually add a listener to theAnimationorAnimationController. Inside the listener callback, callsetState(() {})to trigger a rebuild of the entire widget. This is less efficient thanAnimatedBuilderorAnimatedWidget.
- Using the Value: In the
buildmethod (withinAnimatedBuilder’s builder, or anAnimatedWidget, or the mainbuildif usingaddListener), access the animation’s current value usinganimation.valueand apply it to widget properties (likewidth,height,opacity,color,transform). - Starting the Animation: Call methods on the
AnimationControllerlike_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.
- Create a new Flutter project:
flutter create animation_example. - Replace
lib/main.dartwith 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 AnimationControllerclass _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.