From e235ccd76ff88063c9ca358ed12818f4907a8017 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Thu, 23 Aug 2018 11:50:04 -0700 Subject: [PATCH] Support disabled animations (#20354) --- .../material/progress_indicator_demo.dart | 1 + packages/flutter/lib/semantics.dart | 1 + .../src/animation/animation_controller.dart | 69 ++++++++++++++-- .../lib/src/material/refresh_indicator.dart | 1 - .../lib/src/material/reorderable_list.dart | 2 + .../flutter/lib/src/rendering/binding.dart | 10 +-- .../flutter/lib/src/scheduler/ticker.dart | 1 - .../flutter/lib/src/semantics/binding.dart | 49 ++++++++++++ packages/flutter/lib/src/widgets/binding.dart | 2 +- .../animation/animation_controller_test.dart | 80 +++++++++++++++++++ .../foundation/service_extensions_test.dart | 1 + .../test/rendering/rendering_tester.dart | 2 +- .../flutter/test/scheduler/ticker_test.dart | 32 ++++++++ .../lib/src/extension/extension.dart | 2 +- packages/flutter_test/lib/src/binding.dart | 4 + 15 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 packages/flutter/lib/src/semantics/binding.dart diff --git a/examples/flutter_gallery/lib/demo/material/progress_indicator_demo.dart b/examples/flutter_gallery/lib/demo/material/progress_indicator_demo.dart index 0a82920efe1..5a26eb8a15e 100644 --- a/examples/flutter_gallery/lib/demo/material/progress_indicator_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/progress_indicator_demo.dart @@ -21,6 +21,7 @@ class _ProgressIndicatorDemoState extends State with Sing _controller = new AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, + animationBehavior: AnimationBehavior.preserve, )..forward(); _animation = new CurvedAnimation( diff --git a/packages/flutter/lib/semantics.dart b/packages/flutter/lib/semantics.dart index 79db7ea62ff..b8b30d9da39 100644 --- a/packages/flutter/lib/semantics.dart +++ b/packages/flutter/lib/semantics.dart @@ -13,5 +13,6 @@ /// and is used by the platform-specific accessibility services. library semantics; +export 'src/semantics/binding.dart'; export 'src/semantics/semantics.dart'; export 'src/semantics/semantics_service.dart'; diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index 4ed83f34e05..f1ab71fb4ce 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -7,6 +7,7 @@ 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 'animation.dart'; @@ -38,6 +39,30 @@ const Tolerance _kFlingTolerance = Tolerance( distance: 0.01, ); +/// Configures how an [AnimationController] behaves when animations are disabled. +/// +/// 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 +/// behavior. +/// +/// For example, the [AnimationController] which controls the physics simulation +/// for a scrollable list will have [AnimationBehavior.preserve] so that when +/// a user attempts to scroll it does not jump to the end/beginning too quickly. +enum AnimationBehavior { + /// The [AnimationController] will reduce its duration when + /// [AccessibilityFeatures.disableAnimations] is true. + normal, + + /// The [AnimationController] will preserve its behavior. + /// + /// This is the default for repeating animations in order to prevent them from + /// flashing rapidly on the screen if the widget does not take the + /// [AccessibilityFeatures.disableAnimations] flag into account. + preserve, +} + /// A controller for an animation. /// /// This class lets you perform tasks such as: @@ -120,6 +145,7 @@ class AnimationController extends Animation this.debugLabel, this.lowerBound = 0.0, this.upperBound = 1.0, + this.animationBehavior = AnimationBehavior.normal, @required TickerProvider vsync, }) : assert(lowerBound != null), assert(upperBound != null), @@ -151,6 +177,7 @@ class AnimationController extends Animation this.duration, this.debugLabel, @required TickerProvider vsync, + this.animationBehavior = AnimationBehavior.preserve, }) : assert(value != null), assert(vsync != null), lowerBound = double.negativeInfinity, @@ -170,6 +197,14 @@ class AnimationController extends Animation /// identifying animation controller instances in debug output. final String debugLabel; + /// The behavior of the controller when [AccessibilityFeatures.disableAnimations] + /// is true. + /// + /// Defaults to [AnimationBehavior.normal] for the [new AnimationBehavior] + /// constructor, and [AnimationBehavior.preserve] for the + /// [new AnimationBehavior.unbounded] constructor. + final AnimationBehavior animationBehavior; + /// Returns an [Animation] for this animation controller, so that a /// pointer to this object can be passed around without allowing users of that /// pointer to mutate the [AnimationController] state. @@ -363,7 +398,18 @@ class AnimationController extends Animation return _animateToInternal(target, duration: duration, curve: curve); } - TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear }) { + 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) { + switch (behavior) { + case AnimationBehavior.normal: + scale = 0.05; + break; + case AnimationBehavior.preserve: + break; + } + } Duration simulationDuration = duration; if (simulationDuration == null) { assert(() { @@ -398,7 +444,7 @@ class AnimationController extends Animation } assert(simulationDuration > Duration.zero); assert(!isAnimating); - return _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve)); + return _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve, scale)); } /// Starts running this animation in the forward direction, and @@ -441,11 +487,22 @@ 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 }) { + 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; - final Simulation simulation = new SpringSimulation(_kFlingSpringDescription, value, target, velocity) + double scale = 1.0; + final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior; + if (SemanticsBinding.instance.disableAnimations) { + switch (behavior) { + case AnimationBehavior.normal: + scale = 200.0; + break; + case AnimationBehavior.preserve: + break; + } + } + final Simulation simulation = new SpringSimulation(_kFlingSpringDescription, value, target, velocity * scale) ..tolerance = _kFlingTolerance; return animateWith(simulation); } @@ -558,11 +615,11 @@ class AnimationController extends Animation } class _InterpolationSimulation extends Simulation { - _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve) + _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale) : assert(_begin != null), assert(_end != null), assert(duration != null && duration.inMicroseconds > 0), - _durationInSeconds = duration.inMicroseconds / Duration.microsecondsPerSecond; + _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond; final double _durationInSeconds; final double _begin; diff --git a/packages/flutter/lib/src/material/refresh_indicator.dart b/packages/flutter/lib/src/material/refresh_indicator.dart index e75c90e4436..eb3dbbf20d5 100644 --- a/packages/flutter/lib/src/material/refresh_indicator.dart +++ b/packages/flutter/lib/src/material/refresh_indicator.dart @@ -153,7 +153,6 @@ class RefreshIndicatorState extends State with TickerProviderS @override void initState() { super.initState(); - _positionController = new AnimationController(vsync: this); _positionFactor = new Tween( begin: 0.0, diff --git a/packages/flutter/lib/src/material/reorderable_list.dart b/packages/flutter/lib/src/material/reorderable_list.dart index f4e7ab06702..0f17f791972 100644 --- a/packages/flutter/lib/src/material/reorderable_list.dart +++ b/packages/flutter/lib/src/material/reorderable_list.dart @@ -154,6 +154,7 @@ class _ReorderableListContent extends StatefulWidget { } class _ReorderableListContentState extends State<_ReorderableListContent> with TickerProviderStateMixin { + // The extent along the [widget.scrollDirection] axis to allow a child to // drop into when the user reorders list children. // @@ -280,6 +281,7 @@ class _ReorderableListContentState extends State<_ReorderableListContent> with T viewport.getOffsetToReveal(contextObject, 1.0).offset + margin, ); final bool onScreen = scrollOffset <= topOffset && scrollOffset >= bottomOffset; + // If the context is off screen, then we request a scroll to make it visible. if (!onScreen) { _scrolling = true; diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 88b1c2e1b1e..a967bf52f3d 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -21,7 +21,7 @@ import 'view.dart'; export 'package:flutter/gestures.dart' show HitTestResult; /// The glue between the render tree and the Flutter engine. -abstract class RendererBinding extends BindingBase with ServicesBinding, SchedulerBinding, HitTestable { +abstract class RendererBinding extends BindingBase with ServicesBinding, SchedulerBinding, SemanticsBinding, HitTestable { // This class is intended to be used as a mixin, and should not be // extended directly. factory RendererBinding._() => null; @@ -153,12 +153,6 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul @protected void handleTextScaleFactorChanged() { } - /// Called when the platform accessibility features change. - /// - /// See [Window.onAccessibilityFeaturesChanged]. - @protected - void handleAccessibilityFeaturesChanged() {} - /// Returns a [ViewConfiguration] configured for the [RenderView] based on the /// current environment. /// @@ -341,7 +335,7 @@ void debugDumpSemanticsTree(DebugSemanticsDumpOrder childOrder) { /// that layer's binding. /// /// See also [BindingBase]. -class RenderingFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, RendererBinding { +class RenderingFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, SemanticsBinding, RendererBinding { /// Creates a binding for the rendering layer. /// /// The `root` render box is attached directly to the [renderView] and is diff --git a/packages/flutter/lib/src/scheduler/ticker.dart b/packages/flutter/lib/src/scheduler/ticker.dart index 928d22bbb5d..2e1840a33d2 100644 --- a/packages/flutter/lib/src/scheduler/ticker.dart +++ b/packages/flutter/lib/src/scheduler/ticker.dart @@ -225,7 +225,6 @@ class Ticker { _animationId = null; _startTime ??= timeStamp; - _onTick(timeStamp - _startTime); // The onTick callback may have scheduled another tick already, for diff --git a/packages/flutter/lib/src/semantics/binding.dart b/packages/flutter/lib/src/semantics/binding.dart new file mode 100644 index 00000000000..d86ffa368d9 --- /dev/null +++ b/packages/flutter/lib/src/semantics/binding.dart @@ -0,0 +1,49 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' as ui show AccessibilityFeatures, window; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + + +/// 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 { + // This class is intended to be used as a mixin, and should not be + // extended directly. + factory SemanticsBinding._() => null; + + /// The current [SemanticsBinding], if one has been created. + static SemanticsBinding get instance => _instance; + static SemanticsBinding _instance; + + @override + void initInstances() { + super.initInstances(); + _instance = this; + _accessibilityFeatures = ui.window.accessibilityFeatures; + } + + /// Called when the platform accessibility features change. + /// + /// See [Window.onAccessibilityFeaturesChanged]. + @protected + void handleAccessibilityFeaturesChanged() { + _accessibilityFeatures = ui.window.accessibilityFeatures; + } + + /// The currently active set of [AccessibilityFeatures]. + /// + /// This is initialized the first time [runApp] is called and updated whenever + /// a flag is changed. + /// + /// 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; +} \ No newline at end of file diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 46b5f044c0f..5140bfd030f 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -917,7 +917,7 @@ class RenderObjectToWidgetElement extends RootRenderObje /// A concrete binding for applications based on the Widgets framework. /// This is the glue that binds the framework to the Flutter engine. -class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, RendererBinding, WidgetsBinding { +class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { /// Returns an instance of the [WidgetsBinding], creating and /// initializing it if necessary. If one is created, it will be a diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart index fcb8ee6aaa1..3cc8283ba1a 100644 --- a/packages/flutter/test/animation/animation_controller_test.dart +++ b/packages/flutter/test/animation/animation_controller_test.dart @@ -578,4 +578,84 @@ void main() { expect(statusLog, equals([ AnimationStatus.forward, AnimationStatus.completed ])); statusLog.clear(); }); + + group('AnimationBehavior', () { + test('Default values for constructor', () { + final AnimationController controller = new AnimationController(vsync: const TestVSync()); + expect(controller.animationBehavior, AnimationBehavior.normal); + + final AnimationController repeating = new AnimationController.unbounded(vsync: const TestVSync()); + expect(repeating.animationBehavior, AnimationBehavior.preserve); + }); + + testWidgets('AnimationBehavior.preserve runs at normal speed when animatingTo', (WidgetTester tester) async { + tester.binding.disableAnimations = true; + final AnimationController controller = new AnimationController( + vsync: const TestVSync(), + animationBehavior: AnimationBehavior.preserve, + ); + + expect(controller.value, 0.0); + expect(controller.status, AnimationStatus.dismissed); + + controller.animateTo(1.0, duration: const Duration(milliseconds: 100)); + tick(const Duration(milliseconds: 0)); + tick(const Duration(milliseconds: 50)); + + expect(controller.value, 0.5); + expect(controller.status, AnimationStatus.forward); + + tick(const Duration(milliseconds: 0)); + tick(const Duration(milliseconds: 150)); + + 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; + final AnimationController controller = new AnimationController( + vsync: const TestVSync(), + animationBehavior: AnimationBehavior.normal, + ); + + expect(controller.value, 0.0); + expect(controller.status, AnimationStatus.dismissed); + + controller.animateTo(1.0, duration: const Duration(milliseconds: 100)); + tick(const Duration(milliseconds: 0)); + tick(const Duration(microseconds: 2500)); + + expect(controller.value, 0.5); + expect(controller.status, AnimationStatus.forward); + + tick(const Duration(milliseconds: 0)); + tick(const Duration(milliseconds: 5, microseconds: 1000)); + + 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; + final AnimationController controller = new AnimationController( + vsync: const TestVSync(), + ); + final AnimationController fastController = new AnimationController( + vsync: const TestVSync(), + ); + + controller.fling(velocity: 1.0, animationBehavior: AnimationBehavior.preserve); + fastController.fling(velocity: 1.0, animationBehavior: AnimationBehavior.normal); + tick(const Duration(milliseconds: 0)); + tick(const Duration(milliseconds: 50)); + + // 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/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index bc29aec9892..c68a2d50ee1 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -20,6 +20,7 @@ class TestServiceExtensionsBinding extends BindingBase GestureBinding, SchedulerBinding, PaintingBinding, + SemanticsBinding, RendererBinding, WidgetsBinding { diff --git a/packages/flutter/test/rendering/rendering_tester.dart b/packages/flutter/test/rendering/rendering_tester.dart index 5cb28a574fc..5c9d284cc3e 100644 --- a/packages/flutter/test/rendering/rendering_tester.dart +++ b/packages/flutter/test/rendering/rendering_tester.dart @@ -11,7 +11,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart' show EnginePhase; export 'package:flutter_test/flutter_test.dart' show EnginePhase; -class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, GestureBinding, SchedulerBinding, PaintingBinding, RendererBinding { +class TestRenderingFlutterBinding extends BindingBase with ServicesBinding, GestureBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding { EnginePhase phase = EnginePhase.composite; @override diff --git a/packages/flutter/test/scheduler/ticker_test.dart b/packages/flutter/test/scheduler/ticker_test.dart index 9fd788c4375..272753efc55 100644 --- a/packages/flutter/test/scheduler/ticker_test.dart +++ b/packages/flutter/test/scheduler/ticker_test.dart @@ -83,6 +83,38 @@ void main() { expect(ticker.toString(debugIncludeStack: true), contains('testFunction')); }); + testWidgets('Ticker can be sped up with time dilation', (WidgetTester tester) async { + timeDilation = 0.5; // Move twice as fast. + Duration lastDuration; + void handleTick(Duration duration) { + lastDuration = duration; + } + + final Ticker ticker = new Ticker(handleTick); + ticker.start(); + await tester.pump(const Duration(milliseconds: 10)); + await tester.pump(const Duration(milliseconds: 10)); + expect(lastDuration, const Duration(milliseconds: 20)); + + ticker.dispose(); + }); + + testWidgets('Ticker can be slowed down with time dilation', (WidgetTester tester) async { + timeDilation = 2.0; // Move half as fast. + Duration lastDuration; + void handleTick(Duration duration) { + lastDuration = duration; + } + + final Ticker ticker = new Ticker(handleTick); + ticker.start(); + await tester.pump(const Duration(milliseconds: 10)); + await tester.pump(const Duration(milliseconds: 10)); + expect(lastDuration, const Duration(milliseconds: 5)); + + ticker.dispose(); + }); + testWidgets('Ticker stops ticking when application is paused', (WidgetTester tester) async { int tickCount = 0; void handleTick(Duration duration) { diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index 2043ed5bd71..9bf524431a3 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -36,7 +36,7 @@ const String _extensionMethod = 'ext.flutter.$_extensionMethodName'; /// eventually completes to a string response. typedef Future DataHandler(String message); -class _DriverBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, PaintingBinding, RendererBinding, WidgetsBinding { +class _DriverBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { _DriverBinding(this._handler, this._silenceErrors); final DataHandler _handler; diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index c65443883c0..0d647e0b9be 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -86,6 +86,7 @@ const Size _kDefaultTestViewportSize = Size(800.0, 600.0); abstract class TestWidgetsFlutterBinding extends BindingBase with SchedulerBinding, GestureBinding, + SemanticsBinding, RendererBinding, ServicesBinding, PaintingBinding, @@ -127,6 +128,9 @@ 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.