Back to Posts

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

  1. Performance Optimization

    • Use AnimatedBuilder for complex animations
    • Dispose of controllers properly
    • Avoid unnecessary rebuilds
  2. Animation Duration

    • Keep animations short (usually under 300ms)
    • Use appropriate curves for natural movement
    • Consider user interaction timing
  3. State Management

    • Use SingleTickerProviderStateMixin for single animations
    • Use TickerProviderStateMixin for multiple animations
    • Handle animation state properly

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:

  1. Use appropriate animation curves for natural movement
  2. Combine multiple animations for complex effects
  3. Follow best practices for performance
  4. Handle disposal properly to prevent memory leaks
  5. 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.