Flutter provides a powerful animation framework that allows developers to create rich and engaging user experiences. While simple animations can be achieved with basic tweens, more complex animation sequences often require the orchestration of multiple tweens. This involves sequencing animations, combining them in parallel, and controlling their execution to create a seamless visual narrative. This post will guide you through creating complex animation sequences with multiple tweens in Flutter.
Understanding Flutter’s Animation Framework
Before diving into complex sequences, it’s crucial to understand the basic components of Flutter’s animation framework:
- AnimationController: Manages the animation. It is responsible for starting, stopping, and reversing the animation.
- Tween: Defines the start and end values of the animation and interpolates between them.
- Animation: Represents the animation’s value at any given point in time. It listens to the
AnimationController
and provides the current value. - AnimatedWidget: A widget that rebuilds itself whenever the
Animation
it listens to changes its value.
Why Use Multiple Tweens in Animation Sequences?
Using multiple tweens allows you to create nuanced animations that involve changes in multiple properties over time. For instance:
- Fading an object in while simultaneously moving it across the screen.
- Sequencing animations, such as scaling a widget followed by rotating it.
- Creating overlapping animations for a layered effect.
Creating a Basic Animation Sequence
Let’s start by creating a simple sequence where a widget first moves to the right, then fades out.
Step 1: Set Up the AnimationController and Tweens
Initialize the AnimationController
and define the tweens for position and opacity.
import 'package:flutter/material.dart';
class ComplexAnimation extends StatefulWidget {
@override
_ComplexAnimationState createState() => _ComplexAnimationState();
}
class _ComplexAnimationState extends State with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _positionAnimation;
late Animation _opacityAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_positionAnimation = Tween(
begin: Offset.zero,
end: const Offset(1.5, 0.0), // Move to the right
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
_opacityAnimation = Tween(
begin: 1.0,
end: 0.0, // Fade out
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.5, 1.0, curve: Curves.easeOut), // Starts fading after 50% of the duration
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Complex Animation Sequence'),
),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _opacityAnimation.value,
child: SlideTransition(
position: _positionAnimation,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.forward();
},
child: Icon(Icons.play_arrow),
),
);
}
}
In this example:
_positionAnimation
moves the widget horizontally._opacityAnimation
fades the widget out. It uses anInterval
curve to start fading only after 50% of the total duration.AnimatedBuilder
listens to the controller and applies both animations.
Step 2: Trigger the Animation
The FloatingActionButton
starts the animation when pressed.
Combining Animations in Parallel
To run animations concurrently, you can combine them within the same AnimatedBuilder
.
Example: Scaling and Rotating a Widget Simultaneously
import 'package:flutter/material.dart';
class ParallelAnimation extends StatefulWidget {
@override
_ParallelAnimationState createState() => _ParallelAnimationState();
}
class _ParallelAnimationState extends State with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation _scaleAnimation;
late Animation _rotationAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_scaleAnimation = Tween(
begin: 1.0,
end: 2.0, // Scale up
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
_rotationAnimation = Tween(
begin: 0.0,
end: 360.0 * (3.14159265359 / 180), // Rotate 360 degrees (in radians)
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Parallel Animations'),
),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value,
child: Container(
width: 100,
height: 100,
color: Colors.green,
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.forward();
},
child: Icon(Icons.play_arrow),
),
);
}
}
Here, both _scaleAnimation
and _rotationAnimation
run simultaneously, controlled by the same AnimationController
.
Sequencing Animations with Future.delayed
and AnimationController.addStatusListener
For sequential animations, you can use Future.delayed
or AnimationController.addStatusListener
to trigger subsequent animations upon completion of the previous ones.
Using Future.delayed
import 'package:flutter/material.dart';
class SequentialAnimation extends StatefulWidget {
@override
_SequentialAnimationState createState() => _SequentialAnimationState();
}
class _SequentialAnimationState extends State with SingleTickerProviderStateMixin {
late AnimationController _controller1;
late AnimationController _controller2;
late Animation _positionAnimation;
late Animation _opacityAnimation;
@override
void initState() {
super.initState();
_controller1 = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_controller2 = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_positionAnimation = Tween(
begin: Offset.zero,
end: const Offset(1.0, 0.0), // Move to the right
).animate(
CurvedAnimation(
parent: _controller1,
curve: Curves.easeInOut,
),
);
_opacityAnimation = Tween(
begin: 1.0,
end: 0.0, // Fade out
).animate(
CurvedAnimation(
parent: _controller2,
curve: Curves.easeOut,
),
);
}
@override
void dispose() {
_controller1.dispose();
_controller2.dispose();
super.dispose();
}
Future startAnimations() async {
await _controller1.forward().orCancel;
await Future.delayed(Duration(milliseconds: 500)); // Delay before starting the next animation
await _controller2.forward().orCancel;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sequential Animations'),
),
body: Center(
child: AnimatedBuilder(
animation: Listenable.merge([_controller1, _controller2]),
builder: (context, child) {
return Opacity(
opacity: _opacityAnimation.value,
child: SlideTransition(
position: _positionAnimation,
child: Container(
width: 100,
height: 100,
color: Colors.orange,
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
startAnimations();
},
child: Icon(Icons.play_arrow),
),
);
}
}
In this code:
- Two
AnimationController
instances are used:_controller1
for moving the widget and_controller2
for fading it out. startAnimations
usesFuture.delayed
to create a delay between the two animations.
Using AnimationController.addStatusListener
import 'package:flutter/material.dart';
class SequentialAnimationStatusListener extends StatefulWidget {
@override
_SequentialAnimationStatusListenerState createState() => _SequentialAnimationStatusListenerState();
}
class _SequentialAnimationStatusListenerState extends State with SingleTickerProviderStateMixin {
late AnimationController _controller1;
late AnimationController _controller2;
late Animation _positionAnimation;
late Animation _opacityAnimation;
@override
void initState() {
super.initState();
_controller1 = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_controller2 = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
_positionAnimation = Tween(
begin: Offset.zero,
end: const Offset(1.0, 0.0), // Move to the right
).animate(
CurvedAnimation(
parent: _controller1,
curve: Curves.easeInOut,
),
);
_opacityAnimation = Tween(
begin: 1.0,
end: 0.0, // Fade out
).animate(
CurvedAnimation(
parent: _controller2,
curve: Curves.easeOut,
),
);
_controller1.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller2.forward();
}
});
}
@override
void dispose() {
_controller1.dispose();
_controller2.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sequential Animations (Status Listener)'),
),
body: Center(
child: AnimatedBuilder(
animation: Listenable.merge([_controller1, _controller2]),
builder: (context, child) {
return Opacity(
opacity: _opacityAnimation.value,
child: SlideTransition(
position: _positionAnimation,
child: Container(
width: 100,
height: 100,
color: Colors.purple,
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller1.forward();
},
child: Icon(Icons.play_arrow),
),
);
}
}
Key aspects of this implementation:
- The
addStatusListener
method is attached to_controller1
. - When
_controller1
completes (AnimationStatus.completed
),_controller2.forward()
is called to start the fade-out animation.
Advanced Techniques and Considerations
- Using Curves: Different curves can dramatically change the feel of an animation. Experiment with various curves to achieve the desired effect.
- Chaining with
then()
: For complex sequences, consider creating extension methods onAnimationController
to chain animations usingFuture.then()
. - Error Handling: Ensure your animation logic gracefully handles errors or interruptions.
Conclusion
Creating complex animation sequences with multiple tweens in Flutter involves careful management of AnimationController
instances, tweens, and animation listeners. By combining animations in parallel, sequencing them using Future.delayed
or addStatusListener
, and employing different curves, you can create visually appealing and engaging user experiences. Mastering these techniques allows you to bring your Flutter apps to life with sophisticated animations.