How to Animate a Widget Using Tween Animation in Flutter
•14 min read
Animations can significantly enhance your app's user experience. In this guide, we'll explore how to use Tween animations in Flutter to create smooth, professional-looking animations for various widget properties.
Basic Tween Animation
Simple Size Animation
class SizeAnimation extends StatefulWidget { @override _SizeAnimationState createState() => _SizeAnimationState(); } class _SizeAnimationState extends State<SizeAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(milliseconds: 500), vsync: this, ); _animation = Tween<double>( begin: 100, end: 200, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); _controller.addListener(() { setState(() {}); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { if (_controller.status == AnimationStatus.completed) { _controller.reverse(); } else { _controller.forward(); } }, child: Container( width: _animation.value, height: _animation.value, color: Colors.blue, child: Center( child: Text( 'Tap to Animate', style: TextStyle(color: Colors.white), ), ), ), ); } }
Color Animation
Animated Button
class ColorChangeButton extends StatefulWidget { @override _ColorChangeButtonState createState() => _ColorChangeButtonState(); } class _ColorChangeButtonState extends State<ColorChangeButton> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<Color?> _colorAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(milliseconds: 300), vsync: this, ); _colorAnimation = ColorTween( begin: Colors.blue, end: Colors.red, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _colorAnimation, builder: (context, child) { return ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: _colorAnimation.value, padding: EdgeInsets.symmetric( horizontal: 32, vertical: 16, ), ), onPressed: () { if (_controller.status == AnimationStatus.completed) { _controller.reverse(); } else { _controller.forward(); } }, child: Text( 'Animate Color', style: TextStyle(fontSize: 18), ), ); }, ); } }
Position Animation
Sliding Card
class SlidingCard extends StatefulWidget { @override _SlidingCardState createState() => _SlidingCardState(); } class _SlidingCardState extends State<SlidingCard> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<Offset> _offsetAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(milliseconds: 800), vsync: this, ); _offsetAnimation = Tween<Offset>( begin: Offset(-1.0, 0.0), end: Offset.zero, ).animate(CurvedAnimation( parent: _controller, curve: Curves.elasticOut, )); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SlideTransition( position: _offsetAnimation, child: Card( elevation: 4, child: Container( width: 300, padding: EdgeInsets.all(16), child: Column( children: [ Text( 'Sliding Card', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), SizedBox(height: 16), Text( 'This card slides in from the left with an elastic effect.', ), ], ), ), ), ), SizedBox(height: 16), ElevatedButton( onPressed: () { if (_controller.status == AnimationStatus.completed) { _controller.reverse(); } else { _controller.forward(); } }, child: Text('Animate'), ), ], ); } }
Multiple Properties Animation
Animated Profile Card
class AnimatedProfileCard extends StatefulWidget { @override _AnimatedProfileCardState createState() => _AnimatedProfileCardState(); } class _AnimatedProfileCardState extends State<AnimatedProfileCard> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _scaleAnimation; late Animation<double> _opacityAnimation; late Animation<Color?> _colorAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(milliseconds: 1000), vsync: this, ); _scaleAnimation = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Curves.elasticOut, )); _opacityAnimation = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Interval(0.5, 1.0), )); _colorAnimation = ColorTween( begin: Colors.grey[300], end: Colors.blue[100], ).animate(CurvedAnimation( parent: _controller, curve: Interval(0.0, 0.5), )); _controller.forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Card( color: _colorAnimation.value, child: Opacity( opacity: _opacityAnimation.value, child: Container( width: 300, padding: EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( radius: 50, backgroundImage: NetworkImage( 'https://example.com/profile.jpg', ), ), SizedBox(height: 16), Text( 'John Doe', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), Text( 'Flutter Developer', style: TextStyle( fontSize: 16, color: Colors.grey[600], ), ), ], ), ), ), ), ); }, ); } }
Best Practices
-
Performance Optimization
- Use
AnimatedBuilder
for complex animations - Dispose of controllers properly
- Avoid unnecessary rebuilds
- Use
-
Animation Duration
- Keep animations short (usually under 300ms)
- Use appropriate curves for natural movement
- Consider user interaction timing
-
State Management
- Use
SingleTickerProviderStateMixin
for single animations - Use
TickerProviderStateMixin
for multiple animations - Handle animation state properly
- Use
Common Issues and Solutions
Issue 1: Animation Jank
// Bad Practice setState(() { // Updating UI on every animation frame }); // Good Practice AnimatedBuilder( animation: _animation, builder: (context, child) { return YourWidget(); }, );
Issue 2: Memory Leaks
// Always dispose controllers @override void dispose() { _controller.dispose(); super.dispose(); }
Conclusion
Tween animations in Flutter provide a powerful way to create engaging user experiences:
- Use appropriate animation curves for natural movement
- Combine multiple animations for complex effects
- Follow best practices for performance
- Handle disposal properly to prevent memory leaks
- Consider user experience when designing animations
Remember that animations should enhance the user experience, not hinder it. Keep them smooth, purposeful, and not too lengthy.