accessibility: remove animations (WIP)
This commit is contained in:
parent
808d2de3bd
commit
8560ebdd14
14 changed files with 76 additions and 35 deletions
|
@ -17,7 +17,7 @@ extension ExtraAccessibilityAnimations on AccessibilityAnimations {
|
|||
}
|
||||
}
|
||||
|
||||
bool get enabled {
|
||||
bool get animate {
|
||||
switch (this) {
|
||||
case AccessibilityAnimations.system:
|
||||
return !window.accessibilityFeatures.disableAnimations;
|
||||
|
|
|
@ -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<DurationsProvider> 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<Settings, DurationsData>(
|
||||
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,
|
||||
|
|
|
@ -83,7 +83,7 @@ class _AvesAppState extends State<AvesApp> {
|
|||
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
|
||||
);
|
||||
return Selector<Settings, Tuple2<Locale?, bool>>(
|
||||
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;
|
||||
|
|
|
@ -57,7 +57,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
void initState() {
|
||||
super.initState();
|
||||
_browseToSelectAnimation = AnimationController(
|
||||
duration: Durations.iconAnimation,
|
||||
duration: context.read<DurationsData>().iconAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
_isSelectingNotifier.addListener(_onActivityChange);
|
||||
|
|
|
@ -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<T> extends State<ReportOverlay<T>> with SingleTickerPr
|
|||
final total = widget.itemCount;
|
||||
assert(processedCount <= total);
|
||||
final percent = min(1.0, processedCount / total);
|
||||
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: Container(
|
||||
|
@ -156,22 +159,23 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> 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,
|
||||
),
|
||||
|
|
|
@ -297,8 +297,8 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
),
|
||||
);
|
||||
|
||||
final enabledAnimations = context.select<Settings, bool>((v) => v.accessibilityAnimations.enabled);
|
||||
if (enabledAnimations && (widget.heroType == HeroType.always || widget.heroType == HeroType.onTap && _tapped)) {
|
||||
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
|
||||
if (animate && (widget.heroType == HeroType.always || widget.heroType == HeroType.onTap && _tapped)) {
|
||||
chip = Hero(
|
||||
tag: filter,
|
||||
transitionOnUserGestures: true,
|
||||
|
|
|
@ -25,7 +25,7 @@ class MapTheme extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ProxyProvider<Settings, MapThemeData>(
|
||||
update: (_, settings, __) {
|
||||
update: (context, settings, __) {
|
||||
return MapThemeData(
|
||||
interactive: interactive,
|
||||
navigationButton: navigationButton,
|
||||
|
|
|
@ -18,7 +18,7 @@ class TileExtentControllerProvider extends StatelessWidget {
|
|||
builder: (context, constraints) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => ProxyProvider0<TileExtentController>(
|
||||
update: (_, __) => controller..setViewportSize(constraints.biggest),
|
||||
update: (context, __) => controller..setViewportSize(constraints.biggest),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -173,9 +173,9 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enabledAnimations = context.select<Settings, bool>((v) => v.accessibilityAnimations.enabled);
|
||||
final animate = context.select<Settings, bool>((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<ThumbnailImage> {
|
|||
},
|
||||
);
|
||||
|
||||
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<ThumbnailImage> {
|
|||
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) {
|
||||
|
|
|
@ -59,7 +59,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
|
|||
void initState() {
|
||||
super.initState();
|
||||
_browseToSelectAnimation = AnimationController(
|
||||
duration: Durations.iconAnimation,
|
||||
duration: context.read<DurationsData>().iconAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
_isSelectingNotifier.addListener(_onActivityChange);
|
||||
|
|
|
@ -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<Settings, bool>((v) => v.accessibilityAnimations.animate);
|
||||
final byMimeTypes = groupBy<AvesEntry, String>(entries, (entry) => entry.mimeType).map<String, int>((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<String, int> byMimeTypes) {
|
||||
Widget _buildMimeDonut(
|
||||
BuildContext context,
|
||||
String Function(int) label,
|
||||
Map<String, int> byMimeTypes,
|
||||
bool animate,
|
||||
) {
|
||||
if (byMimeTypes.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final sum = byMimeTypes.values.fold<int>(0, (prev, v) => prev + v);
|
||||
|
@ -165,6 +174,7 @@ class StatsPage extends StatelessWidget {
|
|||
children: [
|
||||
charts.PieChart(
|
||||
series,
|
||||
animate: animate,
|
||||
defaultRenderer: charts.ArcRendererConfig<String>(
|
||||
arcWidth: 16,
|
||||
),
|
||||
|
|
|
@ -371,7 +371,7 @@ class _PlayTogglerState extends State<_PlayToggler> with SingleTickerProviderSta
|
|||
void initState() {
|
||||
super.initState();
|
||||
_playPauseAnimation = AnimationController(
|
||||
duration: Durations.iconAnimation,
|
||||
duration: context.read<DurationsData>().iconAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
_registerWidget(widget);
|
||||
|
|
|
@ -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<DurationsData>().iconAnimation + Durations.videoOverlayHideDelay, () {
|
||||
const ToggleOverlayNotification(visible: false).dispatch(context);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -115,8 +115,8 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
},
|
||||
);
|
||||
|
||||
final enabledAnimations = context.select<Settings, bool>((v) => v.accessibilityAnimations.enabled);
|
||||
if (enabledAnimations) {
|
||||
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
|
||||
if (animate) {
|
||||
child = Consumer<HeroInfo?>(
|
||||
builder: (context, info, child) => Hero(
|
||||
tag: info != null && info.entry == mainEntry ? Object.hashAll([info.collectionId, mainEntry.uri]) : hashCode,
|
||||
|
|
Loading…
Reference in a new issue