accessibility: remove animations (WIP)

This commit is contained in:
Thibault Deckers 2021-09-28 18:28:25 +09:00
parent 808d2de3bd
commit 8560ebdd14
14 changed files with 76 additions and 35 deletions

View file

@ -17,7 +17,7 @@ extension ExtraAccessibilityAnimations on AccessibilityAnimations {
}
}
bool get enabled {
bool get animate {
switch (this) {
case AccessibilityAnimations.system:
return !window.accessibilityFeatures.disableAnimations;

View file

@ -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,

View file

@ -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;

View file

@ -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);

View file

@ -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,
),

View file

@ -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,

View file

@ -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,

View file

@ -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,
),
);

View file

@ -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) {

View file

@ -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);

View file

@ -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,
),

View file

@ -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);

View file

@ -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);
});
}

View file

@ -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,