129 lines
3.8 KiB
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),
|
|
);
|
|
}
|
|
}
|