138 lines
3.8 KiB
Dart
138 lines
3.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Moves the week body without adding a second horizontal scroll surface.
|
|
/// Rapid requests are coalesced to the latest week after the current movement.
|
|
class DirectionalWeekTransition extends StatefulWidget {
|
|
const DirectionalWeekTransition({
|
|
super.key,
|
|
required this.week,
|
|
required this.child,
|
|
});
|
|
|
|
final int week;
|
|
final Widget child;
|
|
|
|
@override
|
|
State<DirectionalWeekTransition> createState() =>
|
|
_DirectionalWeekTransitionState();
|
|
}
|
|
|
|
class _DirectionalWeekTransitionState extends State<DirectionalWeekTransition>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _motion = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 280),
|
|
value: 1,
|
|
)..addStatusListener(_onStatus);
|
|
late Widget _incoming;
|
|
Widget? _outgoing;
|
|
late int _incomingWeek;
|
|
double _direction = 1;
|
|
bool _reduceMotion = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_incoming = widget.child;
|
|
_incomingWeek = widget.week;
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_reduceMotion = MediaQuery.disableAnimationsOf(context);
|
|
if (_reduceMotion) _snap();
|
|
}
|
|
|
|
void _snap() {
|
|
_motion.stop();
|
|
_outgoing = null;
|
|
_incoming = widget.child;
|
|
_incomingWeek = widget.week;
|
|
_motion.value = 1;
|
|
}
|
|
|
|
void _start() {
|
|
_outgoing = _incoming;
|
|
_direction = widget.week > _incomingWeek ? 1 : -1;
|
|
_incoming = widget.child;
|
|
_incomingWeek = widget.week;
|
|
_motion.forward(from: 0);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(DirectionalWeekTransition oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (_reduceMotion) {
|
|
_snap();
|
|
} else if (widget.week == _incomingWeek) {
|
|
_incoming = widget.child;
|
|
} else if (!_motion.isAnimating) {
|
|
_start();
|
|
}
|
|
// While moving, widget holds the latest request. Do not restart a tween or
|
|
// accumulate outgoing grids every time another swipe/arrow arrives.
|
|
}
|
|
|
|
void _onStatus(AnimationStatus status) {
|
|
if (status != AnimationStatus.completed ||
|
|
!mounted ||
|
|
_reduceMotion ||
|
|
_outgoing == null) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_outgoing = null;
|
|
if (widget.week != _incomingWeek) _start();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_motion.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => ClipRect(
|
|
child: AnimatedBuilder(
|
|
animation: _motion,
|
|
builder: (context, _) {
|
|
final progress = Curves.easeInOutCubic.transform(_motion.value);
|
|
final stale = widget.week != _incomingWeek;
|
|
return Stack(
|
|
alignment: Alignment.topLeft,
|
|
children: [
|
|
if (_outgoing != null)
|
|
ExcludeSemantics(
|
|
child: ExcludeFocus(
|
|
child: IgnorePointer(
|
|
child: FractionalTranslation(
|
|
key: const Key('week-transition-outgoing'),
|
|
translation: Offset(-_direction * 0.08 * progress, 0),
|
|
child: Opacity(opacity: 1 - progress, child: _outgoing),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
ExcludeSemantics(
|
|
excluding: stale,
|
|
child: ExcludeFocus(
|
|
excluding: stale,
|
|
child: IgnorePointer(
|
|
ignoring: stale,
|
|
child: FractionalTranslation(
|
|
key: const Key('week-transition-incoming'),
|
|
translation: Offset(_direction * 0.08 * (1 - progress), 0),
|
|
child: Opacity(opacity: progress, child: _incoming),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|