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 createState() => _DirectionalWeekTransitionState(); } class _DirectionalWeekTransitionState extends State 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), ), ), ), ), ], ); }, ), ); }