Add feedback service, reminder diagnostics and smooth week transitions
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -300,7 +300,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
|
||||
: _blue.withValues(alpha: 0.72),
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: _blue.withValues(
|
||||
color: Colors.white.withValues(
|
||||
alpha: index == _position.round().clamp(0, 2)
|
||||
? 0.60
|
||||
: 0.38,
|
||||
@@ -308,7 +308,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
|
||||
blurRadius: 8,
|
||||
),
|
||||
Shadow(
|
||||
color: const Color(0xFFEAF4FF).withValues(
|
||||
color: Colors.white.withValues(
|
||||
alpha: index == _position.round().clamp(0, 2)
|
||||
? 0.95
|
||||
: 0.85,
|
||||
@@ -316,7 +316,7 @@ class _GlassDockState extends State<GlassDock> with TickerProviderStateMixin {
|
||||
blurRadius: 2.8,
|
||||
),
|
||||
Shadow(
|
||||
color: _blue.withValues(
|
||||
color: Colors.white.withValues(
|
||||
alpha: index == _position.round().clamp(0, 2)
|
||||
? 0.34
|
||||
: 0.22,
|
||||
|
||||
Reference in New Issue
Block a user