Back to Posts

Building Custom Animations in Flutter

11 min read

Flutter provides powerful tools for creating custom animations. This article will guide you through building custom animations using AnimationController, Tween, and custom painters.

Understanding Animation Basics

1. AnimationController

class CustomAnimation extends StatefulWidget {
  @override
  _CustomAnimationState createState() => _CustomAnimationState();
}

class _CustomAnimationState extends State<CustomAnimation> 
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

2. Tween and CurvedAnimation

class _CustomAnimationState extends State<CustomAnimation> 
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _animation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));

    _controller.repeat();
  }
}

Creating Custom Animations

1. Custom Painter Animation

class CustomPainterAnimation extends CustomPainter {
  final Animation<double> animation;

  CustomPainterAnimation(this.animation) : super(repaint: animation);

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 4 * animation.value;

    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(CustomPainterAnimation oldDelegate) {
    return oldDelegate.animation != animation;
  }
}

2. Using Custom Painter

class AnimatedCircle extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: CustomPainterAnimation(
        Tween<double>(begin: 0, end: 1).animate(
          AnimationController(
            duration: Duration(seconds: 2),
            vsync: this,
          )..repeat(),
        ),
      ),
      child: Container(),
    );
  }
}

Advanced Animation Techniques

1. Staggered Animations

class StaggeredAnimation extends StatefulWidget {
  @override
  _StaggeredAnimationState createState() => _StaggeredAnimationState();
}

class _StaggeredAnimationState extends State<StaggeredAnimation> 
    with TickerProviderStateMixin {
  late AnimationController _controller;
  late List<Animation<double>> _animations;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _animations = List.generate(3, (index) {
      return Tween<double>(
        begin: 0.0,
        end: 1.0,
      ).animate(CurvedAnimation(
        parent: _controller,
        curve: Interval(
          index * 0.3,
          (index + 1) * 0.3,
          curve: Curves.easeInOut,
        ),
      ));
    });

    _controller.forward();
  }
}

2. Physics-Based Animations

class PhysicsAnimation extends StatefulWidget {
  @override
  _PhysicsAnimationState createState() => _PhysicsAnimationState();
}

class _PhysicsAnimationState extends State<PhysicsAnimation> 
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _animation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.bounceOut,
    ));

    _controller.forward();
  }
}

Performance Optimization

1. Using RepaintBoundary

RepaintBoundary(
  child: CustomPaint(
    painter: CustomPainterAnimation(_animation),
    child: Container(),
  ),
)

2. Optimizing Animation Updates

class OptimizedAnimation extends StatefulWidget {
  @override
  _OptimizedAnimationState createState() => _OptimizedAnimationState();
}

class _OptimizedAnimationState extends State<OptimizedAnimation> 
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _animation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(_controller);

    _controller.addListener(() {
      if (_controller.isCompleted) {
        _controller.reverse();
      } else if (_controller.isDismissed) {
        _controller.forward();
      }
    });

    _controller.forward();
  }
}

Best Practices

  1. Use Appropriate Curves: Choose curves that match the animation's purpose
  2. Optimize Performance: Use RepaintBoundary for complex animations
  3. Handle Disposal: Always dispose of AnimationControllers
  4. Test on Multiple Devices: Ensure smooth performance across devices
  5. Consider Accessibility: Provide alternative content for users who prefer reduced motion

Example: Custom Loading Animation

class CustomLoadingAnimation extends StatefulWidget {
  @override
  _CustomLoadingAnimationState createState() => _CustomLoadingAnimationState();
}

class _CustomLoadingAnimationState extends State<CustomLoadingAnimation> 
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _rotationAnimation;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 2),
      vsync: this,
    );

    _rotationAnimation = Tween<double>(
      begin: 0.0,
      end: 2 * pi,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.linear,
    ));

    _scaleAnimation = Tween<double>(
      begin: 0.5,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));

    _controller.repeat();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.rotate(
          angle: _rotationAnimation.value,
          child: Transform.scale(
            scale: _scaleAnimation.value,
            child: Icon(Icons.refresh, size: 48),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

Conclusion

Building custom animations in Flutter involves:

  • Understanding AnimationController and Tween
  • Creating custom painters for complex animations
  • Implementing staggered and physics-based animations
  • Optimizing performance with RepaintBoundary
  • Following best practices for smooth animations
  1. Performance

    • Use const widgets where possible
    • Avoid unnecessary rebuilds
    • Use RepaintBoundary for complex animations
    • Optimize animation curves
  2. User Experience

    • Keep animations short and purposeful
    • Provide clear feedback
    • Consider motion sickness
    • Respect user preferences
  3. Code Organization

    • Separate animation logic
    • Use mixins for common behaviors
    • Implement proper cleanup
    • Document animation parameters

Common Animation Patterns

1. Page Transitions

class CustomPageRoute extends PageRouteBuilder {
  final Widget page;

  CustomPageRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return FadeTransition(
              opacity: animation,
              child: child,
            );
          },
        );
}

// Usage
Navigator.push(
  context,
  CustomPageRoute(page: const NextPage()),
);

2. Loading Animations

class LoadingAnimation extends StatefulWidget {
  const LoadingAnimation({super.key});

  @override
  State<LoadingAnimation> createState() => _LoadingAnimationState();
}

class _LoadingAnimationState extends State<LoadingAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _rotationAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );

    _rotationAnimation = Tween<double>(
      begin: 0,
      end: 2 * math.pi,
    ).animate(_controller);

    _controller.repeat();
  }

  @override
  Widget build(BuildContext context) {
    return RotationTransition(
      turns: _rotationAnimation,
      child: const Icon(Icons.refresh, size: 50),
    );
  }
}

3. Interactive Animations

class InteractiveAnimation extends StatefulWidget {
  const InteractiveAnimation({super.key});

  @override
  State<InteractiveAnimation> createState() => _InteractiveAnimationState();
}

class _InteractiveAnimationState extends State<InteractiveAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  double _dragPosition = 0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );

    _animation = Tween<double>(begin: 0, end: 1).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (details) {
        setState(() {
          _dragPosition += details.delta.dx;
        });
      },
      onPanEnd: (_) {
        if (_dragPosition > 100) {
          _controller.forward();
        } else {
          _controller.reverse();
        }
      },
      child: Transform.translate(
        offset: Offset(_dragPosition, 0),
        child: const FlutterLogo(size: 100),
      ),
    );
  }
}

Testing Animations

void main() {
  group('Animation Tests', () {
    testWidgets('animates correctly', (tester) async {
      await tester.pumpWidget(const MaterialApp(
        home: BasicAnimation(),
      ));

      expect(find.byType(FlutterLogo), findsOneWidget);

      await tester.pump(const Duration(seconds: 1));
      // Verify animation state
    });

    testWidgets('handles user interaction', (tester) async {
      await tester.pumpWidget(const MaterialApp(
        home: InteractiveAnimation(),
      ));

      await tester.drag(find.byType(FlutterLogo), const Offset(200, 0));
      await tester.pumpAndSettle();

      // Verify final position
    });
  });
}

Performance Optimization

  1. Use RepaintBoundary

    RepaintBoundary(
      child: AnimatedContainer(
        duration: const Duration(seconds: 1),
        color: Colors.blue,
      ),
    )
  2. Optimize Animation Curves

    CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutCubic, // More performant than easeOut
    )
  3. Use Transform Instead of Layout Changes

    Transform.scale(
      scale: _animation.value,
      child: const FlutterLogo(),
    )

Conclusion

Creating custom animations in Flutter requires understanding of:

  1. Animation Fundamentals: Controllers, tweens, and curves
  2. Performance Considerations: Optimization and best practices
  3. User Experience: Purposeful and smooth animations
  4. Advanced Techniques: Physics, staggered, and custom animations
  5. Testing: Verifying animation behavior

Remember to:

  • Keep animations purposeful and smooth
  • Optimize for performance
  • Test thoroughly
  • Consider accessibility
  • Document animation parameters

By following these guidelines, you can create engaging and performant animations that enhance your Flutter applications.