Commit 58801a0e authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Expose the current velocity on AnimationController (#6908)

(and minor doc changes)

This will in the future allow Scrollable to not track the Simulation itself.
parent 861492d6
...@@ -49,12 +49,25 @@ class AnimationController extends Animation<double> ...@@ -49,12 +49,25 @@ class AnimationController extends Animation<double>
/// Creates an animation controller. /// Creates an animation controller.
/// ///
/// * [value] is the initial value of the animation. /// * [value] is the initial value of the animation. If defaults to the lower
/// bound.
///
/// * [duration] is the length of time this animation should last. /// * [duration] is the length of time this animation should last.
/// * [debugLabel] is a string to help identify this animation during debugging (used by [toString]). ///
/// * [lowerBound] is the smallest value this animation can obtain and the value at which this animation is deemed to be dismissed. /// * [debugLabel] is a string to help identify this animation during
/// * [upperBound] is the largest value this animation can obtain and the value at which this animation is deemed to be completed. /// debugging (used by [toString]).
/// * `vsync` is the [TickerProvider] for the current context. It can be changed by calling [resync]. ///
/// * [lowerBound] is the smallest value this animation can obtain and the
/// value at which this animation is deemed to be dismissed. It cannot be
/// null.
///
/// * [upperBound] is the largest value this animation can obtain and the
/// value at which this animation is deemed to be completed. It cannot be
/// null.
///
/// * `vsync` is the [TickerProvider] for the current context. It can be
/// changed by calling [resync]. It is required and cannot be null. See
/// [TickerProvider] for advice on obtaining a ticker provider.
AnimationController({ AnimationController({
double value, double value,
this.duration, this.duration,
...@@ -63,6 +76,8 @@ class AnimationController extends Animation<double> ...@@ -63,6 +76,8 @@ class AnimationController extends Animation<double>
this.upperBound: 1.0, this.upperBound: 1.0,
@required TickerProvider vsync, @required TickerProvider vsync,
}) { }) {
assert(lowerBound != null);
assert(upperBound != null);
assert(upperBound >= lowerBound); assert(upperBound >= lowerBound);
assert(vsync != null); assert(vsync != null);
_direction = _AnimationDirection.forward; _direction = _AnimationDirection.forward;
...@@ -73,9 +88,15 @@ class AnimationController extends Animation<double> ...@@ -73,9 +88,15 @@ class AnimationController extends Animation<double>
/// Creates an animation controller with no upper or lower bound for its value. /// Creates an animation controller with no upper or lower bound for its value.
/// ///
/// * [value] is the initial value of the animation. /// * [value] is the initial value of the animation.
///
/// * [duration] is the length of time this animation should last. /// * [duration] is the length of time this animation should last.
/// * [debugLabel] is a string to help identify this animation during debugging (used by [toString]). ///
/// * `vsync` is the [TickerProvider] for the current context. It can be changed by calling [resync]. /// * [debugLabel] is a string to help identify this animation during
/// debugging (used by [toString]).
///
/// * `vsync` is the [TickerProvider] for the current context. It can be
/// changed by calling [resync]. It is required and cannot be null. See
/// [TickerProvider] for advice on obtaining a ticker provider.
/// ///
/// This constructor is most useful for animations that will be driven using a /// This constructor is most useful for animations that will be driven using a
/// physics simulation, especially when the physics simulation has no /// physics simulation, especially when the physics simulation has no
...@@ -149,16 +170,23 @@ class AnimationController extends Animation<double> ...@@ -149,16 +170,23 @@ class AnimationController extends Animation<double>
_checkStatusChanged(); _checkStatusChanged();
} }
double get velocity {
if (!isAnimating)
return 0.0;
return _simulation.dx(lastElapsedDuration.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND);
}
void _internalSetValue(double newValue) { void _internalSetValue(double newValue) {
_value = newValue.clamp(lowerBound, upperBound); _value = newValue.clamp(lowerBound, upperBound);
if (_value == lowerBound) { if (_value == lowerBound) {
_status = AnimationStatus.dismissed; _status = AnimationStatus.dismissed;
} else if (_value == upperBound) { } else if (_value == upperBound) {
_status = AnimationStatus.completed; _status = AnimationStatus.completed;
} else } else {
_status = (_direction == _AnimationDirection.forward) ? _status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward : AnimationStatus.forward :
AnimationStatus.reverse; AnimationStatus.reverse;
}
} }
/// The amount of time that has passed between the time the animation started and the most recent tick of the animation. /// The amount of time that has passed between the time the animation started and the most recent tick of the animation.
...@@ -337,6 +365,7 @@ class AnimationController extends Animation<double> ...@@ -337,6 +365,7 @@ class AnimationController extends Animation<double>
void _tick(Duration elapsed) { void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed; _lastElapsedDuration = elapsed;
double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND; double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND;
assert(elapsedInSeconds >= 0.0);
_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound); _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
if (_simulation.isDone(elapsedInSeconds)) { if (_simulation.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ? _status = (_direction == _AnimationDirection.forward) ?
...@@ -373,7 +402,6 @@ class _InterpolationSimulation extends Simulation { ...@@ -373,7 +402,6 @@ class _InterpolationSimulation extends Simulation {
@override @override
double x(double timeInSeconds) { double x(double timeInSeconds) {
assert(timeInSeconds >= 0.0);
double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0); double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
if (t == 0.0) if (t == 0.0)
return _begin; return _begin;
...@@ -384,7 +412,10 @@ class _InterpolationSimulation extends Simulation { ...@@ -384,7 +412,10 @@ class _InterpolationSimulation extends Simulation {
} }
@override @override
double dx(double timeInSeconds) => 1.0; double dx(double timeInSeconds) {
double epsilon = tolerance.time;
return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
}
@override @override
bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds; bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
...@@ -409,7 +440,7 @@ class _RepeatingSimulation extends Simulation { ...@@ -409,7 +440,7 @@ class _RepeatingSimulation extends Simulation {
} }
@override @override
double dx(double timeInSeconds) => 1.0; double dx(double timeInSeconds) => (max - min) / _periodInSeconds;
@override @override
bool isDone(double timeInSeconds) => false; bool isDone(double timeInSeconds) => false;
......
...@@ -19,6 +19,9 @@ export 'dart:ui' show VoidCallback; ...@@ -19,6 +19,9 @@ export 'dart:ui' show VoidCallback;
/// Slows down animations by this factor to help in development. /// Slows down animations by this factor to help in development.
double get timeDilation => _timeDilation; double get timeDilation => _timeDilation;
double _timeDilation = 1.0; double _timeDilation = 1.0;
/// Setting the time dilation automatically calls [SchedulerBinding.resetEpoch]
/// to ensure that time stamps seen by consumers of the scheduler binding are
/// always increasing.
set timeDilation(double value) { set timeDilation(double value) {
if (_timeDilation == value) if (_timeDilation == value)
return; return;
...@@ -474,7 +477,8 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -474,7 +477,8 @@ abstract class SchedulerBinding extends BindingBase {
Duration _epochStart = Duration.ZERO; Duration _epochStart = Duration.ZERO;
Duration _lastRawTimeStamp = Duration.ZERO; Duration _lastRawTimeStamp = Duration.ZERO;
/// Prepares the scheduler for a non-monotonic change to how time stamps are calcuated. /// Prepares the scheduler for a non-monotonic change to how time stamps are
/// calcuated.
/// ///
/// Callbacks received from the scheduler assume that their time stamps are /// Callbacks received from the scheduler assume that their time stamps are
/// monotonically increasing. The raw time stamp passed to [handleBeginFrame] /// monotonically increasing. The raw time stamp passed to [handleBeginFrame]
...@@ -483,13 +487,13 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -483,13 +487,13 @@ abstract class SchedulerBinding extends BindingBase {
/// to appear to run backwards. /// to appear to run backwards.
/// ///
/// The [resetEpoch] function ensures that the time stamps are monotonic by /// The [resetEpoch] function ensures that the time stamps are monotonic by
/// reseting the base time stamp used for future time stamp adjustments to the /// resetting the base time stamp used for future time stamp adjustments to the
/// current value. For example, if the [timeDilation] decreases, rather than /// current value. For example, if the [timeDilation] decreases, rather than
/// scaling down the [Duration] since the beginning of time, [resetEpoch] will /// scaling down the [Duration] since the beginning of time, [resetEpoch] will
/// ensure that we only scale down the duration since [resetEpoch] was called. /// ensure that we only scale down the duration since [resetEpoch] was called.
/// ///
/// Note: Setting [timeDilation] calls [resetEpoch] automatically. You don't /// Setting [timeDilation] calls [resetEpoch] automatically. You don't need to
/// need to call [resetEpoch] yourself. /// call [resetEpoch] yourself.
void resetEpoch() { void resetEpoch() {
_epochStart = _adjustForEpoch(_lastRawTimeStamp); _epochStart = _adjustForEpoch(_lastRawTimeStamp);
_firstRawTimeStampInEpoch = null; _firstRawTimeStampInEpoch = null;
......
...@@ -16,6 +16,18 @@ import 'binding.dart'; ...@@ -16,6 +16,18 @@ import 'binding.dart';
typedef void TickerCallback(Duration elapsed); typedef void TickerCallback(Duration elapsed);
/// An interface implemented by classes that can vend [Ticker] objects. /// An interface implemented by classes that can vend [Ticker] objects.
///
/// Tickers can be used by any object that wants to be notified whenever a frame
/// triggers, but are most commonly used indirectly via an
/// [AnimationController]. [AnimationController]s need a [TickerProvider] to
/// obtain their [Ticker]. If you are creating an [AnimationController] from a
/// [State], then you can use the [TickerProviderStateMixin] and
/// [SingleTickerProviderStateMixin] classes to obtain a suitable
/// [TickerProvider]. The widget test framework [WidgetTester] object can be
/// used as a ticker provider in the context of tests. In other contexts, you
/// will have to either pass a [TickerProvider] from a higher level (e.g.
/// indirectly from a [State] that mixes in [TickerProviderStateMixin]), or
/// create a custom [TickerProvider] subclass.
abstract class TickerProvider { abstract class TickerProvider {
/// Abstract const constructor. This constructor enables subclasses to provide /// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions. /// const constructors so that they can be used in const expressions.
...@@ -36,6 +48,10 @@ abstract class TickerProvider { ...@@ -36,6 +48,10 @@ abstract class TickerProvider {
/// still elapses, and [start] and [stop] can still be called, but no callbacks /// still elapses, and [start] and [stop] can still be called, but no callbacks
/// are called. /// are called.
/// ///
/// By convention, the [start] and [stop] methods are used by the ticker's
/// consumer, and the [muted] property is controlled by the [TickerProvider]
/// that created the ticker.
///
/// Tickers are driven by the [SchedulerBinding]. See /// Tickers are driven by the [SchedulerBinding]. See
/// [SchedulerBinding.scheduleFrameCallback]. /// [SchedulerBinding.scheduleFrameCallback].
class Ticker { class Ticker {
...@@ -64,6 +80,10 @@ class Ticker { ...@@ -64,6 +80,10 @@ class Ticker {
/// ///
/// When set to false, unsilences the ticker, potentially scheduling a frame /// When set to false, unsilences the ticker, potentially scheduling a frame
/// to handle the next tick. /// to handle the next tick.
///
/// By convention, the [muted] property is controlled by the object that
/// created the [Ticker] (typically a [TickerProvider]), not the object that
/// listens to the ticker's ticks.
set muted(bool value) { set muted(bool value) {
if (value == muted) if (value == muted)
return; return;
...@@ -75,7 +95,7 @@ class Ticker { ...@@ -75,7 +95,7 @@ class Ticker {
} }
} }
/// Whether this ticker has scheduled a call to call its callback /// Whether this [Ticker] has scheduled a call to call its callback
/// on the next frame. /// on the next frame.
/// ///
/// A ticker that is [muted] can be active (see [isActive]) yet not be /// A ticker that is [muted] can be active (see [isActive]) yet not be
...@@ -85,7 +105,7 @@ class Ticker { ...@@ -85,7 +105,7 @@ class Ticker {
// and then this could return an accurate view of the actual scheduler. // and then this could return an accurate view of the actual scheduler.
bool get isTicking => _completer != null && !muted; bool get isTicking => _completer != null && !muted;
/// Whether time is elapsing for this ticker. Becomes true when [start] is /// Whether time is elapsing for this [Ticker]. Becomes true when [start] is
/// called and false when [stop] is called. /// called and false when [stop] is called.
/// ///
/// A ticker can be active yet not be actually ticking (i.e. not be calling /// A ticker can be active yet not be actually ticking (i.e. not be calling
...@@ -95,7 +115,7 @@ class Ticker { ...@@ -95,7 +115,7 @@ class Ticker {
Duration _startTime; Duration _startTime;
/// Starts the clock for this ticker. If the ticker is not [muted], then this /// Starts the clock for this [Ticker]. If the ticker is not [muted], then this
/// also starts calling the ticker's callback once per animation frame. /// also starts calling the ticker's callback once per animation frame.
/// ///
/// The returned future resolves once the ticker [stop]s ticking. /// The returned future resolves once the ticker [stop]s ticking.
...@@ -104,6 +124,9 @@ class Ticker { ...@@ -104,6 +124,9 @@ class Ticker {
/// ///
/// This method cannot be called while the ticker is active. To restart the /// This method cannot be called while the ticker is active. To restart the
/// ticker, first [stop] it. /// ticker, first [stop] it.
///
/// By convention, this method is used by the object that receives the ticks
/// (as opposed to the [TickerProvider] which created the ticker).
Future<Null> start() { Future<Null> start() {
assert(() { assert(() {
if (isTicking) { if (isTicking) {
...@@ -125,13 +148,16 @@ class Ticker { ...@@ -125,13 +148,16 @@ class Ticker {
return _completer.future; return _completer.future;
} }
/// Stops calling the ticker's callback. /// Stops calling this [Ticker]'s callback.
/// ///
/// Causes the future returned by [start] to resolve. /// Causes the future returned by [start] to resolve.
/// ///
/// Calling this sets [isActive] to false. /// Calling this sets [isActive] to false.
/// ///
/// This method does nothing if called when the ticker is inactive. /// This method does nothing if called when the ticker is inactive.
///
/// By convention, this method is used by the object that receives the ticks
/// (as opposed to the [TickerProvider] which created the ticker).
void stop() { void stop() {
if (!isTicking) if (!isTicking)
return; return;
...@@ -153,7 +179,7 @@ class Ticker { ...@@ -153,7 +179,7 @@ class Ticker {
int _animationId; int _animationId;
/// Whether this ticker has already scheduled a frame callback. /// Whether this [Ticker] has already scheduled a frame callback.
@protected @protected
bool get scheduled => _animationId != null; bool get scheduled => _animationId != null;
...@@ -166,6 +192,7 @@ class Ticker { ...@@ -166,6 +192,7 @@ class Ticker {
/// * A tick has already been scheduled for the coming frame. /// * A tick has already been scheduled for the coming frame.
/// * The ticker is not active ([start] has not been called). /// * The ticker is not active ([start] has not been called).
/// * The ticker is not ticking, e.g. because it is [muted] (see [isTicking]). /// * The ticker is not ticking, e.g. because it is [muted] (see [isTicking]).
@protected
bool get shouldScheduleTick => isTicking && !scheduled; bool get shouldScheduleTick => isTicking && !scheduled;
void _tick(Duration timeStamp) { void _tick(Duration timeStamp) {
...@@ -210,8 +237,8 @@ class Ticker { ...@@ -210,8 +237,8 @@ class Ticker {
assert(!shouldScheduleTick); assert(!shouldScheduleTick);
} }
/// Makes this ticker take the state of another ticker, and disposes the other /// Makes this [Ticker] take the state of another ticker, and disposes the
/// ticker. /// other ticker.
/// ///
/// This is useful if an object with a [Ticker] is given a new /// This is useful if an object with a [Ticker] is given a new
/// [TickerProvider] but needs to maintain continuity. In particular, this /// [TickerProvider] but needs to maintain continuity. In particular, this
......
...@@ -208,15 +208,58 @@ void main() { ...@@ -208,15 +208,58 @@ void main() {
); );
expect(controller.toString(), hasOneLineDescription); expect(controller.toString(), hasOneLineDescription);
controller.forward(); controller.forward();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 10));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20)); WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
expect(controller.toString(), hasOneLineDescription); expect(controller.toString(), hasOneLineDescription);
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 120)); WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
expect(controller.toString(), hasOneLineDescription); expect(controller.toString(), hasOneLineDescription);
controller.reverse(); controller.reverse();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20)); WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 40));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30)); WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 50));
expect(controller.toString(), hasOneLineDescription); expect(controller.toString(), hasOneLineDescription);
controller.stop(); controller.stop();
}); });
test('velocity test - linear', () {
AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: const TestVSync(),
);
// mid-flight
controller.forward();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 0));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 500));
expect(controller.velocity, inInclusiveRange(0.9, 1.1));
// edges
controller.forward();
expect(controller.velocity, inInclusiveRange(0.4, 0.6));
WidgetsBinding.instance.handleBeginFrame(Duration.ZERO);
expect(controller.velocity, inInclusiveRange(0.4, 0.6));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 5));
expect(controller.velocity, inInclusiveRange(0.9, 1.1));
controller.forward(from: 0.5);
expect(controller.velocity, inInclusiveRange(0.4, 0.6));
WidgetsBinding.instance.handleBeginFrame(Duration.ZERO);
expect(controller.velocity, inInclusiveRange(0.4, 0.6));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 5));
expect(controller.velocity, inInclusiveRange(0.9, 1.1));
// stopped
controller.forward(from: 1.0);
expect(controller.velocity, 0.0);
WidgetsBinding.instance.handleBeginFrame(Duration.ZERO);
expect(controller.velocity, 0.0);
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 500));
expect(controller.velocity, 0.0);
controller.forward();
WidgetsBinding.instance.handleBeginFrame(Duration.ZERO);
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 1000));
expect(controller.velocity, 0.0);
controller.stop();
});
} }
...@@ -25,7 +25,7 @@ class FooState extends State<Foo> { ...@@ -25,7 +25,7 @@ class FooState extends State<Foo> {
children: <Widget>[ children: <Widget>[
new GestureDetector( new GestureDetector(
onTap: () { onTap: () {
setState(() {}); setState(() { /* this is needed to trigger the original bug this is regression-testing */ });
scrollableKey.currentState.scrollBy(200.0, duration: const Duration(milliseconds: 500)); scrollableKey.currentState.scrollBy(200.0, duration: const Duration(milliseconds: 500));
}, },
child: new DecoratedBox( child: new DecoratedBox(
......
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