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());