This content originally appeared on DEV Community and was authored by Ge Ji
Animations are crucial for enhancing user experience, making app interfaces more lively, intuitive, and engaging. Flutter provides a powerful animation system that supports the implementation of various complex animation effects. This lesson will detailedly introduce the types and implementation methods of animations in Flutter, from basic implicit animations to complex custom explicit animations, helping you master the skills of adding smooth animation effects to your applications.
I. Flutter Animation Basics
Flutter’s animation system is based on the following core concepts:
- Animation: An object that generates values between 0.0 and 1.0. It doesn’t contain rendering content itself but only provides animation values.
- Curve: Defines the speed change of animation progress, such as acceleration and deceleration.
- Controller: Controls the playback, pause, reverse, etc., of the animation and manages the animation life cycle.
- Tween: Defines the start and end values of the animation, mapping the 0-1 values provided by Animation to the actual required value range.
- Animatable: An object that can generate Tweens, supporting more complex value mapping.
Flutter animations are mainly divided into two categories:
- Implicit animations: Animations automatically managed by Flutter, where you only need to define the start and end states.
- Explicit animations: Animations that need to be manually controlled, providing more customization options and control capabilities.
II. Implicit Animations
Implicit animations are the simplest way to implement animations. Flutter provides a series of encapsulated implicit animation widgets that automatically transition smoothly from old values to new values when certain properties of the widget change.
1. AnimatedContainer
AnimatedContainer is the most commonly used implicit animation widget. When its properties (such as size, color, margin, etc.) change, it automatically produces transition animations.
class AnimatedContainerDemo extends StatefulWidget {
const AnimatedContainerDemo({super.key});
@override
State<AnimatedContainerDemo> createState() => _AnimatedContainerDemoState();
}
class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
// State variable to control container properties
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedContainer Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
// Animation duration
duration: const Duration(seconds: 1),
// Animation curve
curve: Curves.easeInOut,
// Dynamically changing properties
width: _isExpanded ? 300 : 100,
height: _isExpanded ? 300 : 100,
color: _isExpanded ? Colors.blue : Colors.red,
padding: _isExpanded
? const EdgeInsets.all(20)
: const EdgeInsets.all(10),
// Content inside the container
child: const Center(
child: Text(
'Animate me!',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
// Toggle state when button is clicked to trigger animation
setState(() {
_isExpanded = !_isExpanded;
});
},
child: const Text('Toggle Animation'),
)
],
),
),
);
}
}
2. Other Common Implicit Animation Widgets
Flutter also provides other implicit animation widgets for specific purposes:
AnimatedOpacity
Animation that controls the transparency change of a widget:
class AnimatedOpacityDemo extends StatefulWidget {
const AnimatedOpacityDemo({super.key});
@override
State<AnimatedOpacityDemo> createState() => _AnimatedOpacityDemoState();
}
class _AnimatedOpacityDemoState extends State<AnimatedOpacityDemo> {
double _opacity = 1.0;
void _toggleOpacity() {
setState(() {
_opacity = _opacity == 1.0 ? 0.0 : 1.0;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedOpacity Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedOpacity(
opacity: _opacity,
duration: const Duration(seconds: 1),
curve: Curves.fastOutSlowIn,
child: Container(
width: 200,
height: 200,
color: Colors.green,
child: const Center(
child: Text(
'Fade me!',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _toggleOpacity,
child: const Text('Toggle Opacity'),
)
],
),
),
);
}
}
AnimatedPositioned
Used in Stack to animate the position change of child widgets:
class AnimatedPositionedDemo extends StatefulWidget {
const AnimatedPositionedDemo({super.key});
@override
State<AnimatedPositionedDemo> createState() => _AnimatedPositionedDemoState();
}
class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
bool _isMoved = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedPositioned Demo')),
body: Stack(
children: [
AnimatedPositioned(
duration: const Duration(seconds: 1),
curve: Curves.bounceOut,
left: _isMoved ? 200 : 50,
top: _isMoved ? 300 : 100,
width: 100,
height: 100,
child: Container(
color: Colors.purple,
child: const Center(
child: Text(
'Move me!',
style: TextStyle(color: Colors.white),
),
),
),
),
Positioned(
bottom: 50,
left: 0,
right: 0,
child: Center(
child: ElevatedButton(
onPressed: () {
setState(() {
_isMoved = !_isMoved;
});
},
child: const Text('Move the Box'),
),
),
)
],
),
);
}
}
AnimatedPadding, AnimatedSize, etc.
There are other similar implicit animation widgets with similar usage:
- AnimatedPadding: Controls the change of padding.
- AnimatedSize: Automatically adjusts according to the size of the child widget and produces animation.
- AnimatedTransform: Controls the animation of transformation effects.
- AnimatedDefaultTextStyle: Controls the animation of text style changes.
3. Advantages and Disadvantages of Implicit Animations
Advantages:
- Easy to use, just modify the state to trigger the animation.
- No need to manually manage the animation controller.
- Suitable for implementing simple transition effects.
Disadvantages:
- Low customization, unable to implement complex animations.
- Lack of fine control capabilities (such as pause, reverse playback).
- Difficult to coordinate when multiple properties are animated at the same time.
III. Explicit Animations
Explicit animations require manual creation and management of animation controllers, providing more precise control and greater flexibility, suitable for implementing complex animation effects.
1. AnimationController and Animation
AnimationController is the core of explicit animations, responsible for controlling the time and state of the animation:
class BasicExplicitAnimation extends StatefulWidget {
const BasicExplicitAnimation({super.key});
@override
State<BasicExplicitAnimation> createState() => _BasicExplicitAnimationState();
}
class _BasicExplicitAnimationState extends State<BasicExplicitAnimation>
with SingleTickerProviderStateMixin {
// Animation controller
late AnimationController _controller;
// Animation object
late Animation<double> _animation;
@override
void initState() {
super.initState();
// Initialize the animation controller
_controller = AnimationController(
vsync: this, // Associated with the current widget's lifecycle
duration: const Duration(seconds: 2), // Animation duration
);
// Create an animation from 0 to 300
_animation = Tween<double>(begin: 0, end: 300).animate(_controller)
// Listen for animation value changes to trigger reconstruction
..addListener(() {
setState(() {});
})
// Listen for animation state changes
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
// Reverse playback after animation completion
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
// Forward playback after animation returns to the starting point
_controller.forward();
}
});
// Start the animation
_controller.forward();
}
@override
void dispose() {
// Release the animation controller resources
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Basic Explicit Animation')),
body: Center(
child: Container(
width: _animation.value, // Use the animation value
height: _animation.value,
color: Colors.orange,
child: const Center(
child: Text(
'Growing Box',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
);
}
}
Note: SingleTickerProviderStateMixin provides a vsync callback to prevent the animation from continuing to consume resources when the widget is invisible. If multiple animation controllers are needed, TickerProviderStateMixin should be used.
2. CurvedAnimation
Using CurvedAnimation can add non-linear speed changes to the animation:
class CurvedAnimationDemo extends StatefulWidget {
const CurvedAnimationDemo({super.key});
@override
State<CurvedAnimationDemo> createState() => _CurvedAnimationDemoState();
}
class _CurvedAnimationDemoState extends State<CurvedAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
);
// Create a curved animation
final curve = CurvedAnimation(
parent: _controller,
curve: Curves.bounceOut, // Bounce effect
reverseCurve: Curves.bounceIn, // Curve for reverse direction
);
// Use the curved animation
_animation = Tween<double>(begin: 50, end: 300).animate(curve)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Curved Animation')),
body: Center(
child: Container(
width: _animation.value,
height: 100,
color: Colors.pink,
child: const Center(
child: Text(
'Bouncy!',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
),
);
}
}
Flutter provides a variety of predefined curves, such as:
- Curves.linear: Linear animation
- Curves.easeIn, Curves.easeOut, Curves.easeInOut: Ease in, ease out, ease in and out
- Curves.bounceIn, Curves.bounceOut: Bounce effect
- Curves.elasticIn, Curves.elasticOut: Elastic effect
3. Tween and Multi-property Animations
Tween defines the value range of the animation, which can be any type. Multiple Tweens can be combined to implement multi-property animations:
class MultiPropertyAnimation extends StatefulWidget {
const MultiPropertyAnimation({super.key});
@override
State<MultiPropertyAnimation> createState() => _MultiPropertyAnimationState();
}
class _MultiPropertyAnimationState extends State<MultiPropertyAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<double> _opacityAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
);
// Size animation
_sizeAnimation = Tween<double>(begin: 50, end: 250).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
// Opacity animation
_opacityAnimation = Tween<double>(begin: 0.2, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: const Interval(0.3, 1.0)) // Start with delay
);
// Color animation
_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.green).animate(
CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.7)) // End early
);
// Listen for animation value changes
_controller.addListener(() {
setState(() {});
});
// Play in a loop
_controller.repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Multi-property Animation')),
body: Center(
child: Opacity(
opacity: _opacityAnimation.value,
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: _colorAnimation.value,
// Using borderRadius property
borderRadius: BorderRadius.circular(_sizeAnimation.value / 10)
),
child: const Center(
child: Text(
'Fancy!',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
),
);
}
}
Interval can set different time intervals for different animation properties, enabling more complex animation choreography.
4. AnimatedBuilder for Optimizing Reconstruction Performance
Using addListener with setState will cause the entire widget to rebuild. Using AnimatedBuilder can only rebuild the part that needs animation, improving performance:
class AnimatedBuilderDemo extends StatefulWidget {
const AnimatedBuilderDemo({super.key});
@override
State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
}
class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 5),
);
_rotationAnimation = Tween<double>(begin: 0, end: 2 * 3.14159).animate(
CurvedAnimation(parent: _controller, curve: Curves.linear)
);
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('AnimatedBuilder Demo')),
body: Center(
// Wrap the part that needs animation with AnimatedBuilder
child: AnimatedBuilder(
animation: _rotationAnimation,
// The builder method only rebuilds the animation-related part
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: child, // Pass in the static child widget to avoid repeated construction
);
},
// Static child widget, which will be built only once
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.amber,
// Using borderRadius property
borderRadius: BorderRadius.circular(20)
),
child: const Center(
child: Text(
'Spinning!',
style: TextStyle(color: Colors.black, fontSize: 24),
),
),
),
),
),
);
}
}
IV. Page Transition Animations
Flutter allows custom transition animations between pages. Various transition effects can be achieved through PageRouteBuilder.
1. Fade Transition
class FadeTransitionDemo extends StatelessWidget {
const FadeTransitionDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Fade Transition Demo')),
body: Center(
child: ElevatedButton(
child: const Text('Go to Second Screen'),
onPressed: () {
// Navigate to the second page with custom transition animation
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: const Duration(seconds: 1), // Transition time
// Build page content
pageBuilder: (context, animation, secondaryAnimation) {
return const SecondScreen();
},
// Build transition effect
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// Fade transition
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second Screen'),
backgroundColor: Colors.green,
),
body: Center(
child: ElevatedButton(
child: const Text('Go Back'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
2. Slide Transition
// Use this transition when navigating
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (context, animation, secondaryAnimation) {
return const SecondScreen();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// Slide transition - slide in from the right
const begin = Offset(1.0, 0.0); // Starting position (right side)
const end = Offset.zero; // Ending position (inside the screen)
const curve = Curves.easeInOut;
// Create an animation from begin to end
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
var offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
)
3. Scale Transition
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (context, animation, secondaryAnimation) {
return const SecondScreen();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// Scale transition
return ScaleTransition(
scale: animation.drive(
Tween(begin: 0.0, end: 1.0).chain(
CurveTween(curve: Curves.bounceOut)
)
),
child: child,
);
},
)
4. Combined Transition Effects
Multiple transition effects can be combined:
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 700),
pageBuilder: (context, animation, secondaryAnimation) {
return const SecondScreen();
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// Combine scale and fade effects
return ScaleTransition(
scale: animation.drive(
Tween(begin: 0.8, end: 1.0).chain(
CurveTween(curve: Curves.easeInOut)
)
),
child: FadeTransition(
opacity: animation.drive(
Tween(begin: 0.0, end: 1.0).chain(
CurveTween(curve: const Interval(0.2, 1.0))
)
),
child: child,
),
);
},
)
V. Example: Button Click Scale Effect
Implement a button with click feedback that produces a complete scale effect when clicked:
class BounceButton extends StatefulWidget {
final Widget child;
final VoidCallback onPressed;
final double scaleFactor;
final Duration duration;
const BounceButton({
super.key,
required this.child,
required this.onPressed,
this.scaleFactor = 0.8,
this.duration = const Duration(milliseconds: 300),
});
@override
State<BounceButton> createState() => _BounceButtonState();
}
class _BounceButtonState extends State<BounceButton>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: widget.scaleFactor,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// Handle press - do not execute forward directly, controlled uniformly by onTapUp
void _onTapDown(TapDownDetails details) {
print("Pressed: Prepare to start animation");
}
// Handle release - control the animation process uniformly to ensure complete execution
void _onTapUp(TapUpDetails details) {
print("Released: Start animation process");
// First execute the complete forward animation, then the reverse animation, and finally trigger the callback
_controller.forward().then((_) {
_controller.reverse().then((_) {
widget.onPressed();
});
});
}
// Handle cancel
void _onTapCancel() {
print("Cancelled: Reverse animation");
_controller.reverse();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(scale: _scaleAnimation.value, child: child);
},
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
behavior: HitTestBehavior.opaque,
child: widget.child,
),
);
}
}
// Usage example
class BounceButtonDemo extends StatelessWidget {
const BounceButtonDemo({super.key});
void _handlePress() {
print('Button pressed!');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Bounce Button Demo')),
body: Center(
child: BounceButton(
onPressed: _handlePress,
scaleFactor: 0.7,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: const Text(
'Click Me',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
}
}
Optimization Description:
- Do not directly execute _controller.forward() in _onTapDown, only mark the button as pressed.
- Uniformly control the animation process in _onTapUp, first execute the complete forward animation (button shrinks), then the reverse animation (button recovers), and finally trigger the business callback.
VII. Animation Performance Optimization
Animation performance is crucial for user experience. Here are some optimization suggestions:
- Use AnimatedBuilder to reduce reconstruction scope: Only rebuild the part that needs animation, avoiding the entire page reconstruction.
- Avoid complex calculations during animation: Animation callbacks should be as concise as possible; complex calculations will cause stuttering.
- Use hardware acceleration: Most animations will automatically use hardware acceleration, but operations that trigger software rendering (such as using saveLayer) should be avoided.
- Control animation frame rate: In most cases, 60fps is sufficient; a higher frame rate will consume more resources.
- Set reasonable animation duration: Generally, the animation duration is between 200-300ms; a longer duration will make users feel delayed.
- Use RepaintBoundary to isolate redraw areas: For animation widgets that are frequently redrawn, wrap them with RepaintBoundary to avoid affecting other widgets.
RepaintBoundary(
child: AnimatedWidget(...),
)
7 . Avoid using opacity animation and shadow at the same time: Using these two effects together will reduce performance; shadows can be temporarily removed during animation.
This content originally appeared on DEV Community and was authored by Ge Ji