chatapp3-flutter/lib/ui_kit/components/sc_rotating_dots_loading.dart

129 lines
3.8 KiB
Dart

import 'dart:math' as math;
import 'dart:ui' show lerpDouble;
import 'package:flutter/material.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
class SCRotatingDotsLoading extends StatefulWidget {
const SCRotatingDotsLoading({
super.key,
this.size,
this.color = SocialChatTheme.primaryLight,
this.dotCount = 8,
this.duration = const Duration(milliseconds: 900),
this.minAdaptiveSize = 14,
this.maxAdaptiveSize = 40,
});
final double? size;
final Color color;
final int dotCount;
final Duration duration;
final double minAdaptiveSize;
final double maxAdaptiveSize;
@override
State<SCRotatingDotsLoading> createState() => _SCRotatingDotsLoadingState();
}
class _SCRotatingDotsLoadingState extends State<SCRotatingDotsLoading>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.duration)
..repeat();
}
@override
void didUpdateWidget(covariant SCRotatingDotsLoading oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.duration != widget.duration) {
_controller
..duration = widget.duration
..repeat();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final configuredSize = widget.size;
if (configuredSize != null) {
return _buildIndicator(configuredSize);
}
return LayoutBuilder(
builder: (context, constraints) {
final adaptiveSize = _resolveAdaptiveSize(constraints);
return Center(child: _buildIndicator(adaptiveSize));
},
);
}
Widget _buildIndicator(double size) {
final normalizedSize = size.clamp(8.0, 200.0);
final dotCount = math.max(1, widget.dotCount);
final maxDotSize = normalizedSize * 0.26;
final radius = normalizedSize / 2 - maxDotSize / 2;
return RepaintBoundary(
child: SizedBox.square(
dimension: normalizedSize,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * math.pi * 2,
child: Stack(
children: List.generate(dotCount, (index) {
final progress = 1 - (index / dotCount);
final dotScale = lerpDouble(0.42, 1, progress)!;
final opacity = lerpDouble(0.18, 1, progress)!;
final dotSize = maxDotSize * dotScale;
final angle = (math.pi * 2 * index / dotCount) - math.pi / 2;
final dx = normalizedSize / 2 + math.cos(angle) * radius;
final dy = normalizedSize / 2 + math.sin(angle) * radius;
return Positioned(
left: dx - dotSize / 2,
top: dy - dotSize / 2,
child: Container(
width: dotSize,
height: dotSize,
decoration: BoxDecoration(
color: widget.color.withValues(alpha: opacity),
shape: BoxShape.circle,
),
),
);
}),
),
);
},
),
),
);
}
double _resolveAdaptiveSize(BoxConstraints constraints) {
final values =
<double>[
constraints.maxWidth,
constraints.maxHeight,
].where((value) => value.isFinite && value > 0).toList();
final shortest = values.isEmpty ? 28.0 : values.reduce(math.min);
final scaled = shortest * 0.52;
return math.max(
widget.minAdaptiveSize,
math.min(widget.maxAdaptiveSize, scaled),
);
}
}