Precise Animation Control with AnimationController
In this tutorial, we’ll learn how to precisely control animations and implement complex animation sequences using Flitter’s AnimationController.
🎯 Learning Goals
After completing this tutorial, you’ll be able to:
- Understand the basic concepts and lifecycle of AnimationController
- Define start and end values using Tween
- Apply easing effects with CurvedAnimation
- Control animation playback, pause, reverse, and repeat
- Execute multiple animations sequentially
- Implement complex animation sequences
🚀 Explicit vs Implicit Animations
The AnimatedContainer, AnimatedOpacity, etc. we’ve learned so far are implicit animations:
- Automatically animate when property values change
- Simple and easy to use
- Limited control functionality
Explicit animations use AnimationController:
- Direct control of animations (start, stop, speed, etc.)
- Can implement complex animation sequences
- Used when more precise control is needed
🎨 AnimationController Basic Concepts
Core Components
- AnimationController: Controls the animation’s progress state
- Tween: Defines start and end values
- Animation: Connects Controller and Tween
- CurvedAnimation: Applies easing effects (optional)
Basic Structure
class MyAnimationState extends State<MyAnimation> {
controller = null;
animation = null;
initState(context) {
super.initState(context);
// 1. Create Controller
this.controller = AnimationController({
duration: 1000 // Duration (milliseconds)
});
// 2. Create Tween and Animation
this.animation = Tween({
begin: 0,
end: 100
}).animated(this.controller);
// 3. Register listener (call setState)
this.controller.addListener(() => {
this.setState();
});
}
dispose() {
// 4. Memory cleanup (required!)
this.controller.dispose();
super.dispose();
}
}
📋 Step-by-Step Practice
Step 1: Basic AnimationController Usage
Let’s directly control a simple size animation:
import { AnimationController, Tween, Animation, CurvedAnimation } from "@meursyphus/flitter";
class BasicControllerAnimation extends StatefulWidget {
createState() {
return new BasicControllerAnimationState();
}
}
class BasicControllerAnimationState extends State<BasicControllerAnimation> {
controller = null;
animation = null;
initState(context) {
super.initState(context);
this.controller = AnimationController({ duration: 1000 });
this.animation = Tween({ begin: 50, end: 200 }).animated(this.controller);
this.controller.addListener(() => {
this.setState();
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
build(context) {
return Column({
children: [
// Control buttons
Row({
children: [
GestureDetector({
onClick: () => this.controller.forward(),
child: Container({
width: 80,
height: 40,
color: '#4CAF50',
child: Text("Start")
})
}),
GestureDetector({
onClick: () => this.controller.reverse(),
child: Container({
width: 80,
height: 40,
color: '#FF5722',
child: Text("Reverse")
})
}),
GestureDetector({
onClick: () => this.controller.reset(),
child: Container({
width: 80,
height: 40,
color: '#9E9E9E',
child: Text("Reset")
})
})
]
}),
// Animated box
Container({
width: this.animation.value,
height: this.animation.value,
color: '#2196F3',
child: Text(`${Math.round(this.animation.value)}px`)
})
]
});
}
}
Step 2: Adding Easing Effects with CurvedAnimation
Let’s apply easing effects to make animations more natural:
class CurvedAnimationExample extends StatefulWidget {
createState() {
return CurvedAnimationExampleState();
}
}
class CurvedAnimationExampleState extends State<CurvedAnimationExample> {
controller = null;
linearAnimation = null;
curvedAnimation = null;
initState(context) {
super.initState(context);
this.controller = AnimationController({ duration: 2000 });
// Linear animation
this.linearAnimation = Tween({
begin: 0,
end: 250
}).animated(this.controller);
// Curved animation
this.curvedAnimation = Tween({
begin: 0,
end: 250
}).animated(CurvedAnimation({
parent: this.controller,
curve: "bounceOut"
}));
this.controller.addListener(() => {
this.setState();
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
build(context) {
return Column({
children: [
GestureDetector({
onClick: () => {
if (this.controller.isCompleted) {
this.controller.reverse();
} else {
this.controller.forward();
}
},
child: Container({
width: 120,
height: 50,
color: '#673AB7',
child: Text("Toggle Animation")
})
}),
// Linear animation
Container({
width: this.linearAnimation.value,
height: 40,
color: '#FF9800',
child: Text("Linear")
}),
// Curved animation
Container({
width: this.curvedAnimation.value,
height: 40,
color: '#E91E63',
child: Text("BounceOut")
})
]
});
}
}
Step 3: Animating Multiple Properties Simultaneously
Let’s animate multiple properties simultaneously with one Controller:
class MultiPropertyAnimation extends StatefulWidget {
createState() {
return new MultiPropertyAnimationState();
}
}
class MultiPropertyAnimationState extends State<MultiPropertyAnimation> {
controller = null;
sizeAnimation = null;
colorAnimation = null;
rotationAnimation = null;
initState(context) {
super.initState(context);
this.controller = AnimationController({ duration: 2000 });
// Size animation
this.sizeAnimation = Tween({
begin: 80,
end: 160
}).animated(CurvedAnimation({
parent: this.controller,
curve: "easeInOut"
}));
// Color animation (0-1 value for color interpolation)
this.colorAnimation = Tween({
begin: 0,
end: 1
}).animated(this.controller);
// Rotation animation
this.rotationAnimation = Tween({
begin: 0,
end: 2
}).animated(CurvedAnimation({
parent: this.controller,
curve: "elasticOut"
}));
this.controller.addListener(() => {
this.setState();
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
getInterpolatedColor() {
const t = this.colorAnimation.value;
const r = Math.round(255 * (1 - t) + 100 * t);
const g = Math.round(150 * (1 - t) + 200 * t);
const b = Math.round(200 * (1 - t) + 50 * t);
return `rgb(${r}, ${g}, ${b})`;
}
build(context) {
return Column({
children: [
GestureDetector({
onClick: () => {
if (this.controller.status === "completed") {
this.controller.reverse();
} else {
this.controller.forward();
}
},
child: Container({
width: 150,
height: 50,
color: '#607D8B',
child: Text("Multi-Property Animation")
})
}),
Transform.rotate({
angle: this.rotationAnimation.value * Math.PI, // Convert to radians
child: Container({
width: this.sizeAnimation.value,
height: this.sizeAnimation.value,
color: this.getInterpolatedColor(),
child: Text("🎨")
})
})
]
});
}
}
Step 4: Sequential Animation (Using Interval)
Let’s create animations that execute sequentially with one Controller:
import { Interval } from "@meursyphus/flitter";
class SequentialAnimation extends StatefulWidget {
createState() {
return new SequentialAnimationState();
}
}
class SequentialAnimationState extends State<SequentialAnimation> {
controller = null;
slideAnimation = null;
fadeAnimation = null;
scaleAnimation = null;
initState(context) {
super.initState(context);
this.controller = AnimationController({ duration: 3000 });
// 0~1 second: Slide animation
this.slideAnimation = Tween({
begin: -200,
end: 0
}).animated(CurvedAnimation({
parent: this.controller,
curve: new Interval(0.0, 0.33, { curve: "easeOut" })
}));
// 1~2 seconds: Fade animation
this.fadeAnimation = Tween({
begin: 0.0,
end: 1.0
}).animated(CurvedAnimation({
parent: this.controller,
curve: new Interval(0.33, 0.66, { curve: "easeIn" })
}));
// 2~3 seconds: Scale animation
this.scaleAnimation = Tween({
begin: 0.5,
end: 1.0
}).animated(CurvedAnimation({
parent: this.controller,
curve: new Interval(0.66, 1.0, { curve: "bounceOut" })
}));
this.controller.addListener(() => {
this.setState();
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
build(context) {
return Column({
children: [
GestureDetector({
onClick: () => {
this.controller.reset();
this.controller.forward();
},
child: Container({
width: 150,
height: 50,
color: '#795548',
child: Text("Start Sequential Animation")
})
}),
Transform.translate({
offset: new Offset(this.slideAnimation.value, 0),
child: Transform.scale({
scale: this.scaleAnimation.value,
child: AnimatedOpacity({
duration: 0, // Immediate change (Controller controls)
opacity: this.fadeAnimation.value,
child: Container({
width: 120,
height: 80,
color: '#009688',
child: Text("Sequential Appear!")
})
})
})
})
]
});
}
}
Step 5: Repeating Animation
Let’s create animations that repeat automatically:
class RepeatAnimation extends StatefulWidget {
createState() {
return new RepeatAnimationState();
}
}
class RepeatAnimationState extends State<RepeatAnimation> {
controller = null;
animation = null;
isRunning = false;
initState(context) {
super.initState(context);
this.controller = AnimationController({ duration: 1000 });
this.animation = Tween({ begin: -50, end: 50 }).animated(
CurvedAnimation({
parent: this.controller,
curve: "easeInOut"
})
);
this.controller.addListener(() => {
this.setState();
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
startRepeating() {
this.isRunning = true;
this.controller.repeat({ reverse: true });
}
stopRepeating() {
this.isRunning = false;
this.controller.stop();
}
build(context) {
return Column({
children: [
Row({
children: [
GestureDetector({
onClick: () => this.startRepeating(),
child: Container({
width: 80,
height: 40,
color: '#4CAF50',
child: Text("Start")
})
}),
GestureDetector({
onClick: () => this.stopRepeating(),
child: Container({
width: 80,
height: 40,
color: '#F44336',
child: Text("Stop")
})
})
]
}),
Container({
width: 200,
height: 100,
color: '#F5F5F5',
child: Stack({
children: [
Positioned({
left: 100 + this.animation.value,
top: 25,
child: Container({
width: 50,
height: 50,
color: '#FF5722',
child: Text("🏃♂️")
})
})
]
})
})
]
});
}
}
🎯 Practice Challenges
TODO 1: Animation with Progress Display
Create an animation with a progress bar that shows animation progress:
class ProgressAnimation extends StatefulWidget {
createState() {
return new ProgressAnimationState();
}
}
class ProgressAnimationState extends State<ProgressAnimation> {
controller = null;
animation = null;
initState(context) {
super.initState(context);
this.controller = AnimationController({ duration: 3000 });
this.animation = Tween({ begin: 0, end: 300 }).animated(
CurvedAnimation({
parent: this.controller,
curve: "easeInOut"
})
);
this.controller.addListener(() => {
this.setState();
});
}
dispose() {
this.controller.dispose();
super.dispose();
}
build(context) {
const progress = this.controller.value; // 0.0 ~ 1.0
return Column({
children: [
// Progress display
Container({
width: 300,
height: 20,
color: '#E0E0E0',
child: Container({
width: 300 * progress,
height: 20,
color: '#4CAF50'
})
}),
Text(`Progress: ${Math.round(progress * 100)}%`),
// Control buttons
Row({
children: [
GestureDetector({
onClick: () => this.controller.forward(),
child: Container({
width: 60,
height: 40,
color: '#2196F3',
child: Text("▶")
})
}),
GestureDetector({
onClick: () => this.controller.reverse(),
child: Container({
width: 60,
height: 40,
color: '#FF9800',
child: Text("◀")
})
}),
GestureDetector({
onClick: () => this.controller.stop(),
child: Container({
width: 60,
height: 40,
color: '#F44336',
child: Text("⏸")
})
}),
GestureDetector({
onClick: () => this.controller.reset(),
child: Container({
width: 60,
height: 40,
color: '#9E9E9E',
child: Text("⏹")
})
})
]
}),
// Animated element
Container({
width: this.animation.value,
height: 60,
color: '#E91E63',
child: Text(`${Math.round(this.animation.value)}px`)
})
]
});
}
}
TODO 2: Multi-Step Animation Sequence
Create a multi-step animation that proceeds to the next step with each button click:
class MultiStepAnimation extends StatefulWidget {
createState() {
return new MultiStepAnimationState();
}
}
class MultiStepAnimationState extends State<MultiStepAnimation> {
controllers = [];
animations = [];
currentStep = 0;
steps = [
{ name: "Appear", duration: 500, curve: "easeOut" },
{ name: "Rotate", duration: 800, curve: "elasticOut" },
{ name: "Scale", duration: 600, curve: "bounceOut" },
{ name: "Fade", duration: 400, curve: "easeIn" }
];
initState(context) {
super.initState(context);
// Create controllers for each step
this.steps.forEach((step, index) => {
const controller = AnimationController({ duration: step.duration });
controller.addListener(() => this.setState());
this.controllers.push(controller);
});
// Define animations for each step
this.animations = [
Tween({ begin: -200, end: 0 }).animated(
CurvedAnimation({
parent: this.controllers[0],
curve: "easeOut"
})
),
Tween({ begin: 0, end: 1 }).animated(
CurvedAnimation({
parent: this.controllers[1],
curve: "elasticOut"
})
),
Tween({ begin: 1, end: 2 }).animated(
CurvedAnimation({
parent: this.controllers[2],
curve: "bounceOut"
})
),
Tween({ begin: 1, end: 0 }).animated(
CurvedAnimation({
parent: this.controllers[3],
curve: "easeIn"
})
)
];
}
dispose() {
this.controllers.forEach(controller => controller.dispose());
super.dispose();
}
nextStep() {
if (this.currentStep < this.steps.length) {
this.controllers[this.currentStep].forward();
this.currentStep++;
}
}
reset() {
this.controllers.forEach(controller => controller.reset());
this.currentStep = 0;
this.setState();
}
build(context) {
return Column({
children: [
Text(`Current Step: ${this.currentStep} / ${this.steps.length}`),
Row({
children: [
GestureDetector({
onClick: () => this.nextStep(),
child: Container({
width: 100,
height: 40,
color: this.currentStep < this.steps.length ? '#4CAF50' : '#BDBDBD',
child: Text("Next Step")
})
}),
GestureDetector({
onClick: () => this.reset(),
child: Container({
width: 80,
height: 40,
color: '#FF5722',
child: Text("Reset")
})
})
]
}),
// Animation element
Transform.translate({
offset: new Offset(this.animations[0].value, 0),
child: Transform.rotate({
angle: this.animations[1].value * Math.PI,
child: Transform.scale({
scale: this.animations[2].value,
child: AnimatedOpacity({
duration: 0,
opacity: this.animations[3].value,
child: Container({
width: 80,
height: 80,
color: '#9C27B0',
child: Text("🎭")
})
})
})
})
})
]
});
}
}
🎨 Expected Results
When completed, the following features should work:
- Basic control: Control animations with forward, reverse, reset
- Curve effects: See the difference between linear and curved animations
- Multiple properties: Size, color, and rotation animate simultaneously
- Sequential execution: Execute in order: slide → fade → scale
- Repeating animation: Automatically repeat back and forth
- Progress display: Check animation progress in real-time
💡 Additional Challenges
For more challenges:
- Complex sequences: Complex animation sequences with 5+ steps
- Interactive control: Direct control of animation progress with drag
- Conditional animations: Execute different animations based on specific conditions
- Physics-based: Simulate spring effects or gravity effects
⚠️ Common Mistakes and Solutions
1. Forgetting dispose()
// ❌ No dispose - Memory leak!
dispose() {
super.dispose(); // Missing controller.dispose()
}
// ✅ Always call dispose
dispose() {
this.controller.dispose();
super.dispose();
}
2. Not Calling setState in addListener
// ❌ No setState - UI won't update
this.controller.addListener(() => {
// No setState() call
});
// ✅ Update UI with setState
this.controller.addListener(() => {
this.setState();
});
3. Wrong Tween Range Settings
// ❌ Unintended range
Tween({ begin: 100, end: 0 }) // Decreasing animation
// ✅ Check intended range
Tween({ begin: 0, end: 100 }) // Increasing animation
4. Interval Range Errors
// ❌ Wrong range (exceeds 0.0~1.0)
new Interval(0.5, 1.5) // Exceeds 1.0
// ✅ Correct range
new Interval(0.0, 0.5) // First half
new Interval(0.5, 1.0) // Second half
5. Confusing Rotation Angle Units
// ❌ Using degree units
Transform.rotate({ angle: 90 }) // Not 90 degrees!
// ✅ Using radian units
Transform.rotate({ angle: Math.PI / 2 }) // 90 degrees
🎓 Key Summary
AnimationController Lifecycle
- initState(): Create Controller, set up Animation, register Listener
- build(): Build UI using animation.value
- dispose(): Release Controller memory (required!)
Control Methods
- forward(): Animate from start → end
- reverse(): Animate from end → start (reverse direction)
- reset(): Move to start position immediately
- stop(): Stop at current position
- repeat(): Repeating animation
- animateTo(value): Animate to specific value
Status Check
- controller.value: Current progress (0.0 ~ 1.0)
- controller.status: Animation status
- controller.isCompleted: Whether completed
- controller.isAnimating: Whether in progress
AnimationController is the most powerful animation tool in Flitter. It’s essential for implementing advanced animations that require precise control and complex sequences.
🚀 Next Steps
In the next tutorial, let’s learn Custom Animations:
- Creating reusable animation widgets by inheriting AnimatedWidget
- Combining CustomPaint with animations
- Implementing physics-based animations
- Applying performance optimization techniques