This article is vol 3 of a series of articles on animation.
Interval
and multiple TickerProviders
In vol 2, we looked at two use cases, and here we will look at the remaining use cases
Multiple animation effects on a single widget at the same timeSequenced animation on a single widget- A single widget with different animations at different times
- Staggered animation for multiple widgets
- Animate multiple Widgets separately
A single widget with different animations at different times -Interval
-.
In the vol2 article, I used a TweenSequence
to apply sequenced animation of single animation effect to a widget.
Then how can we apply different animations multiple times instead of the same animation effect?
You can use Interval
in this case.
Interval
class
Normally, when an Animation
is generated using an AnimationController
and tween
, value will change by its start value to the end value defined in the Tween
, using up entire AnimationController's Duration time.
By using Interval
, you can cut out the specified time within the AnimationController's progress (0 to 1) and apply the animation within that time.
How to use it
1. define the start timing (begin
) and end timing (end
)
In Interval
, the first and second arguments specifies the start and end timing of AnimationController's progress that you want to cut out.
Since the progress of AnimationController is double
from 0 to 1.0, the start and end timing is also specified in double value.
For example, if Duration
is 4 seconds, Interval
with 0 and 0.5 will apply it's Animation
from 0 to 2 seconds.
Interval(
0, // begin
0.5, // end
curve: Curves.ease,
)
It also accepts a curve
as its third argument, so you can add effects to the changes.
2. bind to CurvedAnimation
.
When using Interval
class, use a class called CurvedAnimation
to bind it to AnimationController
.
CurvedAnimation
is a class that can add curve
to animation, but it is mainly used to add curve
to AnimationController
.
By passing Interval
, it creates an Animation
that is only applied between its start and end timing.
CurvedAnimation(
parent: controller,
curve: Interval(
0,
0.5,
curve: Curves.ease,
),
)
3. generate Animation
using AnimationController.drive
method
Generate Animation
using drive
method from AnimationController
and bind it with Tween
.
Animation = CurvedAnimation(
parent: controller,
curve: Interval(
0,
0.5,
curve: Curves.ease,
),
).drive(Tween);
4. Bind the Animation to the Widget
All that is left to do is to attach the animation to the Widget, just like previous examples.
The Animation
class generated with Interval
will apply the animation to the Widget only during the defined start and end times.
Which means?
You can use Interval
to specify the timing for multiple animations to move at different timing.
class _ChainedAnimationState extends State<ChainedAnimation>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late Tween<Alignment> alignTween;
late Tween<double> rotateTween;
late Tween<double> opacityTween;
late Animation<Alignment> alignAnimation;
late Animation<double> rotateAnimation;
late Animation<double> opacityAnimation;
bool animateCompleted = false;
@override
void initState() {
controller =
AnimationController(duration: const Duration(seconds: 4), vsync: this);
// will apply align、rotation, opacity animation to single widget
alignTween = Tween(begin: Alignment.topCenter, end: Alignment.center);
rotateTween = Tween(begin: 0, end: pi * 8);
opacityTween = Tween(begin: 1, end: 0);
alignAnimation = CurvedAnimation(
parent: controller,
// 1. define start and end timing for Interval
curve: const Interval(0, 0.5, curve: Curves.ease),
).drive(alignTween);
// 2. bind Interval to AnimationController using CurvedAnimation
rotateAnimation = CurvedAnimation(
parent: controller,
curve: const Interval(0.5, 0.7, curve: Curves.ease),
).drive(rotateTween);
// 3. create Animation by AnimationController x Tween
opacityAnimation = CurvedAnimation(
parent: controller,
curve: const Interval(0.7, 1, curve: Curves.ease),
).drive(opacityTween);
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.red[300],
title: const Text('Chained Animation'),
),
drawer: const MainDrawer(),
body: AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Opacity(
opacity: opacityAnimation
.value, // <<< 4. bind animation created using Interval
child: Align(
alignment: alignAnimation.value, // <<< 4.
child: Transform.rotate(
angle: rotateAnimation.value, // <<< 4.
child: const Text('Hello world!'),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (!animateCompleted) {
controller.forward().whenComplete(() {
setState(() => animateCompleted = true);
});
return;
}
controller.reverse().whenComplete(
() {
setState(() => animateCompleted = false);
},
);
},
backgroundColor: Colors.yellow[700],
child: const Icon(
Icons.bolt,
color: Colors.black,
),
),
);
}
}
Staggered animation of multiple widgets
Animations which animate in staggered manner is called staggered animation
This staggered Animation can also be implemented using Interval
!
In conclusion, all you have to do is, create an Interval
that defines the timing of each widget's movement, and attach the generated Animation
class to each widget.
The only difference with the previous use case is that you are binding Animation
to different widgets.
class _StaggeredAnimationState extends State<StaggeredAnimation>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late Tween<Offset> offsetTween;
late Animation<Offset> offsetAnimation1;
late Animation<Offset> offsetAnimation2;
late Animation<Offset> offsetAnimation3;
bool animateCompleted = false;
@override
void initState() {
controller = AnimationController(
duration: const Duration(milliseconds: 1500), vsync: this);
// prepare single Tween that would be applied to all of the Animations
offsetTween = Tween(begin: const Offset(-1000, 0), end: Offset.zero);
offsetAnimation1 = CurvedAnimation(
parent: controller,
// 1. define start and end time for each Intervals
curve: const Interval(0, 0.3, curve: Curves.ease),
).drive(offsetTween);
// 2. bind Interval to AnimationController using CurvedAnimation
offsetAnimation2 = CurvedAnimation(
parent: controller,
curve: const Interval(0.3, 0.7, curve: Curves.ease),
).drive(offsetTween);
// 3. create Animation from AnimationController and Tween
offsetAnimation3 = CurvedAnimation(
parent: controller,
curve: const Interval(0.7, 1, curve: Curves.ease),
).drive(offsetTween);
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.purple[300],
title: const Text('Staggered Animation'),
),
drawer: const MainDrawer(),
body: AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 200, horizontal: 60),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Transform.translate(
offset: offsetAnimation1
.value, // 4. bind Animation created with each Interval
child: const Text('Hello world!'),
),
Transform.translate(
offset: offsetAnimation2.value, // 4.
child: const Text('My name is ...'),
),
Transform.translate(
offset: offsetAnimation3.value, // 4.
child: const Text('heyhey1028!!'),
),
],
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (!animateCompleted) {
controller.forward().whenComplete(() {
setState(() => animateCompleted = true);
});
return;
}
controller.reverse().whenComplete(
() {
setState(() => animateCompleted = false);
},
);
},
backgroundColor: Colors.yellow[700],
child: const Icon(
Icons.bolt,
color: Colors.black,
),
),
);
}
}
You can adjust the start and end timing defined in Interval
to control how far apart the chain moves.
Animate multiple Widgets separately
In the previous examples, a single AnimationController
handled multiple widgets or animation effects
So by forwarding that AnimationController
, all animations moved at the same time. But what if you want to move multiple animations separately?
Using TickerProviderStateMixin
In these cases, you need to prepare multiple AnimationControllers
However, SingleTickerProviderStateMixin
we have been using until now do not allow us to generate multiple AnimationControllers
.
In these cases, you can mixin TickerProviderStateMixin
, which can supply multiple TickerProviders
, to State
class.
Use Listenable.merge
Another point is to use Listenable.merge
on the animation
property of the AnimatedBuilder
.
When there are multiple AnimationControllers
like this one, the AnimatedBuilder
needs to monitor the state of all of them and redraw the child widgets, so use Listenable.merge
to make the AnimationController
into a single class
// mixin TickerProviderStateMixin that could provide multiple tickers
class _MultipleTickerProviderState extends State<MultipleTickerProvider>
with TickerProviderStateMixin {
// to animate separately, prepare a multiple AnimationControllers
late AnimationController alignController;
late AnimationController rotateController;
late TweenSequence<Alignment> alignTween;
late Tween<double> rotateTween;
late Animation<Alignment> alignmAnimation;
late Animation<double> rotateAnimation;
bool animatingAlign = false;
bool animatingRotation = false;
@override
void initState() {
// define duration and vsync for each AnimationController separately
rotateController = AnimationController(
duration: const Duration(milliseconds: 1500), vsync: this);
alignController =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
// define Tween for each Animation
rotateTween = Tween(begin: 0, end: 8 * pi);
alignTween = TweenSequence<Alignment>(
[
TweenSequenceItem(
tween: Tween(
begin: Alignment.center,
end: Alignment.topCenter,
),
weight: 0.3,
),
TweenSequenceItem(
tween: Tween(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
weight: 0.4,
),
TweenSequenceItem(
tween: Tween(
begin: Alignment.bottomCenter,
end: Alignment.center,
),
weight: 0.3,
),
],
);
// create Animation using each AnimationController
alignmAnimation = alignController.drive(alignTween);
rotateAnimation = rotateController.drive(rotateTween);
super.initState();
}
@override
void dispose() {
rotateController.dispose();
alignController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.brown[300],
title: const Text('Multiple Ticker Provider'),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
drawer: const MainDrawer(),
body: AnimatedBuilder(
// wrap Animations with Listenable.merge to listen to multiple animation
animation: Listenable.merge([
rotateController,
alignController,
]),
builder: (context, _) {
return Stack(
fit: StackFit.expand,
children: [
Align(
alignment: alignmAnimation.value, // bind animation to widget
child: Transform.rotate(
angle: rotateAnimation.value,
child: const Text('Hello world!!'),
),
)
],
);
},
),
// trigger multiple AnimationControllers separately
floatingActionButton: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
if (!animatingAlign) {
alignController.repeat();
setState(() => animatingAlign = true);
return;
}
alignController.stop();
setState(() => animatingAlign = false);
},
heroTag: 'align',
backgroundColor: Colors.yellow[700],
child: const Icon(
Icons.double_arrow,
color: Colors.black,
),
),
const SizedBox(width: 20),
FloatingActionButton(
onPressed: () {
if (!animatingRotation) {
rotateController.repeat();
setState(() => animatingRotation = true);
return;
}
rotateController.stop();
setState(() => animatingRotation = false);
},
heroTag: 'rotate',
backgroundColor: Colors.yellow[700],
child: const Icon(
Icons.cyclone,
color: Colors.black,
),
),
],
),
),
);
}
}
Sample Repository
https://github.com/heyhey1028/flutter_samples/tree/main/samples/master_animation
That's it!!
I have explained five use cases for complex animations. I believe that many complex animations can be implemented by combining these use cases as a basis.
After going through examples, you might have understood that animation is quite straight forward.
What's next?
However, when you actually start implementing animations, often times, you feel overwhelmed.
In the next article, we will discuss why animation can get overwhelming easily.
https://dev.to/heyhey1028/flutter-mastering-animation-4-the-confusing-parts-of-animations-3cp3
Top comments (0)