Creating Special Effects with Various Animated Widgets
In this tutorial, weโll learn about the various Animated widgets that Flitter provides beyond AnimatedContainer to create more sophisticated and professional animation effects.
๐ฏ Learning Goals
After completing this tutorial, youโll be able to:
- Implement fade in/out effects with AnimatedOpacity
- Create spacing change animations with AnimatedPadding
- Implement position movement animations with AnimatedAlign
- Create size scaling effects with AnimatedScale
- Implement rotation animations with AnimatedRotation
- Combine multiple Animated widgets for complex effects
๐จ Characteristics of Animated Widgets
Each Animated widget is specialized for specific properties, allowing very efficient and smooth implementation of those effects:
- AnimatedOpacity: Transparency changes (fade effects)
- AnimatedPadding: Internal spacing changes
- AnimatedAlign: Alignment position changes (position movement)
- AnimatedScale: Size ratio changes
- AnimatedRotation: Rotation changes
- AnimatedSlide: Relative position movement
- AnimatedPositioned: Absolute position changes within Stack
๐ Step-by-Step Practice
Step 1: AnimatedOpacity - Fade Effects
Letโs create effects where elements appear or disappear by smoothly changing transparency:
import { AnimatedOpacity, Container, Text, GestureDetector } from "@meursyphus/flitter";
class FadeEffect extends StatefulWidget {
createState() {
return new FadeEffectState();
}
}
class FadeEffectState extends State<FadeEffect> {
isVisible = true;
build(context) {
return Column({
children: [
GestureDetector({
onClick: () => {
this.setState(() => {
this.isVisible = !this.isVisible;
});
},
child: AnimatedOpacity({
duration: 500, // Animation duration
opacity: this.isVisible ? 1.0 : 0.0, // Opacity (0.0~1.0)
curve: "easeInOut", // Animation curve
child: Container({
width: 200,
height: 100,
color: '#FF6B6B',
child: Text(this.isVisible ? "Visible!" : "Hidden!")
})
})
}),
Text("Click to see fade effect")
]
});
}
}
Key properties of AnimatedOpacity:
opacity
(number): Transparency (0.0 = completely transparent, 1.0 = completely opaque)duration
(number): Animation duration (milliseconds)curve
(string): Animation curvechild
(Widget): Child widget to be animated
Step 2: AnimatedPadding - Spacing Animation
Letโs smoothly change internal spacing to adjust content layout:
import { AnimatedPadding, EdgeInsets } from "@meursyphus/flitter";
class PaddingAnimation extends StatefulWidget {
createState() {
return new PaddingAnimationState();
}
}
class PaddingAnimationState extends State<PaddingAnimation> {
isExpanded = false;
build(context) {
return GestureDetector({
onClick: () => {
this.setState(() => {
this.isExpanded = !this.isExpanded;
});
},
child: Container({
width: 300,
height: 200,
color: '#E8F4FD',
child: AnimatedPadding({
duration: 400,
curve: "easeOut",
padding: this.isExpanded
? EdgeInsets.all(50) // Large spacing when expanded
: EdgeInsets.all(10), // Small spacing when collapsed
child: Container({
color: '#2196F3',
child: Text(
this.isExpanded ? "Expanded padding" : "Default padding"
)
})
})
})
});
}
}
EdgeInsets usage:
// Same in all directions
EdgeInsets.all(20)
// Symmetric spacing
EdgeInsets.symmetric({
horizontal: 30, // Left and right
vertical: 15 // Top and bottom
})
// Individual specification
EdgeInsets.only({
top: 20,
left: 15,
right: 10,
bottom: 25
})
// Direct specification
EdgeInsets.fromLTRB(10, 20, 15, 25) // Left, top, right, bottom
Step 3: AnimatedAlign - Position Movement Animation
Letโs smoothly change the alignment position of child widgets to create movement effects:
import { AnimatedAlign, Alignment } from "@meursyphus/flitter";
class AlignAnimation extends StatefulWidget {
createState() {
return new AlignAnimationState();
}
}
class AlignAnimationState extends State<AlignAnimation> {
alignmentIndex = 0;
// 9 basic alignment positions
alignments = [
Alignment.topLeft,
Alignment.topCenter,
Alignment.topRight,
Alignment.centerLeft,
Alignment.center,
Alignment.centerRight,
Alignment.bottomLeft,
Alignment.bottomCenter,
Alignment.bottomRight
];
build(context) {
return GestureDetector({
onClick: () => {
this.setState(() => {
this.alignmentIndex = (this.alignmentIndex + 1) % this.alignments.length;
});
},
child: Container({
width: 300,
height: 200,
color: '#F0F0F0',
child: AnimatedAlign({
duration: 600,
curve: "easeInOut",
alignment: this.alignments[this.alignmentIndex],
child: Container({
width: 80,
height: 50,
color: '#9B59B6',
child: Text(`Position ${this.alignmentIndex + 1}`)
})
})
})
});
}
}
Alignment constants:
// 9 basic positions
Alignment.topLeft // Top-left
Alignment.topCenter // Top-center
Alignment.topRight // Top-right
Alignment.centerLeft // Center-left
Alignment.center // Center
Alignment.centerRight // Center-right
Alignment.bottomLeft // Bottom-left
Alignment.bottomCenter // Bottom-center
Alignment.bottomRight // Bottom-right
// Custom alignment (x, y: -1.0 ~ 1.0)
Alignment(0.5, -0.5) // Towards top-right
Step 4: AnimatedScale - Size Ratio Animation
Letโs create animations that scale widgets by ratio:
import { AnimatedScale } from "@meursyphus/flitter";
class ScaleAnimation extends StatefulWidget {
createState() {
return new ScaleAnimationState();
}
}
class ScaleAnimationState extends State<ScaleAnimation> {
isScaled = false;
build(context) {
return GestureDetector({
onClick: () => {
this.setState(() => {
this.isScaled = !this.isScaled;
});
},
child: AnimatedScale({
duration: 400,
curve: "bounceOut",
scale: this.isScaled ? 1.5 : 1.0, // Scale ratio
alignment: Alignment.center, // Scale origin point
child: Container({
width: 100,
height: 100,
color: '#E74C3C',
child: Text(
this.isScaled ? "1.5x!" : "Original"
)
})
})
});
}
}
Step 5: AnimatedRotation - Rotation Animation
Letโs implement animations that smoothly rotate widgets:
import { AnimatedRotation } from "@meursyphus/flitter";
class RotationAnimation extends StatefulWidget {
createState() {
return new RotationAnimationState();
}
}
class RotationAnimationState extends State<RotationAnimation> {
rotationCount = 0;
build(context) {
return GestureDetector({
onClick: () => {
this.setState(() => {
this.rotationCount += 1; // Add one rotation each time
});
},
child: AnimatedRotation({
duration: 800,
curve: "easeInOut",
turns: this.rotationCount, // Number of rotations (1 = 360 degrees)
alignment: Alignment.center, // Rotation origin point
child: Container({
width: 80,
height: 80,
color: '#3498DB',
child: Text("๐")
})
})
});
}
}
Understanding rotation amounts:
turns: 0.25 // 90 degrees rotation
turns: 0.5 // 180 degrees rotation
turns: 1 // 360 degrees (1 rotation)
turns: 1.5 // 540 degrees (1.5 rotations)
turns: -0.5 // Counter-clockwise 180 degrees
Step 6: AnimatedSlide - Relative Position Movement
This animation moves widgets by distances relative to their size:
import { AnimatedSlide, Offset } from "@meursyphus/flitter";
class SlideAnimation extends StatefulWidget {
createState() {
return new SlideAnimationState();
}
}
class SlideAnimationState extends State<SlideAnimation> {
isSlid = false;
build(context) {
return Container({
width: 300,
height: 150,
color: '#F8F9FA',
child: GestureDetector({
onClick: () => {
this.setState(() => {
this.isSlid = !this.isSlid;
});
},
child: AnimatedSlide({
duration: 500,
curve: "easeInOut",
offset: this.isSlid
? Offset(1.0, 0) // Right by widget width
: Offset(0, 0), // Original position
child: Container({
width: 100,
height: 60,
color: '#FF9F43',
child: Text("Slide!")
})
})
})
});
}
}
Understanding Offset:
Offset(0, 0) // Original position
Offset(1, 0) // Right by widget width
Offset(-1, 0) // Left by widget width
Offset(0, 1) // Down by widget height
Offset(0, -1) // Up by widget height
Offset(1, 1) // Diagonal bottom-right
๐ฏ Practice Challenges
TODO 1: Multi-Effect Card
Create an interactive card that combines multiple animations:
class MultiEffectCard extends StatefulWidget {
createState() {
return new MultiEffectCardState();
}
}
class MultiEffectCardState extends State<MultiEffectCard> {
isHovered = false;
isClicked = false;
build(context) {
return GestureDetector({
onMouseEnter: () => {
this.setState(() => {
this.isHovered = true;
});
},
onMouseLeave: () => {
this.setState(() => {
this.isHovered = false;
});
},
onClick: () => {
this.setState(() => {
this.isClicked = !this.isClicked;
});
},
child: AnimatedScale({
duration: 200,
scale: this.isHovered ? 1.05 : 1.0,
child: AnimatedRotation({
duration: 300,
turns: this.isClicked ? 0.02 : 0, // Slight tilt
child: AnimatedOpacity({
duration: 250,
opacity: this.isHovered ? 0.9 : 1.0,
child: Container({
width: 200,
height: 120,
color: '#6C5CE7',
child: AnimatedPadding({
duration: 200,
padding: this.isHovered
? EdgeInsets.all(25)
: EdgeInsets.all(20),
child: Text("Multi-effect card")
})
})
})
})
})
});
}
}
TODO 2: Sequential Animation Menu
Create an animation where multiple menu items appear in sequence:
class SequentialMenu extends StatefulWidget {
createState() {
return new SequentialMenuState();
}
}
class SequentialMenuState extends State<SequentialMenu> {
isMenuOpen = false;
createMenuItem(text, delay) {
return AnimatedSlide({
duration: 400,
curve: "easeOut",
offset: this.isMenuOpen
? Offset(0, 0)
: Offset(-1, 0),
child: AnimatedOpacity({
duration: 300,
opacity: this.isMenuOpen ? 1.0 : 0.0,
child: Container({
width: 150,
height: 50,
color: '#1DD1A1',
margin: EdgeInsets.only({ bottom: 10 }),
child: Text(text)
})
})
});
}
build(context) {
// For delay animations, a more complex implementation is actually needed
// Here we just show the basic concept
return Column({
children: [
GestureDetector({
onClick: () => {
this.setState(() => {
this.isMenuOpen = !this.isMenuOpen;
});
},
child: Container({
width: 150,
height: 50,
color: '#FF6B6B',
child: Text(this.isMenuOpen ? "Close Menu" : "Open Menu")
})
}),
this.createMenuItem("Home", 0),
this.createMenuItem("Services", 100),
this.createMenuItem("About", 200),
this.createMenuItem("Contact", 300)
]
});
}
}
TODO 3: Loading Indicator
Create a loading animation combining multiple Animated widgets:
class LoadingIndicator extends StatefulWidget {
createState() {
return new LoadingIndicatorState();
}
}
class LoadingIndicatorState extends State<LoadingIndicator> {
isLoading = false;
pulseCount = 0;
startLoading() {
this.setState(() => {
this.isLoading = true;
});
// Timer for pulse animation
const interval = setInterval(() => {
if (this.isLoading) {
this.setState(() => {
this.pulseCount += 1;
});
} else {
clearInterval(interval);
}
}, 800);
}
build(context) {
return Column({
children: [
GestureDetector({
onClick: () => {
if (!this.isLoading) {
this.startLoading();
// Auto-complete after 3 seconds
setTimeout(() => {
this.setState(() => {
this.isLoading = false;
});
}, 3000);
}
},
child: Container({
width: 120,
height: 50,
color: '#3742FA',
child: Text(this.isLoading ? "Loading..." : "Start Loading")
})
}),
// Loading indicator
AnimatedOpacity({
duration: 300,
opacity: this.isLoading ? 1.0 : 0.0,
child: AnimatedRotation({
duration: 1000,
turns: this.isLoading ? this.pulseCount : 0,
child: AnimatedScale({
duration: 800,
curve: "easeInOut",
scale: (this.pulseCount % 2 === 0) ? 1.0 : 1.2,
child: Container({
width: 60,
height: 60,
color: '#FF3838',
child: Text("โญ")
})
})
})
})
]
});
}
}
๐จ Expected Results
When completed, the following effects should work:
- Fade effect: Smooth transparency changes for appearing/disappearing effects
- Padding animation: Content expansion/contraction through spacing changes
- Position movement: Smooth movement to 9 different positions
- Size changes: Scaling effects by ratio
- Rotation effect: Rotation with each click
- Slide: Movement by relative distances
๐ก Additional Challenges
For more challenges:
- Staggered animations: Make multiple elements animate sequentially
- Infinite loops: Implement automatically repeating animations
- Complex effects: Combine 3 or more Animated widgets
- Conditional animations: Execute specific effects only under certain conditions
โ ๏ธ Common Mistakes and Solutions
1. opacity Range Overflow
// โ Wrong range
opacity: 1.5 // Exceeds 1.0
opacity: -0.2 // Below 0.0
// โ
Correct range
opacity: 1.0 // Maximum value
opacity: 0.0 // Minimum value
2. Misunderstanding Alignment Coordinates
// โ Wrong understanding
Alignment(100, 200) // Misunderstood as absolute coordinates
// โ
Correct understanding
Alignment(1.0, 1.0) // Relative ratio (-1.0 ~ 1.0)
3. Confusing EdgeInsets Constructors
// โ Wrong usage
EdgeInsets(10, 20, 15, 25) // Direct constructor usage
// โ
Correct usage
EdgeInsets.fromLTRB(10, 20, 15, 25) // Factory method usage
4. Performance with Multiple Animation Nesting
// โ ๏ธ Caution: Too much nesting causes performance degradation
AnimatedScale({
child: AnimatedRotation({
child: AnimatedOpacity({
child: AnimatedSlide({
// Too much nesting
})
})
})
})
// โ
Use only what's needed
AnimatedScale({
child: AnimatedOpacity({
// Combine only necessary effects
})
})
๐ Key Summary
Specialized Areas of Each Widget
- AnimatedOpacity: Transparency โ Fade in/out
- AnimatedPadding: Spacing โ Content expansion/contraction
- AnimatedAlign: Alignment โ Position movement
- AnimatedScale: Ratio โ Size changes
- AnimatedRotation: Rotation โ Rotation effects
- AnimatedSlide: Relative movement โ Slide effects
Combination Strategy
- Single effect: Use one widget for a clear purpose
- Complex effect: Combine 2-3 widgets for rich effects
- Performance consideration: Avoid too much nesting
- User experience: Natural and meaningful animations
With the various Animated widgets learned in this tutorial, you can create professional and sophisticated user interfaces. Understanding the characteristics of each widget and combining them appropriately can provide users with a delightful experience.
๐ Next Steps
In the next tutorial, letโs learn AnimationController Mastery:
- Controlling explicit animations
- Using Tween and CurvedAnimation
- Implementing complex animation sequences
- Controlling animation playback, stop, and repeat