Add reverse functionality to repeat() of AnimationController (#25125)
diff --git a/AUTHORS b/AUTHORS
index f033b44..265c7da 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -34,4 +34,5 @@
Mattijs Fuijkschot <mattijs.fuijkschot@gmail.com>
TruongSinh Tran-Nguyen <i@truongsinh.pro>
Sander Dalby Larsen <srdlarsen@gmail.com>
-Marco Scannadinari <m@scannadinari.co.uk>
\ No newline at end of file
+Marco Scannadinari <m@scannadinari.co.uk>
+Frederik Schweiger <mail@flschweiger.net>
diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart
index 9623bdc..c279f32 100644
--- a/packages/flutter/lib/src/animation/animation_controller.dart
+++ b/packages/flutter/lib/src/animation/animation_controller.dart
@@ -579,7 +579,11 @@
/// 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]).
@@ -587,7 +591,7 @@
/// 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;
@@ -602,7 +606,10 @@
}
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]
@@ -789,21 +796,33 @@
}
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
diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart
index a2a1807..662a59a 100644
--- a/packages/flutter/test/animation/animation_controller_test.dart
+++ b/packages/flutter/test/animation/animation_controller_test.dart
@@ -581,6 +581,93 @@
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());