Files
Eventify-frontend/lib/widgets/bouncing_loader.dart

100 lines
2.7 KiB
Dart
Raw Permalink Normal View History

// lib/widgets/bouncing_loader.dart
import 'package:flutter/material.dart';
/// Three-dot bouncing loader using Curves.bounceOut.
/// Drop-in replacement for CircularProgressIndicator on full-screen loads.
class BouncingLoader extends StatefulWidget {
final Color? color;
final double dotSize;
final double spacing;
const BouncingLoader({
Key? key,
this.color,
this.dotSize = 8.0,
this.spacing = 6.0,
}) : super(key: key);
@override
State<BouncingLoader> createState() => _BouncingLoaderState();
}
class _BouncingLoaderState extends State<BouncingLoader> with TickerProviderStateMixin {
late final List<AnimationController> _controllers;
late final List<Animation<double>> _animations;
static const _duration = Duration(milliseconds: 600);
static const _staggerDelay = Duration(milliseconds: 200);
@override
void initState() {
super.initState();
_controllers = List.generate(
3,
(i) => AnimationController(vsync: this, duration: _duration),
);
_animations = _controllers.map((c) {
return Tween<double>(begin: 0.0, end: -12.0).animate(
CurvedAnimation(parent: c, curve: Curves.bounceOut),
);
}).toList();
_startWithStagger();
}
void _startWithStagger() async {
for (int i = 0; i < _controllers.length; i++) {
await Future.delayed(i == 0 ? Duration.zero : _staggerDelay);
if (!mounted) return;
_startLoop(i);
}
}
void _startLoop(int index) {
if (!mounted) return;
_controllers[index].forward(from: 0).whenComplete(() {
if (mounted) {
Future.delayed(
Duration(milliseconds: _staggerDelay.inMilliseconds * (_controllers.length - 1)),
() { if (mounted) _startLoop(index); },
);
}
});
}
@override
void dispose() {
for (final c in _controllers) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final dotColor = widget.color ?? Theme.of(context).colorScheme.primary;
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (i) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: widget.spacing / 2),
child: AnimatedBuilder(
animation: _animations[i],
builder: (_, __) => Transform.translate(
offset: Offset(0, _animations[i].value),
child: Container(
width: widget.dotSize,
height: widget.dotSize,
decoration: BoxDecoration(
color: dotColor,
shape: BoxShape.circle,
),
),
),
),
);
}),
);
}
}