Skip to content

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

Merged
merged 25 commits into from
Aug 23, 2018
Merged

Conversation

jonahwilliams
Copy link
Member

@jonahwilliams jonahwilliams commented Aug 8, 2018

  • Adds configuration to Tickers to allow them to ignore an active timeDilation by reversing the computation.
  • Adds a TimeDilationBehavior enum and a getter to the 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:

  • Add configuration enum to the AnimationController.
  • Manual adjustment of animation durations in specific widgets.

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 the SpringSimulation used by controller.fling behave under the presences of disable animations.

For the former I decrease the duration, and for the latter I increase the velocity.

@jonahwilliams
Copy link
Member Author

jonahwilliams commented Aug 8, 2018

cc @tvolkert will this break tests? Not this change specifically, but exposing this behavior

@tvolkert
Copy link
Contributor

tvolkert commented Aug 8, 2018

I don't think exposing this will intrinsically break any tests, but I defer to @Hixie on the review.

@jonahwilliams jonahwilliams requested a review from Hixie August 8, 2018 22:26
@jonahwilliams jonahwilliams changed the title [WIP] Support disabled animations using time dilation Support disabled animations using time dilation Aug 8, 2018
@jonahwilliams jonahwilliams changed the title Support disabled animations using time dilation [WI] Support disabled animations using time dilation Aug 8, 2018
@jonahwilliams jonahwilliams changed the title [WI] Support disabled animations using time dilation [WIP] Support disabled animations using time dilation Aug 8, 2018
@jonahwilliams
Copy link
Member Author

WIP for tests

@jonahwilliams jonahwilliams changed the title [WIP] Support disabled animations using time dilation Support disabled animations using time dilation Aug 9, 2018
Copy link
Contributor

@HansMuller HansMuller left a 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.
Copy link
Contributor

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.

Copy link
Member Author

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: timestampts

Copy link
Member Author

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.
///
Copy link
Contributor

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

Copy link
Member Author

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.
Copy link
Contributor

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;
Copy link
Contributor

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 / ...

Copy link
Member Author

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());
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Member Author

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;
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Member Author

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 {
Copy link
Contributor

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?

Copy link
Member Author

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

@Hixie
Copy link
Contributor

Hixie commented Aug 12, 2018

We shouldn't drive this from timeDilation. That's a debugging aid that is intended to make time in general slow down by actually changing the global clock used internally in the app. We should have a separate dilation factor for this that is applied by the AnimationController code, IMHO.

@jonahwilliams
Copy link
Member Author

@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.

@jonahwilliams
Copy link
Member Author

Friendly ping @Hixie , @HansMuller

Copy link
Contributor

@HansMuller HansMuller left a 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
Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Contributor

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.

Copy link
Member Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AnimationController => [AnimationController]

Copy link
Member Author

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
Copy link
Contributor

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.

Copy link
Member Author

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.
Copy link
Contributor

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()]
Copy link
Contributor

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]

Copy link
Member Author

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;
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

@HansMuller HansMuller left a 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]
Copy link
Contributor

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

Copy link
Member Author

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the a => a

Copy link
Member Author

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

@jonahwilliams
Copy link
Member Author

@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.

@HansMuller
Copy link
Contributor

SemanticsBindings sounds rather highfalutin but I don't think you could have called it anything else :-).

Still looks good to me.

@jonahwilliams jonahwilliams merged commit e235ccd into flutter:master Aug 23, 2018
@jonahwilliams jonahwilliams deleted the disable_animation branch August 23, 2018 18:50
///
/// Defaults to [AnimationBehavior.normal] for the [new AnimationBehavior]
/// constructor, and [AnimationBehavior.preserve] for the
/// [new AnimationBehavior.unbounded] constructor.
Copy link
Contributor

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) {
Copy link
Contributor

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}) {
Copy link
Contributor

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;
Copy link
Contributor

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));
Copy link
Contributor

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';


Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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 {
Copy link
Contributor

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) {
Copy link
Contributor

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).

@Hixie
Copy link
Contributor

Hixie commented Aug 24, 2018

LGTM modulo some minor stuff, let me know if you want me to look at it again after the changes

jonahwilliams added a commit to jonahwilliams/flutter that referenced this pull request Aug 29, 2018
jonahwilliams added a commit that referenced this pull request Aug 29, 2018
@@ -127,6 +128,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
@protected
bool get checkIntrinsicSizes => false;

@override
bool disableAnimations = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Animations should disable or degrade when battery saver mode is enabled (Android)
5 participants