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>
/// 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.
/// * [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.
/// * [upperBound] is the largest value this animation can obtain and the value at which this animation is deemed to be completed.
/// * `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]).
///
/// * [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({
double value,
this.duration,
......@@ -63,6 +76,8 @@ class AnimationController extends Animation<double>
this.upperBound: 1.0,
@required TickerProvider vsync,
}) {
assert(lowerBound != null);
assert(upperBound != null);
assert(upperBound >= lowerBound);
assert(vsync != null);
_direction = _AnimationDirection.forward;
......@@ -73,9 +88,15 @@ class AnimationController extends Animation<double>
/// Creates an animation controller with no upper or lower bound for its value.
///
/// * [value] is the initial value of the animation.
///
/// * [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
/// physics simulation, especially when the physics simulation has no
......@@ -149,16 +170,23 @@ class AnimationController extends Animation<double>
_checkStatusChanged();
}
double get velocity {
if (!isAnimating)
return 0.0;
return _simulation.dx(lastElapsedDuration.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND);
}
void _internalSetValue(double newValue) {
_value = newValue.clamp(lowerBound, upperBound);
if (_value == lowerBound) {
_status = AnimationStatus.dismissed;
} else if (_value == upperBound) {
_status = AnimationStatus.completed;
} else
} else {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward :
AnimationStatus.reverse;
}
}
/// 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>
void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed;
double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND;
assert(elapsedInSeconds >= 0.0);
_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
if (_simulation.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ?
......@@ -373,7 +402,6 @@ class _InterpolationSimulation extends Simulation {
@override
double x(double timeInSeconds) {
assert(timeInSeconds >= 0.0);
double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
if (t == 0.0)
return _begin;
......@@ -384,7 +412,10 @@ class _InterpolationSimulation extends Simulation {
}
@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
bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
......@@ -409,7 +440,7 @@ class _RepeatingSimulation extends Simulation {
}
@override
double dx(double timeInSeconds) => 1.0;
double dx(double timeInSeconds) => (max - min) / _periodInSeconds;
@override
bool isDone(double timeInSeconds) => false;
......
......@@ -19,6 +19,9 @@ export 'dart:ui' show VoidCallback;
/// Slows down animations by this factor to help in development.
double get timeDilation => _timeDilation;
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) {
if (_timeDilation == value)
return;
......@@ -474,7 +477,8 @@ abstract class SchedulerBinding extends BindingBase {
Duration _epochStart = 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
/// monotonically increasing. The raw time stamp passed to [handleBeginFrame]
......@@ -483,13 +487,13 @@ abstract class SchedulerBinding extends BindingBase {
/// to appear to run backwards.
///
/// 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
/// scaling down the [Duration] since the beginning of time, [resetEpoch] will
/// ensure that we only scale down the duration since [resetEpoch] was called.
///
/// Note: Setting [timeDilation] calls [resetEpoch] automatically. You don't
/// need to call [resetEpoch] yourself.
/// Setting [timeDilation] calls [resetEpoch] automatically. You don't need to
/// call [resetEpoch] yourself.
void resetEpoch() {
_epochStart = _adjustForEpoch(_lastRawTimeStamp);
_firstRawTimeStampInEpoch = null;
......
......@@ -16,6 +16,18 @@ import 'binding.dart';
typedef void TickerCallback(Duration elapsed);
/// 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 const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
......@@ -36,6 +48,10 @@ abstract class TickerProvider {
/// still elapses, and [start] and [stop] can still be called, but no callbacks
/// 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
/// [SchedulerBinding.scheduleFrameCallback].
class Ticker {
......@@ -64,6 +80,10 @@ class Ticker {
///
/// When set to false, unsilences the ticker, potentially scheduling a frame
/// 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) {
if (value == muted)
return;
......@@ -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.
///
/// A ticker that is [muted] can be active (see [isActive]) yet not be
......@@ -85,7 +105,7 @@ class Ticker {
// and then this could return an accurate view of the actual scheduler.
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.
///
/// A ticker can be active yet not be actually ticking (i.e. not be calling
......@@ -95,7 +115,7 @@ class Ticker {
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.
///
/// The returned future resolves once the ticker [stop]s ticking.
......@@ -104,6 +124,9 @@ class Ticker {
///
/// This method cannot be called while the ticker is active. To restart the
/// 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() {
assert(() {
if (isTicking) {
......@@ -125,13 +148,16 @@ class Ticker {
return _completer.future;
}
/// Stops calling the ticker's callback.
/// Stops calling this [Ticker]'s callback.
///
/// Causes the future returned by [start] to resolve.
///
/// Calling this sets [isActive] to false.
///
/// 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() {
if (!isTicking)
return;
......@@ -153,7 +179,7 @@ class Ticker {
int _animationId;
/// Whether this ticker has already scheduled a frame callback.
/// Whether this [Ticker] has already scheduled a frame callback.
@protected
bool get scheduled => _animationId != null;
......@@ -166,6 +192,7 @@ class Ticker {
/// * A tick has already been scheduled for the coming frame.
/// * The ticker is not active ([start] has not been called).
/// * The ticker is not ticking, e.g. because it is [muted] (see [isTicking]).
@protected
bool get shouldScheduleTick => isTicking && !scheduled;
void _tick(Duration timeStamp) {
......@@ -210,8 +237,8 @@ class Ticker {
assert(!shouldScheduleTick);
}
/// Makes this ticker take the state of another ticker, and disposes the other
/// ticker.
/// Makes this [Ticker] take the state of another ticker, and disposes the
/// other ticker.
///
/// This is useful if an object with a [Ticker] is given a new
/// [TickerProvider] but needs to maintain continuity. In particular, this
......
......@@ -208,15 +208,58 @@ void main() {
);
expect(controller.toString(), hasOneLineDescription);
controller.forward();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 10));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
expect(controller.toString(), hasOneLineDescription);
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 120));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
expect(controller.toString(), hasOneLineDescription);
controller.reverse();
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 20));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 30));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 40));
WidgetsBinding.instance.handleBeginFrame(const Duration(milliseconds: 50));
expect(controller.toString(), hasOneLineDescription);
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> {
children: <Widget>[
new GestureDetector(
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));
},
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