Fix AnimationStatus for repeat(reverse: true) and animateWith (#37739)
diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart
index 408df4ee..29d0fa5 100644
--- a/packages/flutter/lib/src/animation/animation_controller.dart
+++ b/packages/flutter/lib/src/animation/animation_controller.dart
@@ -597,7 +597,9 @@
/// [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.
+ /// the starting value will alternate between [min] and [max] values on each
+ /// repeat. The [status] will be reported as [AnimationStatus.reverse] when
+ /// the animation runs from [max] to [min].
///
/// Returns a [TickerFuture] that never completes. The [TickerFuture.orCancel] future
/// completes with an error when the animation is stopped (e.g. with [stop]).
@@ -623,7 +625,16 @@
assert(max >= min);
assert(max <= upperBound && min >= lowerBound);
assert(reverse != null);
- return animateWith(_RepeatingSimulation(_value, min, max, reverse, period));
+ stop();
+ return _startSimulation(_RepeatingSimulation(_value, min, max, reverse, period, _directionSetter));
+ }
+
+ void _directionSetter(_AnimationDirection direction) {
+ _direction = direction;
+ _status = (_direction == _AnimationDirection.forward) ?
+ AnimationStatus.forward :
+ AnimationStatus.reverse;
+ _checkStatusChanged();
}
/// Drives the animation with a critically damped spring (within [lowerBound]
@@ -656,7 +667,8 @@
}
final Simulation simulation = SpringSimulation(_kFlingSpringDescription, value, target, velocity * scale)
..tolerance = _kFlingTolerance;
- return animateWith(simulation);
+ stop();
+ return _startSimulation(simulation);
}
/// Drives the animation according to the given simulation.
@@ -666,6 +678,9 @@
/// 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.
+ ///
+ /// The [status] is always [AnimationStatus.forward] for the entire duration
+ /// of the simulation.
TickerFuture animateWith(Simulation simulation) {
assert(
_ticker != null,
@@ -673,6 +688,7 @@
'AnimationController methods should not be used after calling dispose.'
);
stop();
+ _direction = _AnimationDirection.forward;
return _startSimulation(simulation);
}
@@ -812,8 +828,10 @@
bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
}
+typedef _DirectionSetter = void Function(_AnimationDirection direction);
+
class _RepeatingSimulation extends Simulation {
- _RepeatingSimulation(double initialValue, this.min, this.max, this.reverse, Duration period)
+ _RepeatingSimulation(double initialValue, this.min, this.max, this.reverse, Duration period, this.directionSetter)
: _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond,
_initialT = (max == min) ? 0.0 : (initialValue / (max - min)) * (period.inMicroseconds / Duration.microsecondsPerSecond) {
assert(_periodInSeconds > 0.0);
@@ -823,6 +841,7 @@
final double min;
final double max;
final bool reverse;
+ final _DirectionSetter directionSetter;
final double _periodInSeconds;
final double _initialT;
@@ -836,8 +855,10 @@
final bool _isPlayingReverse = (totalTimeInSeconds ~/ _periodInSeconds) % 2 == 1;
if (reverse && _isPlayingReverse) {
+ directionSetter(_AnimationDirection.reverse);
return ui.lerpDouble(max, min, t);
} else {
+ directionSetter(_AnimationDirection.forward);
return ui.lerpDouble(min, max, t);
}
}
diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart
index 34c86fa..15796ee 100644
--- a/packages/flutter/test/animation/animation_controller_test.dart
+++ b/packages/flutter/test/animation/animation_controller_test.dart
@@ -846,6 +846,69 @@
expect(() => controller.stop(), throwsAssertionError);
expect(() => controller.forward(), throwsAssertionError);
expect(() => controller.reverse(), throwsAssertionError);
-
});
+
+ test('Simulations run forward', () {
+ final List<AnimationStatus> statuses = <AnimationStatus>[];
+ final AnimationController controller = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(seconds: 1),
+ )..addStatusListener((AnimationStatus status) {
+ statuses.add(status);
+ });
+
+ controller.animateWith(TestSimulation());
+ tick(const Duration(milliseconds: 0));
+ tick(const Duration(seconds: 2));
+ expect(statuses, <AnimationStatus>[AnimationStatus.forward]);
+ });
+
+ test('Simulations run forward even after a reverse run', () {
+ final List<AnimationStatus> statuses = <AnimationStatus>[];
+ final AnimationController controller = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(seconds: 1),
+ )..addStatusListener((AnimationStatus status) {
+ statuses.add(status);
+ });
+ controller.reverse(from: 1.0);
+ tick(const Duration(milliseconds: 0));
+ tick(const Duration(seconds: 2));
+ expect(statuses, <AnimationStatus>[AnimationStatus.completed, AnimationStatus.reverse, AnimationStatus.dismissed]);
+ statuses.clear();
+
+ controller.animateWith(TestSimulation());
+ tick(const Duration(milliseconds: 0));
+ tick(const Duration(seconds: 2));
+ expect(statuses, <AnimationStatus>[AnimationStatus.forward]);
+ });
+
+ test('Repeating animation with reverse: true report as forward and reverse', () {
+ final List<AnimationStatus> statuses = <AnimationStatus>[];
+ final AnimationController controller = AnimationController(
+ vsync: const TestVSync(),
+ duration: const Duration(seconds: 1),
+ )..addStatusListener((AnimationStatus status) {
+ statuses.add(status);
+ });
+
+ controller.repeat(reverse: true);
+ tick(const Duration(milliseconds: 0));
+ tick(const Duration(milliseconds: 999));
+ expect(statuses, <AnimationStatus>[AnimationStatus.forward]);
+ statuses.clear();
+ tick(const Duration(seconds: 1));
+ expect(statuses, <AnimationStatus>[AnimationStatus.reverse]);
+ });
+}
+
+class TestSimulation extends Simulation {
+ @override
+ double dx(double time) => time;
+
+ @override
+ bool isDone(double time) => false;
+
+ @override
+ double x(double time) => time;
}