aves/lib/widgets/common/fx/sweeper.dart
2021-01-11 15:11:05 +09:00

156 lines
4.2 KiB
Dart

import 'dart:math';
import 'package:aves/theme/durations.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class Sweeper extends StatefulWidget {
final WidgetBuilder builder;
final double startAngle;
final double sweepAngle;
final Curve curve;
final ValueNotifier<bool> toggledNotifier;
final bool centerSweep;
final VoidCallback onSweepEnd;
const Sweeper({
Key key,
@required this.builder,
this.startAngle = -pi / 2,
this.sweepAngle = pi / 4,
this.curve = Curves.easeInOutCubic,
@required this.toggledNotifier,
this.centerSweep = true,
this.onSweepEnd,
}) : super(key: key);
@override
_SweeperState createState() => _SweeperState();
}
class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
AnimationController _angleAnimationController;
Animation<double> _angle;
bool _isAppearing = false;
bool get isToggled => widget.toggledNotifier.value;
@override
void initState() {
super.initState();
_angleAnimationController = AnimationController(
duration: Durations.sweepingAnimation,
vsync: this,
);
final startAngle = widget.startAngle;
final sweepAngle = widget.sweepAngle;
final centerSweep = widget.centerSweep;
_angle = Tween(
begin: startAngle - sweepAngle * (centerSweep ? .5 : 0),
end: startAngle + pi * 2 - sweepAngle * (centerSweep ? .5 : 1),
).animate(CurvedAnimation(
parent: _angleAnimationController,
curve: widget.curve,
));
_angleAnimationController.addStatusListener(_onAnimationStatusChange);
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant Sweeper oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_angleAnimationController.removeStatusListener(_onAnimationStatusChange);
_angleAnimationController.dispose();
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(Sweeper widget) {
widget.toggledNotifier.addListener(_onToggle);
}
void _unregisterWidget(Sweeper widget) {
widget.toggledNotifier.removeListener(_onToggle);
}
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: AnimatedOpacity(
opacity: isToggled && (_isAppearing || _angleAnimationController.status == AnimationStatus.forward) ? 1 : 0,
duration: Durations.sweeperOpacityAnimation,
child: ValueListenableBuilder<double>(
valueListenable: _angleAnimationController,
builder: (context, value, child) {
return ClipPath(
child: widget.builder(context),
clipper: _SweepClipPath(
startAngle: _angle.value,
sweepAngle: widget.sweepAngle,
),
);
}),
),
);
}
void _onAnimationStatusChange(AnimationStatus status) {
setState(() {});
if (status == AnimationStatus.completed) {
widget.onSweepEnd?.call();
}
}
Future<void> _onToggle() async {
if (isToggled) {
_isAppearing = true;
setState(() {});
await Future.delayed(Durations.sweeperOpacityAnimation * timeDilation);
_isAppearing = false;
if (mounted) {
_angleAnimationController.reset();
_angleAnimationController.forward();
}
}
if (mounted) {
setState(() {});
}
}
}
class _SweepClipPath extends CustomClipper<Path> {
final double startAngle;
final double sweepAngle;
const _SweepClipPath({@required this.startAngle, @required this.sweepAngle});
@override
Path getClip(Size size) {
final width = size.width;
final height = size.height;
final centerX = width / 2;
final centerY = height / 2;
final diagonal = sqrt(width * width + height * height);
return Path()
..moveTo(centerX, centerY)
..addArc(
Rect.fromCenter(
center: Offset(centerX, centerY),
width: diagonal,
height: diagonal,
),
startAngle,
sweepAngle,
)
..lineTo(centerX, centerY);
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;
}