Animations play a vital role in creating engaging and visually appealing user interfaces in mobile applications. Flutter, a popular open-source UI framework by Google, offers a robust set of tools for creating smooth and expressive animations.
In this comprehensive guide, we'll explore the world of Flutter animations, from the basics to more advanced techniques, accompanied by code samples to help you get started.
1. Introduction to Flutter Animations
Why Animations Matter
Animations provide a more dynamic and engaging user experience, guiding users through interface changes and interactions. They help convey important information, enhance the overall aesthetic of the app, and make interactions more intuitive.
Types of Animations in Flutter
Flutter offers several animation types:
Implicit Animations: These animations are built into existing widgets and can be triggered using widget properties, like AnimatedContainer or AnimatedOpacity.
Tween Animations: These animations interpolate between two values over a specified duration using Tween objects.
Physics-Based Animations: These animations simulate real-world physics, like springs or flings, to create natural-looking motion.
Custom Animations: For more complex scenarios, you can create your own custom animations using CustomPainter and AnimationController.
In this guide, we'll cover examples from each category to give you a well-rounded understanding of Flutter animations.
2. Basic Animations
Animated Container
The AnimatedContainer widget is a straightforward way to animate changes to a container's properties, such as its size, color, and alignment.
class BasicAnimatedContainer extends StatefulWidget {
@override
_BasicAnimatedContainerState createState() => _BasicAnimatedContainerState();
}
class _BasicAnimatedContainerState extends State<BasicAnimatedContainer> {
double _width = 100.0;
double _height = 100.0;
Color _color = Colors.blue;
void _animateContainer() {
setState(() {
_width = 200.0;
_height = 200.0;
_color = Colors.red;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _animateContainer,
child: Center(
child: AnimatedContainer(
duration: Duration(seconds: 1),
width: _width,
height: _height,
color: _color,
),
),
);
}
}
Animated Opacity
The AnimatedOpacity widget allows you to animate the opacity of a widget, making it appear or disappear smoothly.
class BasicAnimatedOpacity extends StatefulWidget {
@override
_BasicAnimatedOpacityState createState() => _BasicAnimatedOpacityState();
}
class _BasicAnimatedOpacityState extends State<BasicAnimatedOpacity> {
bool _visible = true;
void _toggleVisibility() {
setState(() {
_visible = !_visible;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedOpacity(
duration: Duration(seconds: 1),
opacity: _visible ? 1.0 : 0.0,
child: FlutterLogo(size: 150.0),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _toggleVisibility,
child: Text(_visible ? "Hide Logo" : "Show Logo"),
),
],
);
}
}
3. Tween Animations
Animating Widgets with Tween
Tween animations interpolate between two values over a specified duration. Here's an example of animating the position of a widget using a Tween:
class TweenAnimation extends StatefulWidget {
@override
_TweenAnimationState createState() => _TweenAnimationState();
}
class _TweenAnimationState extends State<TweenAnimation> {
double _endValue = 200.0;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_endValue = _endValue == 200.0 ? 100.0 : 200.0;
});
},
child: Center(
child: TweenAnimationBuilder(
tween: Tween<double>(begin: 100.0, end: _endValue),
duration: Duration(seconds: 1),
builder: (BuildContext context, double value, Widget? child) {
return Container(
width: value,
height: value,
color: Colors.blue,
);
},
),
),
);
}
}
Tween Animation Builder
The TweenAnimationBuilder widget is a versatile tool for building animations with Tweens. It allows you to define the tween, duration, and a builder function to create the animated widget.
4. Physics-Based Animations
Using AnimatedBuilder with Curves
Curves define the rate of change in an animation, affecting its acceleration and deceleration. The CurvedAnimation class allows you to apply curves to your animations. Here's an example of using AnimatedBuilder with a curve:
class CurvedAnimationDemo extends StatefulWidget {
@override
_CurvedAnimationDemoState 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: Duration(seconds: 2),
);
final Animation curveAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_animation = Tween<double>(begin: 0, end: 200).animate(curveAnimation);
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _animation,
builder: (BuildContext context, Widget? child) {
return Container(
width: _animation.value,
height: 100,
color: Colors.blue,
);
},
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Creating a Spring Animation
Spring animations simulate the behavior of a spring, creating a bounce-like effect. Flutter provides the SpringSimulation class for this purpose. Here's an example of creating a spring animation:
class SpringAnimationDemo extends StatefulWidget {
@override
_SpringAnimationDemoState createState() => _SpringAnimationDemoState();
}
class _SpringAnimationDemoState extends State<SpringAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
final SpringDescription spring = SpringDescription(
mass: 1,
stiffness: 500,
damping: 20,
);
final SpringSimulation springSimulation = SpringSimulation(
spring,
_controller.value,
1, // The target value
0, // The velocity
);
_animation = Tween<Offset>(begin: Offset.zero, end: Offset(2, 0))
.animate(_controller);
_controller.animateWith(springSimulation);
}
@override
Widget build(BuildContext context) {
return Center(
child: SlideTransition(
position: _animation,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
5. Complex Animations
Staggered Animations
Staggered animations involve animating multiple widgets with different delays, creating an appealing sequence. The StaggeredAnimation class manages this behavior. Here's an example:
class StaggeredAnimationDemo extends StatefulWidget {
@override
_StaggeredAnimationDemoState createState() => _StaggeredAnimationDemoState();
}
class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo>
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
final StaggeredAnimation staggeredAnimation = StaggeredAnimation(
controller: _controller,
itemCount: 3,
);
_animation = Tween<double>(begin: 0, end: 200).animate(staggeredAnimation);
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: ListView.builder(
itemCount: 3,
itemBuilder: (BuildContext context, int index) {
return FadeTransition(
opacity: _animation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: _animation.value,
height: 100,
color: Colors.blue,
),
),
);
},
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class StaggeredAnimation extends Animatable<double> {
final AnimationController controller;
final int itemCount;
StaggeredAnimation({
required this.controller,
required this.itemCount,
}) : super();
@override
double transform(double t) {
int itemCount = this.itemCount;
double fraction = 1.0 / itemCount;
return (t * itemCount).clamp(0.0, itemCount - 1).toDouble() * fraction;
}
}
Hero Animations
Hero animations are used to smoothly transition a widget between two screens. They provide a seamless experience as the widget scales and moves from one screen to another. Here's an example:
class HeroAnimationDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hero Animation'),
),
body: Center(
child: Hero(
tag: 'hero-tag',
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
);
},
),
);
},
child: Scaffold(
appBar: AppBar(
title: const Text('Hero Animation'),
),
body: Center(
child: Hero(
tag: 'hero-tag',
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
),
);
}
}
6. Implicit Animations
Animated CrossFade
The AnimatedCrossFade widget smoothly transitions between two children while crossfading between them. It's useful for scenarios like toggling between two pieces of content.
class CrossFadeDemo extends StatefulWidget {
@override
_CrossFadeDemoState createState() => _CrossFadeDemoState();
}
class _CrossFadeDemoState extends State<CrossFadeDemo> {
bool _showFirst = true;
void _toggle() {
setState(() {
_showFirst = !_showFirst;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedCrossFade(
firstChild: FlutterLogo(size: 150),
secondChild: Container(color: Colors.blue, width: 150, height: 150),
crossFadeState:
_showFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: Duration(seconds: 1),
),
ElevatedButton(
onPressed: _toggle,
child: Text(_showFirst ? 'Show Second' : 'Show First'),
),
],
);
}
}
Animated Switcher
The AnimatedSwitcher widget allows smooth transitions between different children based on a key. It's commonly used for transitions like swapping widgets.
class SwitcherDemo extends StatefulWidget {
@override
_SwitcherDemoState createState() => _SwitcherDemoState();
}
class _SwitcherDemoState extends State<SwitcherDemo> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(seconds: 1),
child: Text(
'$_counter',
key: ValueKey<int>(_counter),
style: TextStyle(fontSize: 48),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}
7. Custom Animations
CustomPainter and AnimationController
The combination of CustomPainter and AnimationController allows you to create complex animations and draw custom shapes. Here's an example of a rotating custom animation using CustomPainter:
class CustomPainterAnimation extends StatefulWidget {
@override
_CustomPainterAnimationState createState() => _CustomPainterAnimationState();
}
class _CustomPainterAnimationState extends State<CustomPainterAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return CustomPaint(
painter: RotatingPainter(_controller.value),
child: Container(width: 150, height: 150),
);
},
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class RotatingPainter extends CustomPainter {
final double rotation;
RotatingPainter(this.rotation);
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
canvas.rotate(rotation * 2 * pi);
final rect = Rect.fromCenter(
center: Offset(0, 0),
width: size.width * 0.8,
height: size.height * 0.8,
);
final paint = Paint()..color = Colors.blue;
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
Creating a Flip Card Animation
Using a combination of Transform, GestureDetector, and AnimationController, you can create a flip card animation.
class FlipCardDemo extends StatefulWidget {
@override
_FlipCardDemoState createState() => _FlipCardDemoState();
}
class _FlipCardDemoState extends State<FlipCardDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
bool _isFront = true;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
}
void _flipCard() {
if (_isFront) {
_controller.forward();
} else {
_controller.reverse();
}
_isFront = !_isFront;
}
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: _flipCard,
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
final double rotationValue = _controller.value;
final double rotationAngle = _isFront ? rotationValue : (1 - rotationValue);
final frontRotation = Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(pi * rotationAngle);
final backRotation = Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(pi * (rotationAngle - 1));
return Stack(
children: [
_buildCard(frontRotation, 'Front', Colors.blue),
_buildCard(backRotation, 'Back', Colors.red),
],
);
},
),
),
);
}
Widget _buildCard(Matrix4 transform, String text, Color color) {
return Center(
child: Transform(
transform: transform,
alignment: Alignment.center,
child: Container(
width: 200,
height: 300,
color: color,
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(fontSize: 24, color: Colors.white),
),
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
8. Performance Optimization
Using the AnimationController's vsync
When creating an AnimationController, it's essential to provide a vsync parameter. This parameter helps in syncing the animation frame rate with the device's refresh rate, enhancing performance and reducing unnecessary updates.
class VsyncAnimation extends StatefulWidget {
@override
_VsyncAnimationState createState() => _VsyncAnimationState();
}
class _VsyncAnimationState extends State<VsyncAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, // Pass `this` as the vsync parameter
duration: Duration(seconds: 2),
);
}
// ...
}
Avoiding Unnecessary Rebuilds
To avoid unnecessary rebuilds of widgets, you can use AnimatedBuilder or ValueListenableBuilder. These widgets rebuild only when the animation value changes, improving overall performance.
class AvoidRebuildsDemo extends StatefulWidget {
@override
_AvoidRebuildsDemoState createState() => _AvoidRebuildsDemoState();
}
class _AvoidRebuildsDemoState extends State<AvoidRebuildsDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
_animation = Tween<double>(begin: 0, end: 1).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Center(
child: ValueListenableBuilder(
valueListenable: _animation,
builder: (BuildContext context, double value, Widget? child) {
return Transform.scale(
scale: value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
},
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
9. Chaining and Sequencing Animations
Using Future.delayed
You can chain animations by using Future.delayed. This creates a delayed effect, allowing one animation to start after the previous one completes.
class DelayedAnimationDemo extends StatefulWidget {
@override
_DelayedAnimationDemoState createState() => _DelayedAnimationDemoState();
}
class _DelayedAnimationDemoState extends State<DelayedAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation1;
late Animation<double> _animation2;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
_animation1 = Tween<double>(begin: 0, end: 1).animate(_controller);
_animation2 = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0), // Starts after the first animation
),
);
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ScaleTransition(
scale: _animation1,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
SizedBox(height: 20),
ScaleTransition(
scale: _animation2,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Using AnimationController's addListener
The addListener method of AnimationController can be used to sequence animations, triggering the second animation when the first animation completes.
class SequenceAnimationDemo extends StatefulWidget {
@override
_SequenceAnimationDemoState createState() => _SequenceAnimationDemoState();
}
class _SequenceAnimationDemoState extends State<SequenceAnimationDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation1;
late Animation<double> _animation2;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
_animation1 = Tween<double>(begin: 0, end: 1).animate(_controller)
..addListener(() {
if (_animation1.isCompleted) {
_controller.reset(); // Reset the controller to restart
_controller.forward(); // Start the second animation
}
});
_animation2 = Tween<double>(begin: 0, end: 1).animate(_controller);
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ScaleTransition(
scale: _animation1,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
SizedBox(height: 20),
ScaleTransition(
scale: _animation2,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
10. Conclusion and Further Learning
Flutter's animation capabilities allow you to create stunning, dynamic user interfaces that engage users and enhance their experience. This guide covered a wide range of animation techniques, from basic animations and tween animations to physics-based simulations and complex custom animations.
As you continue your journey with Flutter animations, consider exploring more advanced topics like Flare animations for vector graphics, using Rive for more complex animations, and experimenting with implicit animations for seamless UI changes.
Remember, mastering Flutter animations takes practice and experimentation. With dedication and creativity, you can bring your app's UI to life and create memorable user experiences that leave a lasting impression.
Happy animating! 🚀
Note: The code samples provided in this blog post are simplified for illustrative purposes. Actual implementation may require additional considerations and optimizations.
Comments