diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index f1ab71fb4ce..a60c53ccce7 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -7,8 +7,8 @@ import 'dart:ui' as ui show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/physics.dart'; -import 'package:flutter/semantics.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/semantics.dart'; import 'animation.dart'; import 'curves.dart'; @@ -44,7 +44,7 @@ const Tolerance _kFlingTolerance = Tolerance( /// When [AccessibilityFeatures.disableAnimations] is true, the device is asking /// flutter to reduce or disable animations as much as possible. To honor this, /// we reduce the duration and the corresponding number of frames for animations. -/// This enum is used to allow certain [AnimationControllers] to opt out of this +/// This enum is used to allow certain [AnimationController]s to opt out of this /// behavior. /// /// For example, the [AnimationController] which controls the physics simulation @@ -200,9 +200,9 @@ class AnimationController extends Animation /// The behavior of the controller when [AccessibilityFeatures.disableAnimations] /// is true. /// - /// Defaults to [AnimationBehavior.normal] for the [new AnimationBehavior] + /// Defaults to [AnimationBehavior.normal] for the [new AnimationController] /// constructor, and [AnimationBehavior.preserve] for the - /// [new AnimationBehavior.unbounded] constructor. + /// [new AnimationController.unbounded] constructor. final AnimationBehavior animationBehavior; /// Returns an [Animation] for this animation controller, so that a @@ -401,9 +401,12 @@ class AnimationController extends Animation TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear, AnimationBehavior animationBehavior }) { final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior; double scale = 1.0; - if (SemanticsBinding.instance.disableAnimations) { + if (_ticker.disableAnimations) { switch (behavior) { case AnimationBehavior.normal: + // Since the framework cannot handle zero duration animations, we run it at 5% of the normal + // duration to limit most animations to a single frame. + // TODO(jonahwilliams): determine a better process for setting duration. scale = 0.05; break; case AnimationBehavior.preserve: @@ -487,15 +490,17 @@ class AnimationController extends Animation /// The most recently returned [TickerFuture], if any, is marked as having been /// canceled, meaning the future never completes and its [TickerFuture.orCancel] /// derivative future completes with a [TickerCanceled] error. - TickerFuture fling({ double velocity = 1.0, AnimationBehavior animationBehavior}) { + TickerFuture fling({ double velocity = 1.0, AnimationBehavior animationBehavior }) { _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward; final double target = velocity < 0.0 ? lowerBound - _kFlingTolerance.distance : upperBound + _kFlingTolerance.distance; double scale = 1.0; final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior; - if (SemanticsBinding.instance.disableAnimations) { + if (_ticker.disableAnimations) { switch (behavior) { case AnimationBehavior.normal: + // TODO(jonahwilliams): determine a better process for setting velocity. + // the value below was arbitrarily chosen because it worked for the drawer widget. scale = 200.0; break; case AnimationBehavior.preserve: diff --git a/packages/flutter/lib/src/scheduler/ticker.dart b/packages/flutter/lib/src/scheduler/ticker.dart index 2e1840a33d2..9cda21e0007 100644 --- a/packages/flutter/lib/src/scheduler/ticker.dart +++ b/packages/flutter/lib/src/scheduler/ticker.dart @@ -68,6 +68,12 @@ class Ticker { TickerFuture _future; + /// Whether or not the platform is requesting that animations be disabled. + /// + /// See also: + /// * [AccessibilityFeatures.disableAnimations], for the setting this value comes from. + bool disableAnimations = false; + /// Whether this ticker has been silenced. /// /// While silenced, a ticker's clock can still run, but the callback will not @@ -273,6 +279,7 @@ class Ticker { assert(_startTime == null); assert(_animationId == null); assert((originalTicker._future == null) == (originalTicker._startTime == null), 'Cannot absorb Ticker after it has been disposed.'); + disableAnimations = originalTicker.disableAnimations; if (originalTicker._future != null) { _future = originalTicker._future; _startTime = originalTicker._startTime; diff --git a/packages/flutter/lib/src/semantics/binding.dart b/packages/flutter/lib/src/semantics/binding.dart index d86ffa368d9..b5219624eba 100644 --- a/packages/flutter/lib/src/semantics/binding.dart +++ b/packages/flutter/lib/src/semantics/binding.dart @@ -5,12 +5,12 @@ import 'dart:ui' as ui show AccessibilityFeatures, window; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; +export 'dart:ui' show AccessibilityFeatures; /// The glue between the semantics layer and the Flutter engine. // TODO(jonahwilliams): move the remaining semantic related bindings here. -class SemanticsBinding extends BindingBase with ServicesBinding { +class SemanticsBinding extends BindingBase { // This class is intended to be used as a mixin, and should not be // extended directly. factory SemanticsBinding._() => null; @@ -23,7 +23,7 @@ class SemanticsBinding extends BindingBase with ServicesBinding { void initInstances() { super.initInstances(); _instance = this; - _accessibilityFeatures = ui.window.accessibilityFeatures; + _accessibilityFeatures = new ValueNotifier(ui.window.accessibilityFeatures); } /// Called when the platform accessibility features change. @@ -31,7 +31,7 @@ class SemanticsBinding extends BindingBase with ServicesBinding { /// See [Window.onAccessibilityFeaturesChanged]. @protected void handleAccessibilityFeaturesChanged() { - _accessibilityFeatures = ui.window.accessibilityFeatures; + _accessibilityFeatures.value = ui.window.accessibilityFeatures; } /// The currently active set of [AccessibilityFeatures]. @@ -41,9 +41,6 @@ class SemanticsBinding extends BindingBase with ServicesBinding { /// /// To listen to changes to accessibility features, create a /// [WidgetsBindingObserver] and listen to [didChangeAccessibilityFeatures]. - ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; - ui.AccessibilityFeatures _accessibilityFeatures; - - /// Whether the device is requesting that animations be disabled. - bool get disableAnimations => accessibilityFeatures.disableAnimations; + ValueListenable get accessibilityFeatures => _accessibilityFeatures; + ValueNotifier _accessibilityFeatures; } \ No newline at end of file diff --git a/packages/flutter/lib/src/widgets/ticker_provider.dart b/packages/flutter/lib/src/widgets/ticker_provider.dart index f788cdc8b94..4bfb0b776e4 100644 --- a/packages/flutter/lib/src/widgets/ticker_provider.dart +++ b/packages/flutter/lib/src/widgets/ticker_provider.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/semantics.dart'; import 'framework.dart'; @@ -94,7 +95,10 @@ abstract class SingleTickerProviderStateMixin extends 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.' ); }()); - _ticker = new Ticker(onTick, debugLabel: 'created by $this'); + final ValueListenable accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures; + _ticker = new Ticker(onTick, debugLabel: 'created by $this') + ..disableAnimations = accessibilityFeatures.value.disableAnimations; + accessibilityFeatures.addListener(_handleAccessibilityFeaturesChanged); // We assume that this is called from initState, build, or some sort of // event handler, and that thus TickerMode.of(context) would return true. We // can't actually check that here because if we're in initState then we're @@ -117,6 +121,8 @@ abstract class SingleTickerProviderStateMixin extends 'The offending ticker was: ${_ticker.toString(debugIncludeStack: true)}' ); }()); + final ValueListenable accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures; + accessibilityFeatures.removeListener(_handleAccessibilityFeaturesChanged); super.dispose(); } @@ -144,6 +150,12 @@ abstract class SingleTickerProviderStateMixin extends properties.add(new DiagnosticsProperty('ticker', _ticker, description: tickerDescription, showSeparator: false, defaultValue: null)); } + void _handleAccessibilityFeaturesChanged() { + final ValueListenable accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures; + if (_ticker != null) { + _ticker.disableAnimations = accessibilityFeatures.value.disableAnimations; + } + } } /// Provides [Ticker] objects that are configured to only tick while the current @@ -167,8 +179,11 @@ abstract class TickerProviderStateMixin extends State< @override Ticker createTicker(TickerCallback onTick) { _tickers ??= new Set<_WidgetTicker>(); - final _WidgetTicker result = new _WidgetTicker(onTick, this, debugLabel: 'created by $this'); + final ValueListenable accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures; + final _WidgetTicker result = new _WidgetTicker(onTick, this, debugLabel: 'created by $this') + ..disableAnimations = accessibilityFeatures.value.disableAnimations; _tickers.add(result); + accessibilityFeatures.addListener(_handleAccessibilityFeaturesChanged); return result; } @@ -198,6 +213,8 @@ abstract class TickerProviderStateMixin extends State< } return true; }()); + final ValueListenable accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures; + accessibilityFeatures.removeListener(_handleAccessibilityFeaturesChanged); super.dispose(); } @@ -205,8 +222,9 @@ abstract class TickerProviderStateMixin extends State< void didChangeDependencies() { final bool muted = !TickerMode.of(context); if (_tickers != null) { - for (Ticker ticker in _tickers) + for (Ticker ticker in _tickers) { ticker.muted = muted; + } } super.didChangeDependencies(); } @@ -224,6 +242,14 @@ abstract class TickerProviderStateMixin extends State< )); } + void _handleAccessibilityFeaturesChanged() { + final ValueListenable accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures; + if (_tickers != null) { + for (Ticker ticker in _tickers) { + ticker.disableAnimations = accessibilityFeatures.value.disableAnimations; + } + } + } } // This class should really be called _DisposingTicker or some such, but this diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart index 3cc8283ba1a..7e56b564495 100644 --- a/packages/flutter/test/animation/animation_controller_test.dart +++ b/packages/flutter/test/animation/animation_controller_test.dart @@ -581,17 +581,16 @@ void main() { group('AnimationBehavior', () { test('Default values for constructor', () { - final AnimationController controller = new AnimationController(vsync: const TestVSync()); + final AnimationController controller = new AnimationController(vsync: const TestVSync(disableAnimations: true)); expect(controller.animationBehavior, AnimationBehavior.normal); - final AnimationController repeating = new AnimationController.unbounded(vsync: const TestVSync()); + final AnimationController repeating = new AnimationController.unbounded(vsync: const TestVSync(disableAnimations: true)); expect(repeating.animationBehavior, AnimationBehavior.preserve); }); - testWidgets('AnimationBehavior.preserve runs at normal speed when animatingTo', (WidgetTester tester) async { - tester.binding.disableAnimations = true; + test('AnimationBehavior.preserve runs at normal speed when animatingTo', () async { final AnimationController controller = new AnimationController( - vsync: const TestVSync(), + vsync: const TestVSync(disableAnimations: true), animationBehavior: AnimationBehavior.preserve, ); @@ -610,13 +609,11 @@ void main() { expect(controller.value, 1.0); expect(controller.status, AnimationStatus.completed); - tester.binding.disableAnimations = false; }); - testWidgets('AnimationBehavior.normal runs at 20x speed when animatingTo', (WidgetTester tester) async { - tester.binding.disableAnimations = true; + test('AnimationBehavior.normal runs at 20x speed when animatingTo', () async { final AnimationController controller = new AnimationController( - vsync: const TestVSync(), + vsync: const TestVSync(disableAnimations: true), animationBehavior: AnimationBehavior.normal, ); @@ -635,17 +632,14 @@ void main() { expect(controller.value, 1.0); expect(controller.status, AnimationStatus.completed); - - tester.binding.disableAnimations = false; }); - testWidgets('AnimationBehavior.normal runs "faster" whan AnimationBehavior.preserve', (WidgetTester tester) async { - tester.binding.disableAnimations = true; + test('AnimationBehavior.normal runs "faster" whan AnimationBehavior.preserve', () { final AnimationController controller = new AnimationController( - vsync: const TestVSync(), + vsync: const TestVSync(disableAnimations: true), ); final AnimationController fastController = new AnimationController( - vsync: const TestVSync(), + vsync: const TestVSync(disableAnimations: true), ); controller.fling(velocity: 1.0, animationBehavior: AnimationBehavior.preserve); @@ -655,7 +649,6 @@ void main() { // We don't assert a specific faction that normal animation. expect(controller.value < fastController.value, true); - tester.binding.disableAnimations = false; }); }); } diff --git a/packages/flutter/test/rendering/proxy_box_test.dart b/packages/flutter/test/rendering/proxy_box_test.dart index 4985f24fded..f036704de8f 100644 --- a/packages/flutter/test/rendering/proxy_box_test.dart +++ b/packages/flutter/test/rendering/proxy_box_test.dart @@ -246,7 +246,7 @@ void main() { class _FakeTickerProvider implements TickerProvider { @override - Ticker createTicker(TickerCallback onTick) { + Ticker createTicker(TickerCallback onTick, [bool disableAnimations = false]) { return new _FakeTicker(); } } @@ -273,6 +273,9 @@ class _FakeTicker implements Ticker { @override bool get shouldScheduleTick => null; + @override + bool disableAnimations = false; + @override void dispose() {} diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 0d647e0b9be..cc6cbc25b88 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -128,9 +128,6 @@ abstract class TestWidgetsFlutterBinding extends BindingBase @protected bool get checkIntrinsicSizes => false; - @override - bool disableAnimations = false; - /// Creates and initializes the binding. This function is /// idempotent; calling it a second time will just return the /// previously-created instance. diff --git a/packages/flutter_test/lib/src/test_vsync.dart b/packages/flutter_test/lib/src/test_vsync.dart index 80d88fe96ae..83bbfc0b75f 100644 --- a/packages/flutter_test/lib/src/test_vsync.dart +++ b/packages/flutter_test/lib/src/test_vsync.dart @@ -10,8 +10,16 @@ import 'package:flutter/scheduler.dart'; /// tree. class TestVSync implements TickerProvider { /// Creates a ticker provider that creates standalone tickers. - const TestVSync(); + const TestVSync({this.disableAnimations = false}); + + /// Whether to disable the animations of tickers create from this picker. + /// + /// See also: + /// + /// * [AccessibilityFeatures.disableAnimations], for the setting that controls this flag. + /// * [AnimationBehavior], for how animation controllers change when created from tickers with this flag. + final bool disableAnimations; @override - Ticker createTicker(TickerCallback onTick) => new Ticker(onTick); + Ticker createTicker(TickerCallback onTick) => new Ticker(onTick)..disableAnimations = disableAnimations; }