Free copy-paste Loader 003 for Flutter: a ready-to-use loader widget with full Dart source code and a live preview. Works on mobile & desktop. No external packages.
Copy the Dart code below into a new file in your Flutter project (for example lib/widgets/loader_003.dart) and use the widget anywhere in your widget tree.
import 'package:flutter/material.dart';
class Loader003 extends StatefulWidget {
const Loader003({
super.key,
this.width = 64.0,
this.height = 48.0,
this.color = const Color(0xFFFF4D4F),
this.trackColor = const Color(0x33FF4D50),
this.strokeWidth = 3.0,
this.duration = const Duration(milliseconds: 1000),
});
final double width;
final double height;
final Color color;
final Color trackColor;
final double strokeWidth;
final Duration duration;
@override
State<Loader003> createState() => _Loader003State();
}
class _Loader003State extends State<Loader003>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration)
..repeat();
}
@override
void didUpdateWidget(covariant Loader003 oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.duration != widget.duration) {
_controller.duration = widget.duration;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.width,
height: widget.height,
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return CustomPaint(
painter: _PulsePainter(
progress: _controller.value,
color: widget.color,
trackColor: widget.trackColor,
strokeWidth: widget.strokeWidth,
),
);
},
),
);
}
}
class _PulsePainter extends CustomPainter {
const _PulsePainter({
this.progress = 0.0,
this.color = const Color(0xFFFF4D4F),
this.trackColor = const Color(0x33FF4D50),
this.strokeWidth = 3.0,
});
final double progress;
final Color color;
final Color trackColor;
final double strokeWidth;
@override
void paint(Canvas canvas, Size size) {
final sx = size.width / 64.0;
final sy = size.height / 48.0;
final path = Path()
..moveTo(0.157 * sx, 23.954 * sy)
..lineTo(14.0 * sx, 23.954 * sy)
..lineTo(21.843 * sx, 48.0 * sy)
..lineTo(43.0 * sx, 0.0 * sy)
..lineTo(50.0 * sx, 24.0 * sy)
..lineTo(64.0 * sx, 24.0 * sy);
final trackPaint = Paint()
..color = trackColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
canvas.drawPath(path, trackPaint);
final metrics = path.computeMetrics().toList();
if (metrics.isEmpty) return;
final metric = metrics.first;
final totalLen = metric.length;
final beamLen = totalLen * 0.35;
final cycleLen = totalLen + beamLen;
final head = progress * cycleLen;
final start = (head - beamLen).clamp(0.0, totalLen);
final end = head.clamp(0.0, totalLen);
if (end > start) {
final pulsePath = metric.extractPath(start, end);
final alpha = progress > 0.82 ? ((1.0 - progress) / 0.18) : 1.0;
final pulsePaint = Paint()
..color = color.withValues(alpha: alpha.clamp(0.0, 1.0))
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.5);
canvas.drawPath(pulsePath, pulsePaint);
}
}
@override
bool shouldRepaint(_PulsePainter oldDelegate) =>
oldDelegate.progress != progress;
}