Smooth Animations with AnimatedContainer
In this tutorial, we’ll learn how to create smooth animations that bring life to web pages using Flitter’s AnimatedContainer.
🎯 Learning Goals
After completing this tutorial, you’ll be able to:
- Understand the basic usage of AnimatedContainer
- Control animation speed and feel with duration and curve
- Implement size, color, and position animations
- Combine StatefulWidget with AnimatedContainer
- Trigger animations through user interactions
🚀 What are Animations?
Animations greatly enhance user experience on the web. Buttons smoothly changing when clicked, cards slightly expanding on hover - these are all examples of animations.
Flitter provides two types of animations:
- Implicit animations: AnimatedContainer, AnimatedOpacity, etc. (what we’ll learn today)
- Explicit animations: Using AnimationController (next tutorial)
🎨 AnimatedContainer Basic Concepts
AnimatedContainer is a special widget that can animate all properties of a Container. Its biggest advantage is that it automatically smoothly transitions when you just change property values.
Basic Structure
AnimatedContainer({
duration: 300, // Animation duration (milliseconds)
width: 100, // Properties to animate
height: 100,
color: '#4ECDC4',
curve: "easeInOut", // Animation curve (optional)
child: Text("Content")
})
Key Properties of AnimatedContainer
Required properties:
duration
(number): Animation duration (milliseconds)
Animatable properties:
width
,height
(number): Container sizecolor
(string): Background color (cannot be used with decoration)decoration
(BoxDecoration): Borders, shadows, gradients, etc.margin
,padding
(EdgeInsets): External/internal spacingalignment
(Alignment): Child widget alignmentconstraints
(Constraints): Constraint conditionstransform
(Matrix4): Transform matrix (rotation, scale, translation)
Other properties:
curve
(string): Animation curvechild
(Widget): Non-animated child widgetclipped
(boolean): Whether to clip boundaries
📋 Step-by-Step Practice
Step 1: Box that Changes Size on Click
Let’s first create a simple box that changes size when clicked:
import { AnimatedContainer, Text, GestureDetector } from "@meursyphus/flitter";
import { StatefulWidget, State } from "@meursyphus/flitter";
class ExpandingBox extends StatefulWidget {
createState() {
return new ExpandingBoxState();
}
}
class ExpandingBoxState extends State<ExpandingBox> {
isLarge = false;
build(context) {
return GestureDetector({
onClick: () => {
this.setState(() => {
this.isLarge = !this.isLarge;
});
},
child: AnimatedContainer({
duration: 500,
width: this.isLarge ? 200 : 100,
height: this.isLarge ? 200 : 100,
color: '#4ECDC4',
child: Text(this.isLarge ? "Big box!" : "Small box")
})
});
}
}
// Export as factory function
export default function ExpandingBox() {
return new _ExpandingBox();
}
Step 2: Change Color Along with Size
Let’s make the color change simultaneously with the size:
class ColorfulBox extends StatefulWidget {
createState() {
return new ColorfulBoxState();
}
}
class ColorfulBoxState extends State<ColorfulBox> {
isExpanded = false;
build(context) {
return GestureDetector({
onClick: () => {
this.setState(() => {
this.isExpanded = !this.isExpanded;
});
},
child: AnimatedContainer({
duration: 600,
width: this.isExpanded ? 180 : 120,
height: this.isExpanded ? 180 : 120,
color: this.isExpanded ? '#FF6B6B' : '#4ECDC4',
curve: "bounceOut", // Bounce effect
child: Text(
this.isExpanded ? "🎉 Expanded!" : "👆 Click me"
)
})
});
}
}
Step 3: Border and Rounded Corner Animations
Let’s use BoxDecoration for more sophisticated animations:
import { AnimatedContainer, Text, GestureDetector, BoxDecoration, Border, BorderSide } from "@meursyphus/flitter";
class FancyBox extends StatefulWidget {
createState() {
return new FancyBoxState();
}
}
class FancyBoxState extends State<FancyBox> {
isActive = false;
build(context) {
return GestureDetector({
onClick: () => {
this.setState(() => {
this.isActive = !this.isActive;
});
},
child: AnimatedContainer({
duration: 400,
width: this.isActive ? 200 : 150,
height: this.isActive ? 120 : 80,
decoration: BoxDecoration({
color: this.isActive ? '#FFE66D' : '#A8E6CF',
borderRadius: this.isActive ? 25 : 10,
border: Border.all({
color: this.isActive ? '#FF6B6B' : '#4ECDC4',
width: this.isActive ? 3 : 1
})
}),
child: Text(
this.isActive ? "✨ Activated!" : "💤 Inactive"
)
})
});
}
}
Step 4: Padding and Alignment Animations
Internal padding and alignment can also be animated:
import { EdgeInsets, Alignment } from "@meursyphus/flitter";
class PaddingBox extends StatefulWidget {
createState() {
return new PaddingBoxState();
}
}
class PaddingBoxState extends State<PaddingBox> {
isPadded = false;
build(context) {
return GestureDetector({
onClick: () => {
this.setState(() => {
this.isPadded = !this.isPadded;
});
},
child: AnimatedContainer({
duration: 300,
width: 200,
height: 200,
color: '#E8F4FD',
padding: this.isPadded
? EdgeInsets.all(40)
: EdgeInsets.all(10),
alignment: this.isPadded
? Alignment.center
: Alignment.topLeft,
child: Container({
color: '#2196F3',
child: Text("Padding change!")
})
})
});
}
}
⚙️ Complete Understanding of duration and curve
duration (Duration)
Specifies the time it takes to complete the animation in milliseconds (ms).
duration: 200 // Fast animation (0.2 seconds)
duration: 500 // Normal speed (0.5 seconds)
duration: 1000 // Slow animation (1 second)
Recommendations:
- Too short (< 150ms): Hard to perceive the animation
- Too long (> 1000ms): Users feel frustrated
- Generally recommended: 200-800ms
curve (Animation Curve)
Determines how the animation’s progress speed changes over time.
Basic curves:
curve: "linear" // Constant speed
curve: "easeIn" // Smooth start, fast end
curve: "easeOut" // Fast start, smooth end
curve: "easeInOut" // Smooth start and end
Special effect curves:
curve: "bounceOut" // Bounce effect
curve: "bounceIn" // Reverse bounce
curve: "bounceInOut" // Bidirectional bounce
curve: "elasticOut" // Elastic effect
curve: "elasticIn" // Reverse elastic
curve: "elasticInOut" // Bidirectional elastic
curve: "backOut" // Overshoot effect
curve: "backIn" // Reverse overshoot
curve: "backInOut" // Bidirectional overshoot
Actual curve Comparison Example
class CurveComparison extends StatefulWidget {
createState() {
return new CurveComparisonState();
}
}
class CurveComparisonState extends State<CurveComparison> {
isAnimated = false;
build(context) {
return Column({
children: [
// Linear
GestureDetector({
onClick: () => this.toggleAnimation(),
child: AnimatedContainer({
duration: 1000,
curve: "linear",
width: this.isAnimated ? 300 : 100,
height: 50,
color: '#FF5722',
child: Text("Linear")
})
}),
// EaseInOut
GestureDetector({
onClick: () => this.toggleAnimation(),
child: AnimatedContainer({
duration: 1000,
curve: "easeInOut",
width: this.isAnimated ? 300 : 100,
height: 50,
color: '#2196F3',
child: Text("EaseInOut")
})
}),
// BounceOut
GestureDetector({
onClick: () => this.toggleAnimation(),
child: AnimatedContainer({
duration: 1000,
curve: "bounceOut",
width: this.isAnimated ? 300 : 100,
height: 50,
color: '#4CAF50',
child: Text("BounceOut")
})
})
]
});
}
toggleAnimation() {
this.setState(() => {
this.isAnimated = !this.isAnimated;
});
}
}
🎯 Practice Challenges
TODO 1: Create a Three-State Change Button
Create a button that cycles through 3 states when clicked:
class TripleStateButton extends StatefulWidget {
createState() {
return new TripleStateButtonState();
}
}
class TripleStateButtonState extends State<TripleStateButton> {
currentState = 0; // 0, 1, 2
getStateConfig() {
const states = [
{ width: 120, height: 60, color: '#3498db', text: "Start" },
{ width: 160, height: 80, color: '#f39c12', text: "In Progress" },
{ width: 200, height: 100, color: '#27ae60', text: "Complete" }
];
return states[this.currentState];
}
build(context) {
const config = this.getStateConfig();
return GestureDetector({
onClick: () => {
this.setState(() => {
this.currentState = (this.currentState + 1) % 3;
});
},
child: AnimatedContainer({
duration: 400,
curve: "easeInOut",
width: config.width,
height: config.height,
color: config.color,
child: Text(config.text)
})
});
}
}
TODO 2: Card with Hover Effect
Create a card that slightly enlarges when hovered:
class HoverCard extends StatefulWidget {
createState() {
return new HoverCardState();
}
}
class HoverCardState extends State<HoverCard> {
isHovered = false;
build(context) {
return GestureDetector({
onMouseEnter: () => {
this.setState(() => {
this.isHovered = true;
});
},
onMouseLeave: () => {
this.setState(() => {
this.isHovered = false;
});
},
child: AnimatedContainer({
duration: 200,
curve: "easeOut",
width: this.isHovered ? 270 : 250,
height: this.isHovered ? 170 : 150,
decoration: BoxDecoration({
color: '#ffffff',
borderRadius: 10,
border: Border.all({
color: this.isHovered ? '#2196F3' : '#e0e0e0',
width: this.isHovered ? 2 : 1
})
}),
padding: EdgeInsets.all(20),
child: Text("Hover over me!")
})
});
}
}
TODO 3: Progress Bar
Create a progress bar that increases with each button click:
class ProgressBar extends StatefulWidget {
createState() {
return new ProgressBarState();
}
}
class ProgressBarState extends State<ProgressBar> {
progress = 0; // 0 ~ 100
build(context) {
return Column({
children: [
Container({
width: 300,
height: 20,
decoration: BoxDecoration({
color: '#f0f0f0',
borderRadius: 10
}),
child: Stack({
children: [
AnimatedContainer({
duration: 500,
curve: "easeOut",
width: (this.progress / 100) * 300,
height: 20,
decoration: BoxDecoration({
color: '#4CAF50',
borderRadius: 10
})
})
]
})
}),
GestureDetector({
onClick: () => {
this.setState(() => {
this.progress = Math.min(this.progress + 20, 100);
if (this.progress >= 100) {
// Reset after completion
setTimeout(() => {
this.setState(() => {
this.progress = 0;
});
}, 1000);
}
});
},
child: Container({
width: 100,
height: 40,
color: '#2196F3',
child: Text("Progress +20%")
})
}),
Text(`Progress: ${this.progress}%`)
]
});
}
}
🎨 Expected Results
When completed, the following features should work:
- Basic box: Size smoothly changes when clicked
- Colorful box: Size and color change simultaneously
- Advanced box: Borders and rounded corners animate
- Three-state button: Cycles through 3 states with changes
- Hover card: Smooth response on mouse hover
- Progress bar: Progress visually increases with clicks
💡 Additional Challenges
For more challenges:
- Chain animations: Make multiple boxes animate in sequence
- Complex animations: Animate padding, margin, shadows all together
- Conditional animations: Execute animations only under specific conditions
- Infinite animations: Create automatically repeating animations
⚠️ Common Mistakes and Solutions
1. Animation Too Fast
// ❌ Too fast to see
duration: 50
// ✅ Appropriate speed
duration: 300
2. setState() Usage Mistake
// ❌ Direct change without setState()
this.isExpanded = !this.isExpanded;
// ✅ Using setState()
this.setState(() => {
this.isExpanded = !this.isExpanded;
});
3. curve String Typo
// ❌ Typo
curve: "easeInout" // 'O' should be uppercase
// ✅ Correct string
curve: "easeInOut"
4. Using color and decoration Together
// ❌ Cannot use both
AnimatedContainer({
color: '#FF0000',
decoration: BoxDecoration({
color: '#0000FF' // Conflict!
})
})
// ✅ Use only decoration
AnimatedContainer({
decoration: BoxDecoration({
color: '#FF0000'
})
})
5. Using new Keyword with Widgets
// ❌ Don't use new with widgets
new AnimatedContainer({ ... })
// ✅ Use factory function
AnimatedContainer({ ... })
🎓 Key Summary
- AnimatedContainer: Widget that can animate all Container properties
- duration: Animation duration (milliseconds), 200-800ms recommended
- curve: Animation progression curve, creates natural movement
- StatefulWidget: Triggers animations through state changes
- GestureDetector: Handles user interactions
AnimatedContainer is the easiest animation widget to use in Flitter. Without complex animation controllers, you can create smooth and natural animations that greatly improve UI/UX.
🚀 Next Steps
In the next tutorial, let’s learn about various Animated widgets:
- Transparency animations with AnimatedOpacity
- Spacing animations with AnimatedPadding
- Position animations with AnimatedAlign
- Running multiple animations simultaneously