From 0a8348b324b64b7db13d88223358774c2b54f4ed Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Mon, 9 Mar 2026 18:03:34 +0100 Subject: [PATCH] respect disableAnimations flag --- .../browser/presentation/screens/browser.dart | 4 ++- .../widgets/browser_menu_sheet.dart | 13 ++++---- .../widgets/draggable_scrollable_header.dart | 5 +++ .../presentation/screens/sync_settings.dart | 8 +++-- app/lib/main.dart | 31 +++++++++++++++++++ app/lib/presentation/main_app.dart | 9 ++++++ .../widgets/animate_gradient_shader.dart | 25 ++++++++++++--- .../widgets/animated_indexed_stack.dart | 26 +++++++++++++--- 8 files changed, 102 insertions(+), 19 deletions(-) diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 6bd477a4..92becfb5 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -76,8 +76,10 @@ class _AnimatedToolbar extends HookWidget { @override Widget build(BuildContext context) { + final disableAnimations = MediaQuery.disableAnimationsOf(context); + final controller = useAnimationController( - duration: _kAnimationDuration, + duration: disableAnimations ? Duration.zero : _kAnimationDuration, initialValue: visible ? 1.0 : 0.0, ); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart index 4ec3606f..1f434bb3 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart @@ -618,10 +618,7 @@ class _PinTopSiteTile extends HookConsumerWidget { } else { await ref .read(topSiteRepositoryProvider.notifier) - .addPinnedSite( - title: tabState.titleOrAuthority, - url: url, - ); + .addPinnedSite(title: tabState.titleOrAuthority, url: url); if (context.mounted) { ui_helper.showInfoMessage(context, 'Pinned to Top Sites'); } @@ -1932,19 +1929,21 @@ class _SyncTile extends HookConsumerWidget { ); final isSyncing = syncStarted || syncInfo?.syncing == true; + final disableAnimations = MediaQuery.disableAnimationsOf(context); + final controller = useAnimationController( - duration: const Duration(seconds: 2), + duration: disableAnimations ? Duration.zero : const Duration(seconds: 2), ); useEffect(() { - if (isSyncing) { + if (isSyncing && !disableAnimations) { unawaited(controller.repeat()); } else { controller.stop(); controller.reset(); } return null; - }, [isSyncing]); + }, [isSyncing, disableAnimations]); return ListTile( leading: RotationTransition( diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart index 4cf39b11..979c2d10 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart @@ -74,6 +74,11 @@ class _DraggableScrollableHeaderState extends State double targetSize, { Duration? customDuration, }) async { + if (MediaQuery.disableAnimationsOf(context)) { + widget.controller.jumpTo(targetSize); + return; + } + if (customDuration != null) { _animationController.duration = customDuration; } else { diff --git a/app/lib/features/sync/presentation/screens/sync_settings.dart b/app/lib/features/sync/presentation/screens/sync_settings.dart index 45201b2a..44f7ee7b 100644 --- a/app/lib/features/sync/presentation/screens/sync_settings.dart +++ b/app/lib/features/sync/presentation/screens/sync_settings.dart @@ -53,19 +53,21 @@ class SyncSettingsScreen extends HookConsumerWidget { return 'Last synced: $formattedDate'; }, [syncInfo, isSyncing]); + final disableAnimations = MediaQuery.disableAnimationsOf(context); + final syncController = useAnimationController( - duration: const Duration(seconds: 2), + duration: disableAnimations ? Duration.zero : const Duration(seconds: 2), ); useEffect(() { - if (isSyncing) { + if (isSyncing && !disableAnimations) { unawaited(syncController.repeat()); } else { syncController.stop(); syncController.reset(); } return null; - }, [isSyncing]); + }, [isSyncing, disableAnimations]); return Scaffold( appBar: AppBar(title: const Text('Firefox Sync')), diff --git a/app/lib/main.dart b/app/lib/main.dart index 6bb37f2e..47d9a26e 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -81,6 +81,31 @@ bool _hasBrokenSurfaceContainerColors(ColorScheme scheme) { scheme.surfaceContainerHighest == scheme.surface; } +class _NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder { + const _NoAnimationPageTransitionsBuilder(); + + @override + Widget buildTransitions( + PageRoute route, + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return child; + } +} + +const _noAnimationPageTransitionsTheme = PageTransitionsTheme( + builders: { + TargetPlatform.android: _NoAnimationPageTransitionsBuilder(), + TargetPlatform.iOS: _NoAnimationPageTransitionsBuilder(), + TargetPlatform.linux: _NoAnimationPageTransitionsBuilder(), + TargetPlatform.macOS: _NoAnimationPageTransitionsBuilder(), + TargetPlatform.windows: _NoAnimationPageTransitionsBuilder(), + }, +); + class _MainWidget extends HookConsumerWidget { const _MainWidget(); @@ -257,11 +282,17 @@ class _MainWidget extends HookConsumerWidget { theme: ThemeData( useMaterial3: true, colorScheme: lightColorScheme, + pageTransitionsTheme: disableAnimations + ? _noAnimationPageTransitionsTheme + : null, extensions: const >[AppColors.light], ), darkTheme: ThemeData( useMaterial3: true, colorScheme: darkColorScheme, + pageTransitionsTheme: disableAnimations + ? _noAnimationPageTransitionsTheme + : null, extensions: const >[AppColors.dark], ), themeMode: themeMode, diff --git a/app/lib/presentation/main_app.dart b/app/lib/presentation/main_app.dart index aa1f8dd5..c83b06e2 100644 --- a/app/lib/presentation/main_app.dart +++ b/app/lib/presentation/main_app.dart @@ -55,6 +55,9 @@ class MainApp extends HookConsumerWidget { theme: theme, darkTheme: darkTheme, themeMode: themeMode, + themeAnimationStyle: disableAnimations + ? AnimationStyle.noAnimation + : null, builder: (context, child) { return _AppMediaQueryOverrides( uiScaleFactor: uiScaleFactor, @@ -85,6 +88,9 @@ class MainApp extends HookConsumerWidget { theme: theme, darkTheme: darkTheme, themeMode: themeMode, + themeAnimationStyle: disableAnimations + ? AnimationStyle.noAnimation + : null, routerConfig: router.value, builder: (context, child) { return _AppMediaQueryOverrides( @@ -103,6 +109,9 @@ class MainApp extends HookConsumerWidget { theme: theme, darkTheme: darkTheme, themeMode: themeMode, + themeAnimationStyle: disableAnimations + ? AnimationStyle.noAnimation + : null, builder: (context, child) { return _AppMediaQueryOverrides( uiScaleFactor: uiScaleFactor, diff --git a/app/lib/presentation/widgets/animate_gradient_shader.dart b/app/lib/presentation/widgets/animate_gradient_shader.dart index 318c9a92..06f42d1a 100644 --- a/app/lib/presentation/widgets/animate_gradient_shader.dart +++ b/app/lib/presentation/widgets/animate_gradient_shader.dart @@ -120,12 +120,24 @@ class _AnimateGradientShaderState extends State List primaryColors = []; List secondaryColors = []; + bool _disableAnimations = false; + @override void initState() { _initialize(); super.initState(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final disableAnimations = MediaQuery.disableAnimationsOf(context); + if (disableAnimations != _disableAnimations) { + _disableAnimations = disableAnimations; + _setAnimations(); + } + } + @override void didUpdateWidget(AnimateGradientShader oldWidget) { _initialize(); @@ -222,10 +234,15 @@ class _AnimateGradientShaderState extends State void _setAnimations() { _controller?.dispose(); _controller = - (widget.controller ?? - AnimationController(vsync: this, duration: widget.duration)) - // ignore: discarded_futures - ..repeat(reverse: widget.reverse); + widget.controller ?? + AnimationController(vsync: this, duration: widget.duration); + + if (_disableAnimations) { + _controller!.value = 0; + } else { + // ignore: discarded_futures + _controller!.repeat(reverse: widget.reverse); + } _animation = CurvedAnimation(parent: _controller!, curve: Curves.easeInOut); } diff --git a/app/lib/presentation/widgets/animated_indexed_stack.dart b/app/lib/presentation/widgets/animated_indexed_stack.dart index a60dc80e..5eb363d1 100644 --- a/app/lib/presentation/widgets/animated_indexed_stack.dart +++ b/app/lib/presentation/widgets/animated_indexed_stack.dart @@ -129,12 +129,30 @@ class _AnimatedIndexedStackState extends State /// The entry which is currently at the top of the stack. _ChildEntry? _currentEntry; + bool _disableAnimations = false; + + Duration get _effectiveDuration => + _disableAnimations ? Duration.zero : widget.duration; + @override void initState() { super.initState(); _updateEntriesList(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final disableAnimations = MediaQuery.disableAnimationsOf(context); + if (disableAnimations != _disableAnimations) { + _disableAnimations = disableAnimations; + for (final entry in _entries) { + entry.primaryController.duration = _effectiveDuration; + entry.secondaryController.duration = _effectiveDuration; + } + } + } + @override void didUpdateWidget(AnimatedIndexedStack oldWidget) { super.didUpdateWidget(oldWidget); @@ -207,8 +225,8 @@ class _AnimatedIndexedStackState extends State // If we find an existing entry, we update its child widget and reuse it. // This ensures it continues to use the same global key and animation controllers. existingEntry.child = child; - existingEntry.primaryController.duration = widget.duration; - existingEntry.secondaryController.duration = widget.duration; + existingEntry.primaryController.duration = _effectiveDuration; + existingEntry.secondaryController.duration = _effectiveDuration; entry = existingEntry; } else { entry = _newEntry(child); @@ -299,11 +317,11 @@ class _AnimatedIndexedStackState extends State key: GlobalKey(), child: child, primaryController: AnimationController( - duration: widget.duration, + duration: _effectiveDuration, vsync: this, ), secondaryController: AnimationController( - duration: widget.duration, + duration: _effectiveDuration, vsync: this, ), );