mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 14:32:12 +00:00
248 lines
6.4 KiB
Dart
248 lines
6.4 KiB
Dart
// Credits: https://github.com/watery-desert/loading_animation_widget/tree/main/lib/src/three_rotating_dots
|
|
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:twonly/src/utils/misc.dart';
|
|
|
|
extension LoadingAnimationControllerX on AnimationController {
|
|
T eval<T>(Tween<T> tween, {Curve curve = Curves.linear}) =>
|
|
tween.transform(curve.transform(value));
|
|
|
|
double evalDouble({
|
|
double from = 0,
|
|
double to = 1,
|
|
double begin = 0,
|
|
double end = 1,
|
|
Curve curve = Curves.linear,
|
|
}) {
|
|
return eval(
|
|
Tween<double>(begin: from, end: to),
|
|
curve: Interval(begin, end, curve: curve),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ThreeRotatingDots extends StatefulWidget {
|
|
const ThreeRotatingDots({
|
|
required this.size,
|
|
this.color,
|
|
super.key,
|
|
});
|
|
final double size;
|
|
final Color? color;
|
|
|
|
@override
|
|
State<ThreeRotatingDots> createState() => _ThreeRotatingDotsState();
|
|
}
|
|
|
|
class _ThreeRotatingDotsState extends State<ThreeRotatingDots>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_animationController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 2000),
|
|
)..repeat();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color = widget.color ?? context.color.primary;
|
|
final size = widget.size;
|
|
final dotSize = size / 3;
|
|
final edgeOffset = (size - dotSize) / 2;
|
|
|
|
const firstDotsInterval = Interval(
|
|
0,
|
|
0.50,
|
|
curve: Curves.easeInOutCubic,
|
|
);
|
|
const secondDotsInterval = Interval(
|
|
0.50,
|
|
1,
|
|
curve: Curves.easeInOutCubic,
|
|
);
|
|
|
|
return SizedBox(
|
|
width: size,
|
|
height: size,
|
|
child: AnimatedBuilder(
|
|
animation: _animationController,
|
|
builder: (_, _) => Transform.translate(
|
|
offset: Offset(0, size / 12),
|
|
child: Stack(
|
|
alignment: Alignment.center,
|
|
children: <Widget>[
|
|
_BuildDot.first(
|
|
color: color,
|
|
size: dotSize,
|
|
controller: _animationController,
|
|
dotOffset: edgeOffset,
|
|
beginAngle: math.pi,
|
|
endAngle: 0,
|
|
interval: firstDotsInterval,
|
|
),
|
|
|
|
_BuildDot.first(
|
|
color: color,
|
|
size: dotSize,
|
|
controller: _animationController,
|
|
dotOffset: edgeOffset,
|
|
beginAngle: 5 * math.pi / 3,
|
|
endAngle: 2 * math.pi / 3,
|
|
interval: firstDotsInterval,
|
|
),
|
|
|
|
_BuildDot.first(
|
|
color: color,
|
|
size: dotSize,
|
|
controller: _animationController,
|
|
dotOffset: edgeOffset,
|
|
beginAngle: 7 * math.pi / 3,
|
|
endAngle: 4 * math.pi / 3,
|
|
interval: firstDotsInterval,
|
|
),
|
|
|
|
/// Next 3 dots
|
|
_BuildDot.second(
|
|
controller: _animationController,
|
|
beginAngle: 0,
|
|
endAngle: -math.pi,
|
|
interval: secondDotsInterval,
|
|
dotOffset: edgeOffset,
|
|
color: color,
|
|
size: dotSize,
|
|
),
|
|
|
|
_BuildDot.second(
|
|
controller: _animationController,
|
|
beginAngle: 2 * math.pi / 3,
|
|
endAngle: -math.pi / 3,
|
|
interval: secondDotsInterval,
|
|
dotOffset: edgeOffset,
|
|
color: color,
|
|
size: dotSize,
|
|
),
|
|
|
|
_BuildDot.second(
|
|
controller: _animationController,
|
|
beginAngle: 4 * math.pi / 3,
|
|
endAngle: math.pi / 3,
|
|
interval: secondDotsInterval,
|
|
dotOffset: edgeOffset,
|
|
color: color,
|
|
size: dotSize,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
class _BuildDot extends StatelessWidget {
|
|
const _BuildDot.second({
|
|
required this.controller,
|
|
required this.beginAngle,
|
|
required this.endAngle,
|
|
required this.interval,
|
|
required this.dotOffset,
|
|
required this.color,
|
|
required this.size,
|
|
}) : first = false;
|
|
|
|
const _BuildDot.first({
|
|
required this.controller,
|
|
required this.beginAngle,
|
|
required this.endAngle,
|
|
required this.interval,
|
|
required this.dotOffset,
|
|
required this.color,
|
|
required this.size,
|
|
}) : first = true;
|
|
final AnimationController controller;
|
|
final double beginAngle;
|
|
final double endAngle;
|
|
final Interval interval;
|
|
final double dotOffset;
|
|
final Color color;
|
|
final double size;
|
|
final bool first;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Visibility(
|
|
visible: first
|
|
? controller.value <= interval.end
|
|
: controller.value >= interval.begin,
|
|
child: Transform.rotate(
|
|
angle: controller.eval(
|
|
Tween<double>(begin: beginAngle, end: endAngle),
|
|
curve: interval,
|
|
),
|
|
child: Transform.translate(
|
|
offset: controller.eval(
|
|
Tween<Offset>(
|
|
begin: first ? Offset.zero : Offset(0, -dotOffset),
|
|
end: first ? Offset(0, -dotOffset) : Offset.zero,
|
|
),
|
|
curve: interval,
|
|
),
|
|
child: DrawDot.circular(
|
|
color: color,
|
|
dotSize: size,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class DrawDot extends StatelessWidget {
|
|
const DrawDot.circular({
|
|
required double dotSize,
|
|
required this.color,
|
|
super.key,
|
|
}) : width = dotSize,
|
|
height = dotSize,
|
|
circular = true;
|
|
|
|
const DrawDot.elliptical({
|
|
required this.width,
|
|
required this.height,
|
|
required this.color,
|
|
super.key,
|
|
}) : circular = false;
|
|
final double width;
|
|
final double height;
|
|
final bool circular;
|
|
final Color color;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: width,
|
|
height: height,
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
shape: circular ? BoxShape.circle : BoxShape.rectangle,
|
|
borderRadius: circular
|
|
? null
|
|
: BorderRadius.all(Radius.elliptical(width, height)),
|
|
),
|
|
);
|
|
}
|
|
}
|