From d1bde2eddea0c3770716f0e25d041e8a20fc6d00 Mon Sep 17 00:00:00 2001 From: Frederik Schweiger Date: Thu, 24 Jan 2019 18:30:45 +0100 Subject: [PATCH] Add reverse functionality to repeat() of AnimationController (#25125) --- AUTHORS | 3 +- .../src/animation/animation_controller.dart | 33 +++++-- .../animation/animation_controller_test.dart | 87 +++++++++++++++++++ 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index f033b44868..265c7daf4b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -34,4 +34,5 @@ Jasper van Riet Mattijs Fuijkschot TruongSinh Tran-Nguyen Sander Dalby Larsen -Marco Scannadinari \ No newline at end of file +Marco Scannadinari +Frederik Schweiger diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index 9623bdc9ca..c279f32a4c 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -579,7 +579,11 @@ class AnimationController extends Animation /// 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 @@ class AnimationController extends Animation /// 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 @@ class AnimationController extends Animation } 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 _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 diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart index a2a180783a..662a59a883 100644 --- a/packages/flutter/test/animation/animation_controller_test.dart +++ b/packages/flutter/test/animation/animation_controller_test.dart @@ -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());