-
Notifications
You must be signed in to change notification settings - Fork 28.6k
Support disabled animations #20354
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support disabled animations #20354
Conversation
cc @tvolkert will this break tests? Not this change specifically, but exposing this behavior |
I don't think exposing this will intrinsically break any tests, but I defer to @Hixie on the review. |
WIP for tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This all looks good but it seems to be missing tests for everything that was added.
TimeDilationBehavior get timeDilationBehavior => TimeDilationBehavior.normal; | ||
} | ||
|
||
/// Configures how a [Timer] behaves in the presence of time dilation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc for this enum and its values seems correct but still a bit difficult to understand.
I think it would be useful to be more expansive, explain what the overall goal is, what the typical use cases are, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've expanded the documentation significantly, I think it reads a lot better now.
|
||
/// Configures how a [Timer] behaves in the presence of time dilation. | ||
enum TimeDilationBehavior { | ||
/// The adjusted timestampts are accepted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: timestampts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
@@ -36,6 +36,20 @@ abstract class TickerProvider { | |||
/// | |||
/// The kind of ticker provided depends on the kind of ticker provider. | |||
Ticker createTicker(TickerCallback onTick); | |||
|
|||
/// The behavior of the created [Ticker]s in the presence of time dilation. | |||
/// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please explain (in laymans terms) what effect the two TimeDilationBehavior values will have on the ticker
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partially covered by the expanded enum doc comments I believe
@@ -59,7 +73,7 @@ class Ticker { | |||
/// | |||
/// An optional label can be provided for debugging purposes. That label | |||
/// will appear in the [toString] output in debug builds. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a good place to mention why you might want to override the default value of the timeDilationBehavior
parameter.
|
||
_onTick(timeStamp - _startTime); | ||
if (timeDilationBehavior == TimeDilationBehavior.unscaled && timeDilation != 1.0) { | ||
final double scale = 1 / timeDilation; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert(timeDilation > 0.0)
1.0 / timeDilation instead of 1 / ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
if (timeDilationBehavior == TimeDilationBehavior.unscaled && timeDilation != 1.0) { | ||
final double scale = 1 / timeDilation; | ||
final Duration delta = timeStamp - _startTime; | ||
final Duration tick = new Duration(microseconds: (delta.inMicroseconds / scale).round()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should clamp the microseconds value to a reasonable range?
@@ -378,6 +378,10 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture | |||
@override | |||
void handleAccessibilityFeaturesChanged() { | |||
super.handleAccessibilityFeaturesChanged(); | |||
if (ui.window.accessibilityFeatures.disableAnimations) | |||
timeDilation = 0.05; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An implementation comment like // see Ticker.timeDilationBehavior
might help. If 0.05 was a local constant var you could explain why 0.05 in the var's doc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -264,6 +264,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin | |||
ScrollBehavior _configuration; | |||
ScrollPhysics _physics; | |||
|
|||
@override | |||
TimeDilationBehavior get timeDilationBehavior => TimeDilationBehavior.unscaled; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you had a standard comment about why timeDilationBehavior is unscaled, you could copy it around with these ...unscaled overrides.
@@ -261,6 +264,9 @@ class _FakeTicker implements Ticker { | |||
@override | |||
String get debugLabel => null; | |||
|
|||
@override | |||
TimeDilationBehavior timeDilationBehavior; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps it would be no less of a fake ticker, but why not return ...normal here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -83,6 +83,54 @@ void main() { | |||
expect(ticker.toString(debugIncludeStack: true), contains('testFunction')); | |||
}); | |||
|
|||
testWidgets('Ticker can be sped up with time dilation', (WidgetTester tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't there be tests for the timeDilationBehavior stuff?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are tests for the time dilation stuff :D
We shouldn't drive this from |
@Hixie I refactored this to have a similar effect using AnimationController config. It requires a bit more plumbing, as the logic will have to be slightly different for each Simulation - I'm not sure if there is a better way to short-circuit that while still preserving their overall behavior - for example, making sure when a spring simulation is dragged it still feels like a spring. |
Friendly ping @Hixie , @HansMuller |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This all seems fine. I had some trouble sorting out the docs and some tests area needed.
@@ -38,6 +38,31 @@ const Tolerance _kFlingTolerance = Tolerance( | |||
distance: 0.01, | |||
); | |||
|
|||
/// Configures how an [AnimationController] behaves when animations are disabled. | |||
/// | |||
/// When [AccessibilityFeatures.disableAnimations] is enabled, animations are |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe replace "enabled" with "true" since "disabledFoo is enabled" is a little bit difficult to parse.
It might also be helpful to smoosh in the idea that "AccessibilityFeatures.disableAnimations" corresponds to the device asking Flutter to disable animations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
/// Configures how an [AnimationController] behaves when animations are disabled. | ||
/// | ||
/// When [AccessibilityFeatures.disableAnimations] is enabled, animations are | ||
/// speed up or modified to remove most of the visual component. This enum is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
speed => sped up (shouldn't this be slowed down?)
to remove most of the visual changes?
I think the overall point is that if the system asks us to disable animations we're going to try and slow down or eliminate animated visual transitions that are just decorative anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rephrased this to be cleared that we reduce the duration and number of frames for an animation
/// the purpose of preserving some visual animation or simulation behavior for | ||
/// accessibility. | ||
/// | ||
/// For example, the [AnimationController which controls the physics simulation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[AnimationController => [AnimationController]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
/// for a scrollable list will have [AnimationBehavior.unscalable] so that when a | ||
/// user attempts to scroll it does not jump to the end/beginning too quickly. | ||
enum AnimationBehavior { | ||
/// The [AnimationController] will reduce or remove the visual component by |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"the visual component" isn't the clearest way to express this because component.
Since the other value is called "unscalable", it might be helpful to describe this value in terms of scaling.
Based on this description, I don't really understand what the AnimationController is going to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rephrased
@@ -170,6 +197,17 @@ class AnimationController extends Animation<double> | |||
/// identifying animation controller instances in debug output. | |||
final String debugLabel; | |||
|
|||
/// The behavior of the animation or simulation when animations are disabled. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this property isn't just about the AnimationController, it would help to explain how it affects anything else.
@@ -170,6 +197,17 @@ class AnimationController extends Animation<double> | |||
/// identifying animation controller instances in debug output. | |||
final String debugLabel; | |||
|
|||
/// The behavior of the animation or simulation when animations are disabled. | |||
/// | |||
/// Defaults to [AnimationBehavior.normal] for the [new AnimationBehavior()] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know that [new Foo] was required to identify a constructor. It looks (grepping through our sources) like the '()' isn't needed, so just [new AnimationBehavior]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
if (ui.window.accessibilityFeatures.disableAnimations) { | ||
switch (behavior) { | ||
case AnimationBehavior.normal: | ||
scale = 0.05; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The descriptions of AnimationBehavior.normal
might as well point out the similarity with timeDilation=0.05 as well as explaining what the actual effect is in, uh, layman's terms.
|
||
/// The [AnimationController] will preserve its behavior. | ||
/// | ||
/// This is the default for repeating animations in order to prevent them from |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This value is typically used by repeating animations, like indeterminate progress indicators, to prevent the AnimationController from ticking at all?
Also: since (as far as I can tell) this value means that we don't do anything when the device requests that animations be disabled, it would be helpful to say as much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed the enum to "preserve" since this captures the concept better, scaling was more related to the time dilation stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@@ -170,6 +197,18 @@ class AnimationController extends Animation<double> | |||
/// identifying animation controller instances in debug output. | |||
final String debugLabel; | |||
|
|||
/// The behavior of the animation when [AccessibilityFeatures.disableAnimations] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the animation => the controller
Same as the last "See also" line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
/// The currently active set of [AccessibilityFeatures]. | ||
/// | ||
/// This is initialized the first time [runApp] is called and updated whenever | ||
/// the a flag is changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the a => a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, but also moved to renderer binding to fix circular dep
@HansMuller I made some changes - I could not expose the disableAnimations flag on either the WidgetsBinding or the RendererBinding since both of those depend on the animation layer, and doing so caused a circular import error. To resolve the situation I create a new SemanticsBindings, which we've been meaning to do anyway, but didn't move anything to it yet. |
SemanticsBindings sounds rather highfalutin but I don't think you could have called it anything else :-). Still looks good to me. |
/// | ||
/// Defaults to [AnimationBehavior.normal] for the [new AnimationBehavior] | ||
/// constructor, and [AnimationBehavior.preserve] for the | ||
/// [new AnimationBehavior.unbounded] constructor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some of these AnimationBehaviors are presumably AnimationControllers?
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should get this via the TickerProvider or via the Ticker, not directly from the binding, so that it is easier to override.
@@ -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}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: inconsistent spacing (space after {
but no space before }
)
if (SemanticsBinding.instance.disableAnimations) { | ||
switch (behavior) { | ||
case AnimationBehavior.normal: | ||
scale = 200.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this and the 0.05 constant earlier are presumably intentional related. We should derive them from the same constant.
@@ -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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just multiply the duration here rather than pass the value and then have the constructor multiply?
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter/services.dart'; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extraneous blank line
/// To listen to changes to accessibility features, create a | ||
/// [WidgetsBindingObserver] and listen to [didChangeAccessibilityFeatures]. | ||
ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; | ||
ui.AccessibilityFeatures _accessibilityFeatures; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could just make this a ValueListenable...
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since you expose AccessibilityFeatures in your API, you should re-export it
|
||
/// 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you need ServicesBinding?
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also wherever you call into SemanticsBinding.instance, handle the case of it being null (which might happen if someone has code that they haven't updated to mix in the new binding).
@@ -127,6 +128,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase | |||
@protected | |||
bool get checkIntrinsicSizes => false; | |||
|
|||
@override | |||
bool disableAnimations = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently docs aren't inherited from mixins...
https://docs.flutter.io/flutter/flutter_test/TestWidgetsFlutterBinding/disableAnimations.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested locally, and it's not about mixins - it's about overriding a getter with a member variable. Filed dart-lang/dartdoc#1779
SingleTickerProviderStateMixin
to allow specific widgets to opt out of disabled animation. This is necessary for certain scroll physics and other features that use the Ticker but aren't strictly an "Animation".Alternatives considered:
AnimationController
.The Ticker is the source of updates for all Animation controllers. Adjusting timing here effects all downstream animations without requiring code changes for most widgets. For special cases, there is an easy opt out with a single line of code. Since multiple animations are created from a single ticker, and we don't (?) combine tickers, the adjustment to the passage of time shouldn't break anything. Time is still monotonic, just not in sync globally.
On the other hand, AnimationController is often created by widgets themselves, meaning each individual widget would have to look up and pass this configuration to the constructor correctly.
Manually adjusting durations based on the setting is opt in, meaning most widgets will never correctly conform, and a also tremendous amount of work.
Both cases requiring reading a global setting, but the former is much less of a bucket brigade. The advantage of being configurable per subtree that manual adjustment gives isn't worth it.
This doesn't address how to handle certain indefinite animations. It might be correct for accessibility purposes to ignore the timeDilation. Otherwise it seems like they should react specifically by showing a specific keyframe animation - this PR doesn't attempt to address this beyond making sure they don't flash/spin at 20x normal speed.
Work towards #4827
Fixes #11436
EDIT:
I have updated the PR to use a configuration enum for the animation controller which controls how the
_InterpolationSimulation
used for most animations and theSpringSimulation
used bycontroller.fling
behave under the presences of disable animations.For the former I decrease the duration, and for the latter I increase the velocity.