CircularProgressIndicator is a Material widget. If your design is a custome one, the spinner is one of the last places Material leaks in.
Meet Spinner, a neutral loading spinner ready for copy-paste:
import 'dart:math';
import 'package:flutter/widgets.dart';
const Color _defaultColor = Color(0xFF1A1A1A);
class Spinner extends StatefulWidget {
final double size;
final double strokeWidth;
final Color? color;
const Spinner({
super.key,
this.size = 20,
this.strokeWidth = 2,
this.color,
});
@override
State<Spinner> createState() => SpinnerState();
}
class SpinnerState extends State<Spinner>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 1000,
),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Semantics(
label: 'Loading',
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * pi,
child: CustomPaint(
painter: _SpinnerPainter(
color: widget.color ?? _defaultColor,
strokeWidth: widget.strokeWidth,
),
size: Size.square(
widget.size,
),
),
);
},
),
);
}
}
class _SpinnerPainter extends CustomPainter {
final Color color;
final double strokeWidth;
_SpinnerPainter({
required this.color,
required this.strokeWidth,
});
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final double inset = strokeWidth / 2;
canvas.drawArc(
Rect.fromLTWH(
inset,
inset,
size.width - strokeWidth,
size.height - strokeWidth,
),
0,
pi * 1.5,
false,
paint,
);
}
@override
bool shouldRepaint(_SpinnerPainter oldDelegate) =>
oldDelegate.color != color || oldDelegate.strokeWidth != strokeWidth;
}
Use it anywhere:
const Spinner()
const Spinner(
size: 24, strokeWidth: 3,
)
const Spinner(
color: Color(0xFFFFFFFF),
)
The arc insets by half the stroke width, so it stays inside size at any stroke. Color falls back to _defaultColor when you don't pass one.