How to Create a Circle Loading Animation in Flutter
Loading animations are essential UI elements that provide visual feedback to users while content is being loaded. In this guide, we'll explore different ways to create circle loading animations in Flutter.
1. Using CustomPainter (Custom Implementation)
The most flexible way to create a custom circle loading animation is using AnimationController
and CustomPainter
.
import 'package:flutter/material.dart'; class CircleLoadingAnimation extends StatefulWidget { final Color color; final double size; final double strokeWidth; const CircleLoadingAnimation({ Key? key, this.color = Colors.blue, this.size = 50.0, this.strokeWidth = 4.0, }) : super(key: key); @override _CircleLoadingAnimationState createState() => _CircleLoadingAnimationState(); } class _CircleLoadingAnimationState extends State<CircleLoadingAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: Duration(seconds: 2), )..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return CustomPaint( size: Size(widget.size, widget.size), painter: CircleLoadingPainter( _controller, color: widget.color, strokeWidth: widget.strokeWidth, ), ); } } class CircleLoadingPainter extends CustomPainter { final Animation<double> animation; final Color color; final double strokeWidth; CircleLoadingPainter( this.animation, { required this.color, required this.strokeWidth, }) : super(repaint: animation); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; final center = Offset(size.width / 2, size.height / 2); final radius = (size.width - strokeWidth) / 2; // Draw background circle paint.color = color.withOpacity(0.2); canvas.drawCircle(center, radius, paint); // Draw animated arc paint.color = color; final sweepAngle = 2 * 3.14159 * animation.value; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), -3.14159 / 2, // Start from top sweepAngle, false, paint, ); } @override bool shouldRepaint(covariant CircleLoadingPainter oldDelegate) => animation != oldDelegate.animation || color != oldDelegate.color || strokeWidth != oldDelegate.strokeWidth; }
Usage Example:
CircleLoadingAnimation( color: Colors.blue, size: 50.0, strokeWidth: 4.0, )
2. Using CircularProgressIndicator (Built-in Widget)
Flutter provides a built-in CircularProgressIndicator
widget for simple loading animations:
// Determinate progress indicator CircularProgressIndicator( value: 0.7, // Progress from 0.0 to 1.0 backgroundColor: Colors.grey[200], color: Colors.blue, strokeWidth: 4.0, ) // Indeterminate progress indicator CircularProgressIndicator( backgroundColor: Colors.grey[200], color: Colors.blue, strokeWidth: 4.0, )
3. Advanced Circle Loading Animation
Here's an example of a more advanced circle loading animation with multiple circles:
class MultiCircleLoadingAnimation extends StatefulWidget { @override _MultiCircleLoadingAnimationState createState() => _MultiCircleLoadingAnimationState(); } class _MultiCircleLoadingAnimationState extends State<MultiCircleLoadingAnimation> with TickerProviderStateMixin { late List<AnimationController> controllers; @override void initState() { super.initState(); controllers = List.generate( 3, (index) => AnimationController( vsync: this, duration: Duration(seconds: 1), )..repeat(), ); // Add delays for each controller for (int i = 0; i < controllers.length; i++) { Future.delayed(Duration(milliseconds: 300 * i), () { if (mounted) controllers[i].repeat(); }); } } @override void dispose() { for (var controller in controllers) { controller.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return Stack( children: List.generate( 3, (index) => ScaleTransition( scale: Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: controllers[index], curve: Curves.easeInOut, ), ), child: Opacity( opacity: 1.0 - (index * 0.3), child: Container( width: 50.0 + (index * 10.0), height: 50.0 + (index * 10.0), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.blue, width: 2.0, ), ), ), ), ), ), ); } }
Best Practices
-
Performance
- Use
RepaintBoundary
when necessary to optimize rendering - Dispose of animation controllers properly
- Use appropriate animation durations (typically 1-2 seconds)
- Use
-
Customization
- Make animations configurable (color, size, speed)
- Consider different states (loading, error, success)
- Support both light and dark themes
-
Accessibility
- Add semantic labels for screen readers
- Consider reducing animation if user prefers reduced motion
- Provide alternative feedback methods
Example Implementation with All Features
class AdvancedCircleLoading extends StatefulWidget { final Color color; final double size; final Duration duration; final String? semanticsLabel; final bool reduceAnimation; const AdvancedCircleLoading({ Key? key, this.color = Colors.blue, this.size = 50.0, this.duration = const Duration(seconds: 1), this.semanticsLabel, this.reduceAnimation = false, }) : super(key: key); @override _AdvancedCircleLoadingState createState() => _AdvancedCircleLoadingState(); } class _AdvancedCircleLoadingState extends State<AdvancedCircleLoading> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: widget.duration, ); if (!widget.reduceAnimation) { _controller.repeat(); } else { _controller.value = 0.5; } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RepaintBoundary( child: Semantics( label: widget.semanticsLabel ?? 'Loading indicator', child: AnimatedBuilder( animation: _controller, builder: (context, child) { return CustomPaint( size: Size(widget.size, widget.size), painter: AdvancedCirclePainter( animation: _controller, color: widget.color, reduceAnimation: widget.reduceAnimation, ), ); }, ), ), ); } }
Conclusion
Circle loading animations are versatile UI elements that can enhance user experience. Whether using the built-in CircularProgressIndicator
or creating custom animations with CustomPainter
, Flutter provides powerful tools for implementing beautiful loading indicators.
Remember to:
- Choose the appropriate implementation based on your needs
- Follow performance best practices
- Consider accessibility
- Maintain consistency with your app's design language
With these techniques, you can create engaging and efficient loading animations that enhance your Flutter application's user experience.