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) { switch (this) {
case AccessibilityAnimations.system: case AccessibilityAnimations.system:
return !window.accessibilityFeatures.disableAnimations; return !window.accessibilityFeatures.disableAnimations;

View file

@ -1,4 +1,5 @@
import 'package:aves/model/settings/accessibility_animations.dart'; 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/model/settings/settings.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -13,7 +14,6 @@ class Durations {
static const toggleableTransitionAnimation = Duration(milliseconds: 200 + 10); // ref `_kToggleDuration` used in `ToggleableStateMixin` static const toggleableTransitionAnimation = Duration(milliseconds: 200 + 10); // ref `_kToggleDuration` used in `ToggleableStateMixin`
// common animations // common animations
static const iconAnimation = Duration(milliseconds: 300);
static const sweeperOpacityAnimation = Duration(milliseconds: 150); static const sweeperOpacityAnimation = Duration(milliseconds: 150);
static const sweepingAnimation = Duration(milliseconds: 650); static const sweepingAnimation = Duration(milliseconds: 650);
static const dialogFieldReachAnimation = Duration(milliseconds: 300); static const dialogFieldReachAnimation = Duration(milliseconds: 300);
@ -72,7 +72,7 @@ class Durations {
static const lastVersionCheckInterval = Duration(days: 7); static const lastVersionCheckInterval = Duration(days: 7);
} }
class DurationsProvider extends StatelessWidget { class DurationsProvider extends StatefulWidget {
final Widget child; final Widget child;
const DurationsProvider({ const DurationsProvider({
@ -80,14 +80,38 @@ class DurationsProvider extends StatelessWidget {
required this.child, required this.child,
}) : super(key: key); }) : 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ProxyProvider<Settings, DurationsData>( return ProxyProvider<Settings, DurationsData>(
update: (_, settings, __) { update: (context, settings, __) {
final enabled = settings.accessibilityAnimations.enabled; final enabled = settings.accessibilityAnimations.animate;
return enabled ? DurationsData() : DurationsData.noAnimation(); return enabled ? DurationsData() : DurationsData.noAnimation();
}, },
child: child, child: widget.child,
); );
} }
} }
@ -96,6 +120,7 @@ class DurationsProvider extends StatelessWidget {
class DurationsData { class DurationsData {
// common animations // common animations
final Duration expansionTileAnimation; final Duration expansionTileAnimation;
final Duration iconAnimation;
final Duration staggeredAnimation; final Duration staggeredAnimation;
final Duration staggeredAnimationPageTarget; final Duration staggeredAnimationPageTarget;
@ -109,6 +134,7 @@ class DurationsData {
const DurationsData({ const DurationsData({
this.expansionTileAnimation = const Duration(milliseconds: 200), this.expansionTileAnimation = const Duration(milliseconds: 200),
this.iconAnimation = const Duration(milliseconds: 300),
this.staggeredAnimation = const Duration(milliseconds: 375), this.staggeredAnimation = const Duration(milliseconds: 375),
this.staggeredAnimationPageTarget = const Duration(milliseconds: 800), this.staggeredAnimationPageTarget = const Duration(milliseconds: 800),
this.viewerVerticalPageScrollAnimation = const Duration(milliseconds: 500), this.viewerVerticalPageScrollAnimation = const Duration(milliseconds: 500),
@ -120,6 +146,7 @@ class DurationsData {
return DurationsData( return DurationsData(
// as of Flutter v2.5.1, `ExpansionPanelList` throws if animation duration is zero // as of Flutter v2.5.1, `ExpansionPanelList` throws if animation duration is zero
expansionTileAnimation: const Duration(microseconds: 1), expansionTileAnimation: const Duration(microseconds: 1),
iconAnimation: Duration.zero,
staggeredAnimation: Duration.zero, staggeredAnimation: Duration.zero,
staggeredAnimationPageTarget: Duration.zero, staggeredAnimationPageTarget: Duration.zero,
viewerVerticalPageScrollAnimation: Duration.zero, viewerVerticalPageScrollAnimation: Duration.zero,

View file

@ -83,7 +83,7 @@ class _AvesAppState extends State<AvesApp> {
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
); );
return Selector<Settings, Tuple2<Locale?, bool>>( 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) { builder: (context, s, child) {
final settingsLocale = s.item1; final settingsLocale = s.item1;
final areAnimationsEnabled = s.item2; final areAnimationsEnabled = s.item2;

View file

@ -57,7 +57,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
void initState() { void initState() {
super.initState(); super.initState();
_browseToSelectAnimation = AnimationController( _browseToSelectAnimation = AnimationController(
duration: Durations.iconAnimation, duration: context.read<DurationsData>().iconAnimation,
vsync: this, vsync: this,
); );
_isSelectingNotifier.addListener(_onActivityChange); _isSelectingNotifier.addListener(_onActivityChange);

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/settings/accessibility_animations.dart';
import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/accessibility_service.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:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:percent_indicator/circular_percent_indicator.dart';
import 'package:provider/provider.dart';
mixin FeedbackMixin { mixin FeedbackMixin {
void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar(); 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; final total = widget.itemCount;
assert(processedCount <= total); assert(processedCount <= total);
final percent = min(1.0, processedCount / total); final percent = min(1.0, processedCount / total);
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
return FadeTransition( return FadeTransition(
opacity: _animation, opacity: _animation,
child: Container( child: Container(
@ -156,22 +159,23 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
child: Center( child: Center(
child: Stack( child: Stack(
children: [ children: [
Container( if (animate)
width: radius, Container(
height: radius, width: radius,
padding: const EdgeInsets.all(strokeWidth / 2), height: radius,
child: CircularProgressIndicator( padding: const EdgeInsets.all(strokeWidth / 2),
color: progressColor.withOpacity(.1), child: CircularProgressIndicator(
strokeWidth: strokeWidth, color: progressColor.withOpacity(.1),
strokeWidth: strokeWidth,
),
), ),
),
CircularPercentIndicator( CircularPercentIndicator(
percent: percent, percent: percent,
lineWidth: strokeWidth, lineWidth: strokeWidth,
radius: radius, radius: radius,
backgroundColor: Colors.white24, backgroundColor: Colors.white24,
progressColor: progressColor, progressColor: progressColor,
animation: true, animation: animate,
center: Text(NumberFormat.percentPattern().format(percent)), center: Text(NumberFormat.percentPattern().format(percent)),
animateFromLastPercent: true, animateFromLastPercent: true,
), ),

View file

@ -297,8 +297,8 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
), ),
); );
final enabledAnimations = context.select<Settings, bool>((v) => v.accessibilityAnimations.enabled); final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
if (enabledAnimations && (widget.heroType == HeroType.always || widget.heroType == HeroType.onTap && _tapped)) { if (animate && (widget.heroType == HeroType.always || widget.heroType == HeroType.onTap && _tapped)) {
chip = Hero( chip = Hero(
tag: filter, tag: filter,
transitionOnUserGestures: true, transitionOnUserGestures: true,

View file

@ -25,7 +25,7 @@ class MapTheme extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ProxyProvider<Settings, MapThemeData>( return ProxyProvider<Settings, MapThemeData>(
update: (_, settings, __) { update: (context, settings, __) {
return MapThemeData( return MapThemeData(
interactive: interactive, interactive: interactive,
navigationButton: navigationButton, navigationButton: navigationButton,

View file

@ -18,7 +18,7 @@ class TileExtentControllerProvider extends StatelessWidget {
builder: (context, constraints) { builder: (context, constraints) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) => ProxyProvider0<TileExtentController>( builder: (context, constraints) => ProxyProvider0<TileExtentController>(
update: (_, __) => controller..setViewportSize(constraints.biggest), update: (context, __) => controller..setViewportSize(constraints.biggest),
child: child, child: child,
), ),
); );

View file

@ -173,9 +173,9 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
@override @override
Widget build(BuildContext context) { 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) { if (!entry.canDecode || _lastException != null) {
return _buildError(context, enabledAnimations); return _buildError(context, animate);
} }
// use `RawImage` instead of `Image`, using `ImageInfo` to check dimensions // 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( image = Hero(
tag: widget.heroTag!, tag: widget.heroTag!,
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {
@ -248,13 +248,13 @@ class _ThumbnailImageState extends State<ThumbnailImage> {
return image; return image;
} }
Widget _buildError(BuildContext context, bool enabledAnimations) { Widget _buildError(BuildContext context, bool animate) {
Widget child = ErrorThumbnail( Widget child = ErrorThumbnail(
entry: entry, entry: entry,
extent: extent, extent: extent,
); );
if (enabledAnimations && widget.heroTag != null) { if (animate && widget.heroTag != null) {
child = Hero( child = Hero(
tag: widget.heroTag!, tag: widget.heroTag!,
flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) {

View file

@ -59,7 +59,7 @@ class _FilterGridAppBarState<T extends CollectionFilter> extends State<FilterGri
void initState() { void initState() {
super.initState(); super.initState();
_browseToSelectAnimation = AnimationController( _browseToSelectAnimation = AnimationController(
duration: Durations.iconAnimation, duration: context.read<DurationsData>().iconAnimation,
vsync: this, vsync: this,
); );
_isSelectingNotifier.addListener(_onActivityChange); _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/location.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/tag.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_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
@ -22,6 +24,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:percent_indicator/linear_percent_indicator.dart'; import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:provider/provider.dart';
class StatsPage extends StatelessWidget { class StatsPage extends StatelessWidget {
static const routeName = '/collection/stats'; static const routeName = '/collection/stats';
@ -67,6 +70,7 @@ class StatsPage extends StatelessWidget {
text: context.l10n.collectionEmptyImages, text: context.l10n.collectionEmptyImages,
); );
} else { } 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 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 imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image')));
final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video'))); final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video')));
@ -74,8 +78,8 @@ class StatsPage extends StatelessWidget {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
_buildMimeDonut(context, (sum) => context.l10n.statsImage(sum), imagesByMimeTypes), _buildMimeDonut(context, (sum) => context.l10n.statsImage(sum), imagesByMimeTypes, animate),
_buildMimeDonut(context, (sum) => context.l10n.statsVideo(sum), videoByMimeTypes), _buildMimeDonut(context, (sum) => context.l10n.statsVideo(sum), videoByMimeTypes, animate),
], ],
); );
@ -94,7 +98,7 @@ class StatsPage extends StatelessWidget {
lineHeight: lineHeight, lineHeight: lineHeight,
backgroundColor: Colors.white24, backgroundColor: Colors.white24,
progressColor: Theme.of(context).colorScheme.secondary, progressColor: Theme.of(context).colorScheme.secondary,
animation: true, animation: animate,
leading: const Icon(AIcons.location), leading: const Icon(AIcons.location),
// right padding to match leading, so that inside label is aligned with outside label below // 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), 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(); if (byMimeTypes.isEmpty) return const SizedBox.shrink();
final sum = byMimeTypes.values.fold<int>(0, (prev, v) => prev + v); final sum = byMimeTypes.values.fold<int>(0, (prev, v) => prev + v);
@ -165,6 +174,7 @@ class StatsPage extends StatelessWidget {
children: [ children: [
charts.PieChart( charts.PieChart(
series, series,
animate: animate,
defaultRenderer: charts.ArcRendererConfig<String>( defaultRenderer: charts.ArcRendererConfig<String>(
arcWidth: 16, arcWidth: 16,
), ),

View file

@ -371,7 +371,7 @@ class _PlayTogglerState extends State<_PlayToggler> with SingleTickerProviderSta
void initState() { void initState() {
super.initState(); super.initState();
_playPauseAnimation = AnimationController( _playPauseAnimation = AnimationController(
duration: Durations.iconAnimation, duration: context.read<DurationsData>().iconAnimation,
vsync: this, vsync: this,
); );
_registerWidget(widget); _registerWidget(widget);

View file

@ -181,7 +181,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} else { } else {
await controller.play(); await controller.play();
// hide overlay // hide overlay
_overlayHidingTimer = Timer(Durations.iconAnimation + Durations.videoOverlayHideDelay, () { _overlayHidingTimer = Timer(context.read<DurationsData>().iconAnimation + Durations.videoOverlayHideDelay, () {
const ToggleOverlayNotification(visible: false).dispatch(context); 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); final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
if (enabledAnimations) { if (animate) {
child = Consumer<HeroInfo?>( child = Consumer<HeroInfo?>(
builder: (context, info, child) => Hero( builder: (context, info, child) => Hero(
tag: info != null && info.entry == mainEntry ? Object.hashAll([info.collectionId, mainEntry.uri]) : hashCode, tag: info != null && info.entry == mainEntry ? Object.hashAll([info.collectionId, mainEntry.uri]) : hashCode,