Skip to content

Commit

Permalink
Add reverse functionality to repeat() of AnimationController (flutter…
Browse files Browse the repository at this point in the history
  • Loading branch information
flschweiger authored and Hans Muller committed Jan 24, 2019
1 parent c362d8d commit d1bde2e
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 8 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ Jasper van Riet <[email protected]>
Mattijs Fuijkschot <[email protected]>
TruongSinh Tran-Nguyen <[email protected]>
Sander Dalby Larsen <[email protected]>
Marco Scannadinari <[email protected]>
Marco Scannadinari <[email protected]>
Frederik Schweiger <[email protected]>
33 changes: 26 additions & 7 deletions packages/flutter/lib/src/animation/animation_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -579,15 +579,19 @@ class AnimationController extends Animation<double>
/// Starts running this animation in the forward direction, and
/// restarts the animation when it completes.
///
/// Defaults to repeating between the lower and upper bounds.
/// Defaults to repeating between the [lowerBound] and [upperBound] of the
/// [AnimationController] when no explicit value is set for [min] and [max].
///
/// With [reverse] set to true, instead of always starting over at [min]
/// the value will alternate between [min] and [max] values on each repeat.
///
/// Returns a [TickerFuture] that never completes. The [TickerFuture.orCancel] future
/// completes with an error when the animation is stopped (e.g. with [stop]).
///
/// 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 repeat({ double min, double max, Duration period }) {
TickerFuture repeat({ double min, double max, bool reverse = false, Duration period }) {
min ??= lowerBound;
max ??= upperBound;
period ??= duration;
Expand All @@ -602,7 +606,10 @@ class AnimationController extends Animation<double>
}
return true;
}());
return animateWith(_RepeatingSimulation(min, max, period));
assert(max >= min);
assert(max <= upperBound && min >= lowerBound);
assert(reverse != null);
return animateWith(_RepeatingSimulation(_value, min, max, reverse, period));
}

/// Drives the animation with a critically damped spring (within [lowerBound]
Expand Down Expand Up @@ -789,21 +796,33 @@ class _InterpolationSimulation extends Simulation {
}

class _RepeatingSimulation extends Simulation {
_RepeatingSimulation(this.min, this.max, Duration period)
: _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond {
_RepeatingSimulation(double initialValue, this.min, this.max, this.reverse, Duration period)
: _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond,
_initialT = (max == min) ? 0.0 : (initialValue / (max - min)) * (period.inMicroseconds / Duration.microsecondsPerSecond) {
assert(_periodInSeconds > 0.0);
assert(_initialT >= 0.0);
}

final double min;
final double max;
final bool reverse;

final double _periodInSeconds;
final double _initialT;

@override
double x(double timeInSeconds) {
assert(timeInSeconds >= 0.0);
final double t = (timeInSeconds / _periodInSeconds) % 1.0;
return ui.lerpDouble(min, max, t);

final double totalTimeInSeconds = timeInSeconds + _initialT;
final double t = (totalTimeInSeconds / _periodInSeconds) % 1.0;
final bool _isPlayingReverse = (totalTimeInSeconds ~/ _periodInSeconds) % 2 == 1;

if (reverse && _isPlayingReverse) {
return ui.lerpDouble(max, min, t);
} else {
return ui.lerpDouble(min, max, t);
}
}

@override
Expand Down
87 changes: 87 additions & 0 deletions packages/flutter/test/animation/animation_controller_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,93 @@ void main() {
statusLog.clear();
});

test('calling repeat with reverse set to true makes the animation alternate '
'between lowerBound and upperBound values on each repeat', () {
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 100),
value: 0.0,
lowerBound: 0.0,
upperBound: 1.0,
vsync: const TestVSync(),
);

expect(controller.value, 0.0);

controller.repeat(reverse: true);
tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 25));
expect(controller.value, 0.25);

tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 125));
expect(controller.value, 0.75);

controller.reset();
controller.value = 1.0;
expect(controller.value, 1.0);

controller.repeat(reverse: true);
tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 25));
expect(controller.value, 0.75);

tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 125));
expect(controller.value, 0.25);

controller.reset();
controller.value = 0.5;
expect(controller.value, 0.5);

controller.repeat(reverse: true);
tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 50));
expect(controller.value, 1.0);

tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 150));
expect(controller.value, 0.0);
});

test('calling repeat with specified min and max values makes the animation '
'alternate between min and max values on each repeat', () {
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 100),
value: 0.0,
lowerBound: 0.0,
upperBound: 1.0,
vsync: const TestVSync(),
);

expect(controller.value, 0.0);

controller.repeat(reverse: true, min: 0.5, max: 1.0);
tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 50));
expect(controller.value, 0.75);

tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 100));
expect(controller.value, 1.00);

tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 200));
expect(controller.value, 0.5);

controller.reset();
controller.value = 0.0;
expect(controller.value, 0.0);

controller.repeat(reverse: true, min: 1.0, max: 1.0);
tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 25));
expect(controller.value, 1.0);

tick(const Duration(milliseconds: 0));
tick(const Duration(milliseconds: 125));
expect(controller.value, 1.0);
});

group('AnimationBehavior', () {
test('Default values for constructor', () {
final AnimationController controller = AnimationController(vsync: const TestVSync());
Expand Down

0 comments on commit d1bde2e

Please sign in to comment.