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