mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Support disabled animations (#20354)
This commit is contained in:
parent
1f31c3b354
commit
e235ccd76f
@ -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
|
||||
|
||||
49
packages/flutter/lib/src/semantics/binding.dart
Normal file
49
packages/flutter/lib/src/semantics/binding.dart
Normal file
@ -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;
|
||||
}
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user