ClideMarquee honours reduced motion (MediaQuery.disableAnimations) (T-284)
The status-bar footer marquee drove a raw Ticker whenever its content overflowed, ignoring MediaQuery.disableAnimations — unlike the turn indicator (T-273), which stops on the same flag. Two costs: reduced-motion users still got the scrolling footer, and the perpetual ticker was the historical pumpAndSettle-hang culprit (its tests cope by only ever pump()-ing). Unify on the one mechanism: read disableAnimations in didChangeDependencies (as running_indicator does) and gate the ticker on it via _syncTicker(). Under reduced motion the marquee never starts the ticker (stops + resets if running) and renders the child statically (clipped, no looped copy). Toggling the flag at runtime starts/stops the scroll. Tests: reduced-motion marquee does not scroll and pumpAndSettle completes (no hang); flipping the flag off lets an overflowing marquee scroll again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,12 @@ class _ClideMarqueeState extends State<ClideMarquee> with SingleTickerProviderSt
|
||||
double _offset = 0;
|
||||
Duration _last = Duration.zero;
|
||||
|
||||
/// Honour reduced motion (T-284): the same `MediaQuery.disableAnimations`
|
||||
/// flag the running indicator gates on (T-273). When set, the marquee never
|
||||
/// scrolls — it shows the child statically (clipped) — so reduced-motion
|
||||
/// users get no motion and `pumpAndSettle` isn't wedged by a perpetual ticker.
|
||||
bool _reduced = false;
|
||||
|
||||
bool get _overflow => _contentWidth > _viewportWidth + 0.5;
|
||||
|
||||
@override
|
||||
@@ -47,6 +53,16 @@ class _ClideMarqueeState extends State<ClideMarquee> with SingleTickerProviderSt
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _measure());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final reduced = MediaQuery.maybeOf(context)?.disableAnimations ?? false;
|
||||
if (reduced != _reduced) {
|
||||
setState(() => _reduced = reduced);
|
||||
_syncTicker();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ClideMarquee old) {
|
||||
super.didUpdateWidget(old);
|
||||
@@ -59,10 +75,17 @@ class _ClideMarqueeState extends State<ClideMarquee> with SingleTickerProviderSt
|
||||
if ((w - _contentWidth).abs() > 0.5) {
|
||||
setState(() => _contentWidth = w);
|
||||
}
|
||||
if (_overflow && !_ticker.isActive) {
|
||||
_syncTicker();
|
||||
}
|
||||
|
||||
/// Start the scroll ticker only when the content overflows AND motion is
|
||||
/// allowed; otherwise stop it and reset to the start.
|
||||
void _syncTicker() {
|
||||
final shouldRun = _overflow && !_reduced;
|
||||
if (shouldRun && !_ticker.isActive) {
|
||||
_last = Duration.zero;
|
||||
_ticker.start();
|
||||
} else if (!_overflow && _ticker.isActive) {
|
||||
} else if (!shouldRun && _ticker.isActive) {
|
||||
_ticker.stop();
|
||||
_offset = 0;
|
||||
if (_scroll.hasClients) _scroll.jumpTo(0);
|
||||
@@ -70,7 +93,7 @@ class _ClideMarqueeState extends State<ClideMarquee> with SingleTickerProviderSt
|
||||
}
|
||||
|
||||
void _tick(Duration elapsed) {
|
||||
if (!_overflow || !_scroll.hasClients) return;
|
||||
if (!_overflow || _reduced || !_scroll.hasClients) return;
|
||||
final dt = _last == Duration.zero ? 0.0 : (elapsed - _last).inMicroseconds / 1e6;
|
||||
_last = elapsed;
|
||||
final span = _contentWidth + widget.gap;
|
||||
@@ -101,7 +124,7 @@ class _ClideMarqueeState extends State<ClideMarquee> with SingleTickerProviderSt
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
KeyedSubtree(key: _childKey, child: widget.child),
|
||||
if (_overflow) ...[SizedBox(width: widget.gap), widget.child],
|
||||
if (_overflow && !_reduced) ...[SizedBox(width: widget.gap), widget.child],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user