Commit d1bde2ed authored by Frederik Schweiger's avatar Frederik Schweiger Committed by Hans Muller

Add reverse functionality to repeat() of AnimationController (#25125)

parent c362d8da
...@@ -34,4 +34,5 @@ Jasper van Riet <jaspervanriet@gmail.com> ...@@ -34,4 +34,5 @@ Jasper van Riet <jaspervanriet@gmail.com>
Mattijs Fuijkschot <mattijs.fuijkschot@gmail.com> Mattijs Fuijkschot <mattijs.fuijkschot@gmail.com>
TruongSinh Tran-Nguyen <i@truongsinh.pro> TruongSinh Tran-Nguyen <i@truongsinh.pro>
Sander Dalby Larsen <srdlarsen@gmail.com> Sander Dalby Larsen <srdlarsen@gmail.com>
Marco Scannadinari <m@scannadinari.co.uk> Marco Scannadinari <m@scannadinari.co.uk>
\ No newline at end of file Frederik Schweiger <mail@flschweiger.net>
...@@ -579,7 +579,11 @@ class AnimationController extends Animation<double> ...@@ -579,7 +579,11 @@ class AnimationController extends Animation<double>
/// Starts running this animation in the forward direction, and /// Starts running this animation in the forward direction, and
/// restarts the animation when it completes. /// 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 /// Returns a [TickerFuture] that never completes. The [TickerFuture.orCancel] future
/// completes with an error when the animation is stopped (e.g. with [stop]). /// completes with an error when the animation is stopped (e.g. with [stop]).
...@@ -587,7 +591,7 @@ class AnimationController extends Animation<double> ...@@ -587,7 +591,7 @@ class AnimationController extends Animation<double>
/// The most recently returned [TickerFuture], if any, is marked as having been /// The most recently returned [TickerFuture], if any, is marked as having been
/// canceled, meaning the future never completes and its [TickerFuture.orCancel] /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
/// derivative future completes with a [TickerCanceled] error. /// 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; min ??= lowerBound;
max ??= upperBound; max ??= upperBound;
period ??= duration; period ??= duration;
...@@ -602,7 +606,10 @@ class AnimationController extends Animation<double> ...@@ -602,7 +606,10 @@ class AnimationController extends Animation<double>
} }
return true; 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] /// Drives the animation with a critically damped spring (within [lowerBound]
...@@ -789,21 +796,33 @@ class _InterpolationSimulation extends Simulation { ...@@ -789,21 +796,33 @@ class _InterpolationSimulation extends Simulation {
} }
class _RepeatingSimulation extends Simulation { class _RepeatingSimulation extends Simulation {
_RepeatingSimulation(this.min, this.max, Duration period) _RepeatingSimulation(double initialValue, this.min, this.max, this.reverse, Duration period)
: _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond { : _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond,
_initialT = (max == min) ? 0.0 : (initialValue / (max - min)) * (period.inMicroseconds / Duration.microsecondsPerSecond) {
assert(_periodInSeconds > 0.0); assert(_periodInSeconds > 0.0);
assert(_initialT >= 0.0);
} }
final double min; final double min;
final double max; final double max;
final bool reverse;
final double _periodInSeconds; final double _periodInSeconds;
final double _initialT;
@override @override
double x(double timeInSeconds) { double x(double timeInSeconds) {
assert(timeInSeconds >= 0.0); 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 @override
......
...@@ -581,6 +581,93 @@ void main() { ...@@ -581,6 +581,93 @@ void main() {
statusLog.clear(); 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', () { group('AnimationBehavior', () {
test('Default values for constructor', () { test('Default values for constructor', () {
final AnimationController controller = AnimationController(vsync: const TestVSync()); final AnimationController controller = AnimationController(vsync: const TestVSync());
......
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