diff --git a/lib/model/settings/accessibility_animations.dart b/lib/model/settings/accessibility_animations.dart index 8af597d78..33819c55d 100644 --- a/lib/model/settings/accessibility_animations.dart +++ b/lib/model/settings/accessibility_animations.dart @@ -17,7 +17,7 @@ extension ExtraAccessibilityAnimations on AccessibilityAnimations { } } - bool get enabled { + bool get animate { switch (this) { case AccessibilityAnimations.system: return !window.accessibilityFeatures.disableAnimations; diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index e9f509671..0f99240a7 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -1,4 +1,5 @@ import 'package:aves/model/settings/accessibility_animations.dart'; +import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; @@ -13,7 +14,6 @@ class Durations { static const toggleableTransitionAnimation = Duration(milliseconds: 200 + 10); // ref `_kToggleDuration` used in `ToggleableStateMixin` // common animations - static const iconAnimation = Duration(milliseconds: 300); static const sweeperOpacityAnimation = Duration(milliseconds: 150); static const sweepingAnimation = Duration(milliseconds: 650); static const dialogFieldReachAnimation = Duration(milliseconds: 300); @@ -72,7 +72,7 @@ class Durations { static const lastVersionCheckInterval = Duration(days: 7); } -class DurationsProvider extends StatelessWidget { +class DurationsProvider extends StatefulWidget { final Widget child; const DurationsProvider({ @@ -80,14 +80,38 @@ class DurationsProvider extends StatelessWidget { required this.child, }) : super(key: key); + @override + _DurationsProviderState createState() => _DurationsProviderState(); +} + +class _DurationsProviderState extends State with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance!.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance!.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAccessibilityFeatures() { + if (settings.accessibilityAnimations == AccessibilityAnimations.system) { + // TODO TLAD update provider + } + } + @override Widget build(BuildContext context) { return ProxyProvider( - update: (_, settings, __) { - final enabled = settings.accessibilityAnimations.enabled; + update: (context, settings, __) { + final enabled = settings.accessibilityAnimations.animate; return enabled ? DurationsData() : DurationsData.noAnimation(); }, - child: child, + child: widget.child, ); } } @@ -96,6 +120,7 @@ class DurationsProvider extends StatelessWidget { class DurationsData { // common animations final Duration expansionTileAnimation; + final Duration iconAnimation; final Duration staggeredAnimation; final Duration staggeredAnimationPageTarget; @@ -109,6 +134,7 @@ class DurationsData { const DurationsData({ this.expansionTileAnimation = const Duration(milliseconds: 200), + this.iconAnimation = const Duration(milliseconds: 300), this.staggeredAnimation = const Duration(milliseconds: 375), this.staggeredAnimationPageTarget = const Duration(milliseconds: 800), this.viewerVerticalPageScrollAnimation = const Duration(milliseconds: 500), @@ -120,6 +146,7 @@ class DurationsData { return DurationsData( // as of Flutter v2.5.1, `ExpansionPanelList` throws if animation duration is zero expansionTileAnimation: const Duration(microseconds: 1), + iconAnimation: Duration.zero, staggeredAnimation: Duration.zero, staggeredAnimationPageTarget: Duration.zero, viewerVerticalPageScrollAnimation: Duration.zero, diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index fd9d708e8..5b81966b5 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -83,7 +83,7 @@ class _AvesAppState extends State { body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), ); return Selector>( - selector: (context, s) => Tuple2(s.locale, s.initialized ? s.accessibilityAnimations.enabled : true), + selector: (context, s) => Tuple2(s.locale, s.initialized ? s.accessibilityAnimations.animate : true), builder: (context, s, child) { final settingsLocale = s.item1; final areAnimationsEnabled = s.item2; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 6ec76cd79..93e51e1cb 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -57,7 +57,7 @@ class _CollectionAppBarState extends State with SingleTickerPr void initState() { super.initState(); _browseToSelectAnimation = AnimationController( - duration: Durations.iconAnimation, + duration: context.read().iconAnimation, vsync: this, ); _isSelectingNotifier.addListener(_onActivityChange); diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index e37f655e7..b21060554 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; +import 'package:aves/model/settings/accessibility_animations.dart'; import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/accessibility_service.dart'; @@ -8,6 +9,7 @@ import 'package:aves/theme/durations.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:percent_indicator/circular_percent_indicator.dart'; +import 'package:provider/provider.dart'; mixin FeedbackMixin { void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar(); @@ -142,6 +144,7 @@ class _ReportOverlayState extends State> with SingleTickerPr final total = widget.itemCount; assert(processedCount <= total); final percent = min(1.0, processedCount / total); + final animate = context.select((v) => v.accessibilityAnimations.animate); return FadeTransition( opacity: _animation, child: Container( @@ -156,22 +159,23 @@ class _ReportOverlayState extends State> with SingleTickerPr child: Center( child: Stack( children: [ - Container( - width: radius, - height: radius, - padding: const EdgeInsets.all(strokeWidth / 2), - child: CircularProgressIndicator( - color: progressColor.withOpacity(.1), - strokeWidth: strokeWidth, + if (animate) + Container( + width: radius, + height: radius, + padding: const EdgeInsets.all(strokeWidth / 2), + child: CircularProgressIndicator( + color: progressColor.withOpacity(.1), + strokeWidth: strokeWidth, + ), ), - ), CircularPercentIndicator( percent: percent, lineWidth: strokeWidth, radius: radius, backgroundColor: Colors.white24, progressColor: progressColor, - animation: true, + animation: animate, center: Text(NumberFormat.percentPattern().format(percent)), animateFromLastPercent: true, ), diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 54fc91daa..859fac9c3 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -297,8 +297,8 @@ class _AvesFilterChipState extends State { ), ); - final enabledAnimations = context.select((v) => v.accessibilityAnimations.enabled); - if (enabledAnimations && (widget.heroType == HeroType.always || widget.heroType == HeroType.onTap && _tapped)) { + final animate = context.select((v) => v.accessibilityAnimations.animate); + if (animate && (widget.heroType == HeroType.always || widget.heroType == HeroType.onTap && _tapped)) { chip = Hero( tag: filter, transitionOnUserGestures: true, diff --git a/lib/widgets/common/map/theme.dart b/lib/widgets/common/map/theme.dart index 570813bca..c2690d2f2 100644 --- a/lib/widgets/common/map/theme.dart +++ b/lib/widgets/common/map/theme.dart @@ -25,7 +25,7 @@ class MapTheme extends StatelessWidget { @override Widget build(BuildContext context) { return ProxyProvider( - update: (_, settings, __) { + update: (context, settings, __) { return MapThemeData( interactive: interactive, navigationButton: navigationButton, diff --git a/lib/widgets/common/providers/tile_extent_controller_provider.dart b/lib/widgets/common/providers/tile_extent_controller_provider.dart index 4bc322343..995279d68 100644 --- a/lib/widgets/common/providers/tile_extent_controller_provider.dart +++ b/lib/widgets/common/providers/tile_extent_controller_provider.dart @@ -18,7 +18,7 @@ class TileExtentControllerProvider extends StatelessWidget { builder: (context, constraints) { return LayoutBuilder( builder: (context, constraints) => ProxyProvider0( - update: (_, __) => controller..setViewportSize(constraints.biggest), + update: (context, __) => controller..setViewportSize(constraints.biggest), child: child, ), ); diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index 322feadf2..ed3461e1c 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -173,9 +173,9 @@ class _ThumbnailImageState extends State { @override Widget build(BuildContext context) { - final enabledAnimations = context.select((v) => v.accessibilityAnimations.enabled); + final animate = context.select((v) => v.accessibilityAnimations.animate); if (!entry.canDecode || _lastException != null) { - return _buildError(context, enabledAnimations); + return _buildError(context, animate); } // use `RawImage` instead of `Image`, using `ImageInfo` to check dimensions @@ -231,7 +231,7 @@ class _ThumbnailImageState extends State { }, ); - if (enabledAnimations && widget.heroTag != null) { + if (animate && widget.heroTag != null) { image = Hero( tag: widget.heroTag!, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { @@ -248,13 +248,13 @@ class _ThumbnailImageState extends State { return image; } - Widget _buildError(BuildContext context, bool enabledAnimations) { + Widget _buildError(BuildContext context, bool animate) { Widget child = ErrorThumbnail( entry: entry, extent: extent, ); - if (enabledAnimations && widget.heroTag != null) { + if (animate && widget.heroTag != null) { child = Hero( tag: widget.heroTag!, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index dac903429..cc5bbbfc5 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -59,7 +59,7 @@ class _FilterGridAppBarState extends State().iconAnimation, vsync: this, ); _isSelectingNotifier.addListener(_onActivityChange); diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 585cba489..bf32ce914 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -5,6 +5,8 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; +import 'package:aves/model/settings/accessibility_animations.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; @@ -22,6 +24,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; +import 'package:provider/provider.dart'; class StatsPage extends StatelessWidget { static const routeName = '/collection/stats'; @@ -67,6 +70,7 @@ class StatsPage extends StatelessWidget { text: context.l10n.collectionEmptyImages, ); } else { + final animate = context.select((v) => v.accessibilityAnimations.animate); final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image'))); final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video'))); @@ -74,8 +78,8 @@ class StatsPage extends StatelessWidget { alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ - _buildMimeDonut(context, (sum) => context.l10n.statsImage(sum), imagesByMimeTypes), - _buildMimeDonut(context, (sum) => context.l10n.statsVideo(sum), videoByMimeTypes), + _buildMimeDonut(context, (sum) => context.l10n.statsImage(sum), imagesByMimeTypes, animate), + _buildMimeDonut(context, (sum) => context.l10n.statsVideo(sum), videoByMimeTypes, animate), ], ); @@ -94,7 +98,7 @@ class StatsPage extends StatelessWidget { lineHeight: lineHeight, backgroundColor: Colors.white24, progressColor: Theme.of(context).colorScheme.secondary, - animation: true, + animation: animate, leading: const Icon(AIcons.location), // right padding to match leading, so that inside label is aligned with outside label below padding: EdgeInsets.symmetric(horizontal: lineHeight) + const EdgeInsets.only(right: 24), @@ -130,7 +134,12 @@ class StatsPage extends StatelessWidget { ); } - Widget _buildMimeDonut(BuildContext context, String Function(int) label, Map byMimeTypes) { + Widget _buildMimeDonut( + BuildContext context, + String Function(int) label, + Map byMimeTypes, + bool animate, + ) { if (byMimeTypes.isEmpty) return const SizedBox.shrink(); final sum = byMimeTypes.values.fold(0, (prev, v) => prev + v); @@ -165,6 +174,7 @@ class StatsPage extends StatelessWidget { children: [ charts.PieChart( series, + animate: animate, defaultRenderer: charts.ArcRendererConfig( arcWidth: 16, ), diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video.dart index 5809c2b38..5c606a422 100644 --- a/lib/widgets/viewer/overlay/bottom/video.dart +++ b/lib/widgets/viewer/overlay/bottom/video.dart @@ -371,7 +371,7 @@ class _PlayTogglerState extends State<_PlayToggler> with SingleTickerProviderSta void initState() { super.initState(); _playPauseAnimation = AnimationController( - duration: Durations.iconAnimation, + duration: context.read().iconAnimation, vsync: this, ); _registerWidget(widget); diff --git a/lib/widgets/viewer/video_action_delegate.dart b/lib/widgets/viewer/video_action_delegate.dart index b751a774e..96ac37e7f 100644 --- a/lib/widgets/viewer/video_action_delegate.dart +++ b/lib/widgets/viewer/video_action_delegate.dart @@ -181,7 +181,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } else { await controller.play(); // hide overlay - _overlayHidingTimer = Timer(Durations.iconAnimation + Durations.videoOverlayHideDelay, () { + _overlayHidingTimer = Timer(context.read().iconAnimation + Durations.videoOverlayHideDelay, () { const ToggleOverlayNotification(visible: false).dispatch(context); }); } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 9401777dc..e3a9e153d 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -115,8 +115,8 @@ class _EntryPageViewState extends State { }, ); - final enabledAnimations = context.select((v) => v.accessibilityAnimations.enabled); - if (enabledAnimations) { + final animate = context.select((v) => v.accessibilityAnimations.animate); + if (animate) { child = Consumer( builder: (context, info, child) => Hero( tag: info != null && info.entry == mainEntry ? Object.hashAll([info.collectionId, mainEntry.uri]) : hashCode,