Unverified Commit e235ccd7 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Support disabled animations (#20354)

parent 1f31c3b3
......@@ -21,6 +21,7 @@ class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo> with Sing
_controller = new AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
animationBehavior: AnimationBehavior.preserve,
)..forward();
_animation = new CurvedAnimation(
......
......@@ -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';
......@@ -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<double>
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<double>
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<double>
/// 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<double>] 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<double>
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<double>
}
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<double>
/// 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<double>
}
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;
......
......@@ -153,7 +153,6 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
@override
void initState() {
super.initState();
_positionController = new AnimationController(vsync: this);
_positionFactor = new Tween<double>(
begin: 0.0,
......
......@@ -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;
......
......@@ -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
......
......@@ -225,7 +225,6 @@ class Ticker {
_animationId = null;
_startTime ??= timeStamp;
_onTick(timeStamp - _startTime);
// The onTick callback may have scheduled another tick already, for
......
// 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
......@@ -917,7 +917,7 @@ class RenderObjectToWidgetElement<T extends RenderObject> 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
......
......@@ -578,4 +578,84 @@ void main() {
expect(statusLog, equals(<AnimationStatus>[ 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;
});
});
}
......@@ -20,6 +20,7 @@ class TestServiceExtensionsBinding extends BindingBase
GestureBinding,
SchedulerBinding,
PaintingBinding,
SemanticsBinding,
RendererBinding,
WidgetsBinding {
......
......@@ -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
......
......@@ -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) {
......
......@@ -36,7 +36,7 @@ const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
/// eventually completes to a string response.
typedef Future<String> 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;
......
......@@ -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.
......
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