Unverified Commit 0e9cfe7d authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Address follow up to https://github.com/flutter/flutter/pull/20354 (#21181)

parent 44cdb049
...@@ -7,8 +7,8 @@ import 'dart:ui' as ui show lerpDouble; ...@@ -7,8 +7,8 @@ import 'dart:ui' as ui show lerpDouble;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart'; import 'package:flutter/physics.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart';
import 'animation.dart'; import 'animation.dart';
import 'curves.dart'; import 'curves.dart';
...@@ -44,7 +44,7 @@ const Tolerance _kFlingTolerance = Tolerance( ...@@ -44,7 +44,7 @@ const Tolerance _kFlingTolerance = Tolerance(
/// When [AccessibilityFeatures.disableAnimations] is true, the device is asking /// When [AccessibilityFeatures.disableAnimations] is true, the device is asking
/// flutter to reduce or disable animations as much as possible. To honor this, /// 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. /// 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. /// behavior.
/// ///
/// For example, the [AnimationController] which controls the physics simulation /// For example, the [AnimationController] which controls the physics simulation
...@@ -200,9 +200,9 @@ class AnimationController extends Animation<double> ...@@ -200,9 +200,9 @@ class AnimationController extends Animation<double>
/// The behavior of the controller when [AccessibilityFeatures.disableAnimations] /// The behavior of the controller when [AccessibilityFeatures.disableAnimations]
/// is true. /// is true.
/// ///
/// Defaults to [AnimationBehavior.normal] for the [new AnimationBehavior] /// Defaults to [AnimationBehavior.normal] for the [new AnimationController]
/// constructor, and [AnimationBehavior.preserve] for the /// constructor, and [AnimationBehavior.preserve] for the
/// [new AnimationBehavior.unbounded] constructor. /// [new AnimationController.unbounded] constructor.
final AnimationBehavior animationBehavior; final AnimationBehavior animationBehavior;
/// Returns an [Animation<double>] for this animation controller, so that a /// Returns an [Animation<double>] for this animation controller, so that a
...@@ -401,9 +401,12 @@ class AnimationController extends Animation<double> ...@@ -401,9 +401,12 @@ class AnimationController extends Animation<double>
TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear, AnimationBehavior animationBehavior }) { TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear, AnimationBehavior animationBehavior }) {
final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior; final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior;
double scale = 1.0; double scale = 1.0;
if (SemanticsBinding.instance.disableAnimations) { if (_ticker.disableAnimations) {
switch (behavior) { switch (behavior) {
case AnimationBehavior.normal: 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; scale = 0.05;
break; break;
case AnimationBehavior.preserve: case AnimationBehavior.preserve:
...@@ -487,15 +490,17 @@ class AnimationController extends Animation<double> ...@@ -487,15 +490,17 @@ class AnimationController extends Animation<double>
/// The most recently returned [TickerFuture], if any, is marked as having been /// The most recently returned [TickerFuture], if any, is marked as having been
/// canceled, meaning the future never completes and its [TickerFuture.orCancel] /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
/// derivative future completes with a [TickerCanceled] error. /// 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; _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward;
final double target = velocity < 0.0 ? lowerBound - _kFlingTolerance.distance final double target = velocity < 0.0 ? lowerBound - _kFlingTolerance.distance
: upperBound + _kFlingTolerance.distance; : upperBound + _kFlingTolerance.distance;
double scale = 1.0; double scale = 1.0;
final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior; final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior;
if (SemanticsBinding.instance.disableAnimations) { if (_ticker.disableAnimations) {
switch (behavior) { switch (behavior) {
case AnimationBehavior.normal: 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; scale = 200.0;
break; break;
case AnimationBehavior.preserve: case AnimationBehavior.preserve:
......
...@@ -68,6 +68,12 @@ class Ticker { ...@@ -68,6 +68,12 @@ class Ticker {
TickerFuture _future; 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. /// Whether this ticker has been silenced.
/// ///
/// While silenced, a ticker's clock can still run, but the callback will not /// While silenced, a ticker's clock can still run, but the callback will not
...@@ -273,6 +279,7 @@ class Ticker { ...@@ -273,6 +279,7 @@ class Ticker {
assert(_startTime == null); assert(_startTime == null);
assert(_animationId == null); assert(_animationId == null);
assert((originalTicker._future == null) == (originalTicker._startTime == null), 'Cannot absorb Ticker after it has been disposed.'); assert((originalTicker._future == null) == (originalTicker._startTime == null), 'Cannot absorb Ticker after it has been disposed.');
disableAnimations = originalTicker.disableAnimations;
if (originalTicker._future != null) { if (originalTicker._future != null) {
_future = originalTicker._future; _future = originalTicker._future;
_startTime = originalTicker._startTime; _startTime = originalTicker._startTime;
......
...@@ -5,12 +5,12 @@ ...@@ -5,12 +5,12 @@
import 'dart:ui' as ui show AccessibilityFeatures, window; import 'dart:ui' as ui show AccessibilityFeatures, window;
import 'package:flutter/foundation.dart'; 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. /// The glue between the semantics layer and the Flutter engine.
// TODO(jonahwilliams): move the remaining semantic related bindings here. // 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 // This class is intended to be used as a mixin, and should not be
// extended directly. // extended directly.
factory SemanticsBinding._() => null; factory SemanticsBinding._() => null;
...@@ -23,7 +23,7 @@ class SemanticsBinding extends BindingBase with ServicesBinding { ...@@ -23,7 +23,7 @@ class SemanticsBinding extends BindingBase with ServicesBinding {
void initInstances() { void initInstances() {
super.initInstances(); super.initInstances();
_instance = this; _instance = this;
_accessibilityFeatures = ui.window.accessibilityFeatures; _accessibilityFeatures = new ValueNotifier<ui.AccessibilityFeatures>(ui.window.accessibilityFeatures);
} }
/// Called when the platform accessibility features change. /// Called when the platform accessibility features change.
...@@ -31,7 +31,7 @@ class SemanticsBinding extends BindingBase with ServicesBinding { ...@@ -31,7 +31,7 @@ class SemanticsBinding extends BindingBase with ServicesBinding {
/// See [Window.onAccessibilityFeaturesChanged]. /// See [Window.onAccessibilityFeaturesChanged].
@protected @protected
void handleAccessibilityFeaturesChanged() { void handleAccessibilityFeaturesChanged() {
_accessibilityFeatures = ui.window.accessibilityFeatures; _accessibilityFeatures.value = ui.window.accessibilityFeatures;
} }
/// The currently active set of [AccessibilityFeatures]. /// The currently active set of [AccessibilityFeatures].
...@@ -41,9 +41,6 @@ class SemanticsBinding extends BindingBase with ServicesBinding { ...@@ -41,9 +41,6 @@ class SemanticsBinding extends BindingBase with ServicesBinding {
/// ///
/// To listen to changes to accessibility features, create a /// To listen to changes to accessibility features, create a
/// [WidgetsBindingObserver] and listen to [didChangeAccessibilityFeatures]. /// [WidgetsBindingObserver] and listen to [didChangeAccessibilityFeatures].
ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; ValueListenable<ui.AccessibilityFeatures> get accessibilityFeatures => _accessibilityFeatures;
ui.AccessibilityFeatures _accessibilityFeatures; ValueNotifier<ui.AccessibilityFeatures> _accessibilityFeatures;
/// Whether the device is requesting that animations be disabled.
bool get disableAnimations => accessibilityFeatures.disableAnimations;
} }
\ No newline at end of file
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/semantics.dart';
import 'framework.dart'; import 'framework.dart';
...@@ -94,7 +95,10 @@ abstract class SingleTickerProviderStateMixin<T extends StatefulWidget> extends ...@@ -94,7 +95,10 @@ abstract class SingleTickerProviderStateMixin<T extends StatefulWidget> extends
'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.' 'mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.'
); );
}()); }());
_ticker = new Ticker(onTick, debugLabel: 'created by $this'); final ValueListenable<AccessibilityFeatures> 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 // 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 // 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 // can't actually check that here because if we're in initState then we're
...@@ -117,6 +121,8 @@ abstract class SingleTickerProviderStateMixin<T extends StatefulWidget> extends ...@@ -117,6 +121,8 @@ abstract class SingleTickerProviderStateMixin<T extends StatefulWidget> extends
'The offending ticker was: ${_ticker.toString(debugIncludeStack: true)}' 'The offending ticker was: ${_ticker.toString(debugIncludeStack: true)}'
); );
}()); }());
final ValueListenable<AccessibilityFeatures> accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures;
accessibilityFeatures.removeListener(_handleAccessibilityFeaturesChanged);
super.dispose(); super.dispose();
} }
...@@ -144,6 +150,12 @@ abstract class SingleTickerProviderStateMixin<T extends StatefulWidget> extends ...@@ -144,6 +150,12 @@ abstract class SingleTickerProviderStateMixin<T extends StatefulWidget> extends
properties.add(new DiagnosticsProperty<Ticker>('ticker', _ticker, description: tickerDescription, showSeparator: false, defaultValue: null)); properties.add(new DiagnosticsProperty<Ticker>('ticker', _ticker, description: tickerDescription, showSeparator: false, defaultValue: null));
} }
void _handleAccessibilityFeaturesChanged() {
final ValueListenable<AccessibilityFeatures> accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures;
if (_ticker != null) {
_ticker.disableAnimations = accessibilityFeatures.value.disableAnimations;
}
}
} }
/// Provides [Ticker] objects that are configured to only tick while the current /// Provides [Ticker] objects that are configured to only tick while the current
...@@ -167,8 +179,11 @@ abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State< ...@@ -167,8 +179,11 @@ abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State<
@override @override
Ticker createTicker(TickerCallback onTick) { Ticker createTicker(TickerCallback onTick) {
_tickers ??= new Set<_WidgetTicker>(); _tickers ??= new Set<_WidgetTicker>();
final _WidgetTicker result = new _WidgetTicker(onTick, this, debugLabel: 'created by $this'); final ValueListenable<AccessibilityFeatures> accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures;
final _WidgetTicker result = new _WidgetTicker(onTick, this, debugLabel: 'created by $this')
..disableAnimations = accessibilityFeatures.value.disableAnimations;
_tickers.add(result); _tickers.add(result);
accessibilityFeatures.addListener(_handleAccessibilityFeaturesChanged);
return result; return result;
} }
...@@ -198,6 +213,8 @@ abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State< ...@@ -198,6 +213,8 @@ abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State<
} }
return true; return true;
}()); }());
final ValueListenable<AccessibilityFeatures> accessibilityFeatures = SemanticsBinding.instance.accessibilityFeatures;
accessibilityFeatures.removeListener(_handleAccessibilityFeaturesChanged);
super.dispose(); super.dispose();
} }
...@@ -205,8 +222,9 @@ abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State< ...@@ -205,8 +222,9 @@ abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State<
void didChangeDependencies() { void didChangeDependencies() {
final bool muted = !TickerMode.of(context); final bool muted = !TickerMode.of(context);
if (_tickers != null) { if (_tickers != null) {
for (Ticker ticker in _tickers) for (Ticker ticker in _tickers) {
ticker.muted = muted; ticker.muted = muted;
}
} }
super.didChangeDependencies(); super.didChangeDependencies();
} }
...@@ -224,6 +242,14 @@ abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State< ...@@ -224,6 +242,14 @@ abstract class TickerProviderStateMixin<T extends StatefulWidget> extends State<
)); ));
} }
void _handleAccessibilityFeaturesChanged() {
final ValueListenable<AccessibilityFeatures> 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 // This class should really be called _DisposingTicker or some such, but this
......
...@@ -581,17 +581,16 @@ void main() { ...@@ -581,17 +581,16 @@ void main() {
group('AnimationBehavior', () { group('AnimationBehavior', () {
test('Default values for constructor', () { 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); 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); expect(repeating.animationBehavior, AnimationBehavior.preserve);
}); });
testWidgets('AnimationBehavior.preserve runs at normal speed when animatingTo', (WidgetTester tester) async { test('AnimationBehavior.preserve runs at normal speed when animatingTo', () async {
tester.binding.disableAnimations = true;
final AnimationController controller = new AnimationController( final AnimationController controller = new AnimationController(
vsync: const TestVSync(), vsync: const TestVSync(disableAnimations: true),
animationBehavior: AnimationBehavior.preserve, animationBehavior: AnimationBehavior.preserve,
); );
...@@ -610,13 +609,11 @@ void main() { ...@@ -610,13 +609,11 @@ void main() {
expect(controller.value, 1.0); expect(controller.value, 1.0);
expect(controller.status, AnimationStatus.completed); expect(controller.status, AnimationStatus.completed);
tester.binding.disableAnimations = false;
}); });
testWidgets('AnimationBehavior.normal runs at 20x speed when animatingTo', (WidgetTester tester) async { test('AnimationBehavior.normal runs at 20x speed when animatingTo', () async {
tester.binding.disableAnimations = true;
final AnimationController controller = new AnimationController( final AnimationController controller = new AnimationController(
vsync: const TestVSync(), vsync: const TestVSync(disableAnimations: true),
animationBehavior: AnimationBehavior.normal, animationBehavior: AnimationBehavior.normal,
); );
...@@ -635,17 +632,14 @@ void main() { ...@@ -635,17 +632,14 @@ void main() {
expect(controller.value, 1.0); expect(controller.value, 1.0);
expect(controller.status, AnimationStatus.completed); expect(controller.status, AnimationStatus.completed);
tester.binding.disableAnimations = false;
}); });
testWidgets('AnimationBehavior.normal runs "faster" whan AnimationBehavior.preserve', (WidgetTester tester) async { test('AnimationBehavior.normal runs "faster" whan AnimationBehavior.preserve', () {
tester.binding.disableAnimations = true;
final AnimationController controller = new AnimationController( final AnimationController controller = new AnimationController(
vsync: const TestVSync(), vsync: const TestVSync(disableAnimations: true),
); );
final AnimationController fastController = new AnimationController( final AnimationController fastController = new AnimationController(
vsync: const TestVSync(), vsync: const TestVSync(disableAnimations: true),
); );
controller.fling(velocity: 1.0, animationBehavior: AnimationBehavior.preserve); controller.fling(velocity: 1.0, animationBehavior: AnimationBehavior.preserve);
...@@ -655,7 +649,6 @@ void main() { ...@@ -655,7 +649,6 @@ void main() {
// We don't assert a specific faction that normal animation. // We don't assert a specific faction that normal animation.
expect(controller.value < fastController.value, true); expect(controller.value < fastController.value, true);
tester.binding.disableAnimations = false;
}); });
}); });
} }
...@@ -246,7 +246,7 @@ void main() { ...@@ -246,7 +246,7 @@ void main() {
class _FakeTickerProvider implements TickerProvider { class _FakeTickerProvider implements TickerProvider {
@override @override
Ticker createTicker(TickerCallback onTick) { Ticker createTicker(TickerCallback onTick, [bool disableAnimations = false]) {
return new _FakeTicker(); return new _FakeTicker();
} }
} }
...@@ -273,6 +273,9 @@ class _FakeTicker implements Ticker { ...@@ -273,6 +273,9 @@ class _FakeTicker implements Ticker {
@override @override
bool get shouldScheduleTick => null; bool get shouldScheduleTick => null;
@override
bool disableAnimations = false;
@override @override
void dispose() {} void dispose() {}
......
...@@ -128,9 +128,6 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -128,9 +128,6 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
@protected @protected
bool get checkIntrinsicSizes => false; bool get checkIntrinsicSizes => false;
@override
bool disableAnimations = false;
/// Creates and initializes the binding. This function is /// Creates and initializes the binding. This function is
/// idempotent; calling it a second time will just return the /// idempotent; calling it a second time will just return the
/// previously-created instance. /// previously-created instance.
......
...@@ -10,8 +10,16 @@ import 'package:flutter/scheduler.dart'; ...@@ -10,8 +10,16 @@ import 'package:flutter/scheduler.dart';
/// tree. /// tree.
class TestVSync implements TickerProvider { class TestVSync implements TickerProvider {
/// Creates a ticker provider that creates standalone tickers. /// 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 @override
Ticker createTicker(TickerCallback onTick) => new Ticker(onTick); Ticker createTicker(TickerCallback onTick) => new Ticker(onTick)..disableAnimations = disableAnimations;
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment