Add reverseDuration to AnimationController (#32730)

This adds a reverseDuration parameter to AnimationController so that the animation has a different duration when going in reverse as it does going forward.
diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart
index 9c9cca7..866481b 100644
--- a/packages/flutter/lib/src/animation/animation_controller.dart
+++ b/packages/flutter/lib/src/animation/animation_controller.dart
@@ -231,6 +231,7 @@
   AnimationController({
     double value,
     this.duration,
+    this.reverseDuration,
     this.debugLabel,
     this.lowerBound = 0.0,
     this.upperBound = 1.0,
@@ -264,6 +265,7 @@
   AnimationController.unbounded({
     double value = 0.0,
     this.duration,
+    this.reverseDuration,
     this.debugLabel,
     @required TickerProvider vsync,
     this.animationBehavior = AnimationBehavior.preserve,
@@ -300,8 +302,17 @@
   Animation<double> get view => this;
 
   /// The length of time this animation should last.
+  ///
+  /// If [reverseDuration] is specified, then [duration] is only used when going
+  /// [forward]. Otherwise, it specifies the duration going in both directions.
   Duration duration;
 
+  /// The length of time this animation should last when going in [reverse].
+  ///
+  /// The value of [duration] us used if [reverseDuration] is not specified or
+  /// set to null.
+  Duration reverseDuration;
+
   Ticker _ticker;
 
   /// Recreates the [Ticker] with the new [TickerProvider].
@@ -429,7 +440,7 @@
     assert(() {
       if (duration == null) {
         throw FlutterError(
-          'AnimationController.forward() called with no default Duration.\n'
+          'AnimationController.forward() called with no default duration.\n'
           'The "duration" property should be set, either in the constructor or later, before '
           'calling the forward() function.'
         );
@@ -460,10 +471,10 @@
   /// reached at the end of the animation.
   TickerFuture reverse({ double from }) {
     assert(() {
-      if (duration == null) {
+      if (duration == null && reverseDuration == null) {
         throw FlutterError(
-          'AnimationController.reverse() called with no default Duration.\n'
-          'The "duration" property should be set, either in the constructor or later, before '
+          'AnimationController.reverse() called with no default duration or reverseDuration.\n'
+          'The "duration" or "reverseDuration" property should be set, either in the constructor or later, before '
           'calling the reverse() function.'
         );
       }
@@ -541,11 +552,11 @@
     Duration simulationDuration = duration;
     if (simulationDuration == null) {
       assert(() {
-        if (this.duration == null) {
+        if ((this.duration == null && _direction == _AnimationDirection.reverse && reverseDuration == null) || this.duration == null) {
           throw FlutterError(
-            'AnimationController.animateTo() called with no explicit Duration and no default Duration.\n'
+            'AnimationController.animateTo() called with no explicit duration and no default duration or reverseDuration.\n'
             'Either the "duration" argument to the animateTo() method should be provided, or the '
-            '"duration" property should be set, either in the constructor or later, before '
+            '"duration" and/or "reverseDuration" property should be set, either in the constructor or later, before '
             'calling the animateTo() function.'
           );
         }
@@ -553,7 +564,11 @@
       }());
       final double range = upperBound - lowerBound;
       final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
-      simulationDuration = this.duration * remainingFraction;
+      final Duration directionDuration =
+        (_direction == _AnimationDirection.reverse && reverseDuration != null)
+        ? reverseDuration
+        : this.duration;
+      simulationDuration = directionDuration * remainingFraction;
     } else if (target == value) {
       // Already at target, don't animate.
       simulationDuration = Duration.zero;
diff --git a/packages/flutter/lib/src/rendering/animated_size.dart b/packages/flutter/lib/src/rendering/animated_size.dart
index c9456b8..9d0b68e 100644
--- a/packages/flutter/lib/src/rendering/animated_size.dart
+++ b/packages/flutter/lib/src/rendering/animated_size.dart
@@ -76,6 +76,7 @@
   RenderAnimatedSize({
     @required TickerProvider vsync,
     @required Duration duration,
+    Duration reverseDuration,
     Curve curve = Curves.linear,
     AlignmentGeometry alignment = Alignment.center,
     TextDirection textDirection,
@@ -88,6 +89,7 @@
     _controller = AnimationController(
       vsync: vsync,
       duration: duration,
+      reverseDuration: reverseDuration,
     )..addListener(() {
       if (_controller.value != _lastValue)
         markNeedsLayout();
@@ -120,6 +122,14 @@
     _controller.duration = value;
   }
 
+  /// The duration of the animation when running in reverse.
+  Duration get reverseDuration => _controller.reverseDuration;
+  set reverseDuration(Duration value) {
+    if (value == _controller.reverseDuration)
+      return;
+    _controller.reverseDuration = value;
+  }
+
   /// The curve of the animation.
   Curve get curve => _animation.curve;
   set curve(Curve value) {
diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart
index ccb2a5b..26f95a3 100644
--- a/packages/flutter/lib/src/widgets/animated_cross_fade.dart
+++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart
@@ -124,6 +124,7 @@
     this.alignment = Alignment.topCenter,
     @required this.crossFadeState,
     @required this.duration,
+    this.reverseDuration,
     this.layoutBuilder = defaultLayoutBuilder,
   }) : assert(firstChild != null),
        assert(secondChild != null),
@@ -154,6 +155,11 @@
   /// The duration of the whole orchestrated animation.
   final Duration duration;
 
+  /// The duration of the whole orchestrated animation when running in reverse.
+  ///
+  /// If not supplied, this defaults to [duration].
+  final Duration reverseDuration;
+
   /// The fade curve of the first child.
   ///
   /// Defaults to [Curves.linear].
@@ -232,6 +238,8 @@
     super.debugFillProperties(properties);
     properties.add(EnumProperty<CrossFadeState>('crossFadeState', crossFadeState));
     properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: Alignment.topCenter));
+    properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
+    properties.add(IntProperty('reverseDuration', reverseDuration?.inMilliseconds, unit: 'ms', defaultValue: null));
   }
 }
 
@@ -243,7 +251,11 @@
   @override
   void initState() {
     super.initState();
-    _controller = AnimationController(duration: widget.duration, vsync: this);
+    _controller = AnimationController(
+      duration: widget.duration,
+      reverseDuration: widget.reverseDuration,
+      vsync: this,
+    );
     if (widget.crossFadeState == CrossFadeState.showSecond)
       _controller.value = 1.0;
     _firstAnimation = _initAnimation(widget.firstCurve, true);
@@ -274,6 +286,8 @@
     super.didUpdateWidget(oldWidget);
     if (widget.duration != oldWidget.duration)
       _controller.duration = widget.duration;
+    if (widget.reverseDuration != oldWidget.reverseDuration)
+      _controller.reverseDuration = widget.reverseDuration;
     if (widget.firstCurve != oldWidget.firstCurve)
       _firstAnimation = _initAnimation(widget.firstCurve, true);
     if (widget.secondCurve != oldWidget.secondCurve)
@@ -347,6 +361,7 @@
       child: AnimatedSize(
         alignment: widget.alignment,
         duration: widget.duration,
+        reverseDuration: widget.reverseDuration,
         curve: widget.sizeCurve,
         vsync: this,
         child: widget.layoutBuilder(topChild, topKey, bottomChild, bottomKey),
diff --git a/packages/flutter/lib/src/widgets/animated_size.dart b/packages/flutter/lib/src/widgets/animated_size.dart
index 8ef3bad..43e4ec3 100644
--- a/packages/flutter/lib/src/widgets/animated_size.dart
+++ b/packages/flutter/lib/src/widgets/animated_size.dart
@@ -20,6 +20,7 @@
     this.alignment = Alignment.center,
     this.curve = Curves.linear,
     @required this.duration,
+    this.reverseDuration,
     @required this.vsync,
   }) : super(key: key, child: child);
 
@@ -52,6 +53,12 @@
   /// size.
   final Duration duration;
 
+  /// The duration when transitioning this widget's size to match the child's
+  /// size when going in reverse.
+  ///
+  /// If not specified, defaults to [duration].
+  final Duration reverseDuration;
+
   /// The [TickerProvider] for this widget.
   final TickerProvider vsync;
 
@@ -60,6 +67,7 @@
     return RenderAnimatedSize(
       alignment: alignment,
       duration: duration,
+      reverseDuration: reverseDuration,
       curve: curve,
       vsync: vsync,
       textDirection: Directionality.of(context),
@@ -71,8 +79,17 @@
     renderObject
       ..alignment = alignment
       ..duration = duration
+      ..reverseDuration = reverseDuration
       ..curve = curve
       ..vsync = vsync
       ..textDirection = Directionality.of(context);
   }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: Alignment.topCenter));
+    properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
+    properties.add(IntProperty('reverseDuration', reverseDuration?.inMilliseconds, unit: 'ms', defaultValue: null));
+  }
 }
diff --git a/packages/flutter/lib/src/widgets/animated_switcher.dart b/packages/flutter/lib/src/widgets/animated_switcher.dart
index 9f3a1a1..92042c4 100644
--- a/packages/flutter/lib/src/widgets/animated_switcher.dart
+++ b/packages/flutter/lib/src/widgets/animated_switcher.dart
@@ -150,6 +150,7 @@
     Key key,
     this.child,
     @required this.duration,
+    this.reverseDuration,
     this.switchInCurve = Curves.linear,
     this.switchOutCurve = Curves.linear,
     this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,
@@ -177,11 +178,20 @@
   /// The duration of the transition from the old [child] value to the new one.
   ///
   /// This duration is applied to the given [child] when that property is set to
-  /// a new child. The same duration is used when fading out. Changing
-  /// [duration] will not affect the durations of transitions already in
-  /// progress.
+  /// a new child. The same duration is used when fading out, unless
+  /// [reverseDuration] is set. Changing [duration] will not affect the
+  /// durations of transitions already in progress.
   final Duration duration;
 
+  /// The duration of the transition from the new [child] value to the old one.
+  ///
+  /// This duration is applied to the given [child] when that property is set to
+  /// a new child. Changing [reverseDuration] will not affect the durations of
+  /// transitions already in progress.
+  ///
+  /// If not set, then the value of [duration] is used by default.
+  final Duration reverseDuration;
+
   /// The animation curve to use when transitioning in a new [child].
   ///
   /// This curve is applied to the given [child] when that property is set to a
@@ -272,6 +282,13 @@
       alignment: Alignment.center,
     );
   }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
+    properties.add(IntProperty('reverseDuration', reverseDuration?.inMilliseconds, unit: 'ms', defaultValue: null));
+  }
 }
 
 class _AnimatedSwitcherState extends State<AnimatedSwitcher> with TickerProviderStateMixin {
@@ -333,6 +350,7 @@
       return;
     final AnimationController controller = AnimationController(
       duration: widget.duration,
+      reverseDuration: widget.reverseDuration,
       vsync: this,
     );
     final Animation<double> animation = CurvedAnimation(
diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart
index d9868f5..1714ef7 100644
--- a/packages/flutter/lib/src/widgets/implicit_animations.dart
+++ b/packages/flutter/lib/src/widgets/implicit_animations.dart
@@ -227,6 +227,7 @@
     Key key,
     this.curve = Curves.linear,
     @required this.duration,
+    this.reverseDuration,
   }) : assert(curve != null),
        assert(duration != null),
        super(key: key);
@@ -237,6 +238,12 @@
   /// The duration over which to animate the parameters of this container.
   final Duration duration;
 
+  /// The duration over which to animate the parameters of this container when
+  /// the animation is going in the reverse direction.
+  ///
+  /// Defaults to [duration] if not specified.
+  final Duration reverseDuration;
+
   @override
   ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState();
 
@@ -244,6 +251,7 @@
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     super.debugFillProperties(properties);
     properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
+    properties.add(IntProperty('reverseDuration', reverseDuration?.inMilliseconds, unit: 'ms', defaultValue: null));
   }
 }
 
@@ -280,6 +288,7 @@
     super.initState();
     _controller = AnimationController(
       duration: widget.duration,
+      reverseDuration: widget.reverseDuration,
       debugLabel: kDebugMode ? '${widget.toStringShort()}' : null,
       vsync: this,
     );
@@ -294,6 +303,7 @@
     if (widget.curve != oldWidget.curve)
       _updateCurve();
     _controller.duration = widget.duration;
+    _controller.reverseDuration = widget.reverseDuration;
     if (_constructTweens()) {
       forEachTween((Tween<dynamic> tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
         _updateTween(tween, targetValue);
@@ -489,6 +499,7 @@
     this.child,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : assert(margin == null || margin.isNonNegative),
        assert(padding == null || padding.isNonNegative),
        assert(decoration == null || decoration.debugAssertIsValid()),
@@ -503,7 +514,7 @@
           ? constraints?.tighten(width: width, height: height)
             ?? BoxConstraints.tightFor(width: width, height: height)
           : constraints,
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// The [child] contained by the container.
   ///
@@ -645,9 +656,10 @@
     this.child,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : assert(padding != null),
        assert(padding.isNonNegative),
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// The amount of space by which to inset the child.
   final EdgeInsetsGeometry padding;
@@ -716,8 +728,9 @@
     this.child,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : assert(alignment != null),
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// How to align the child.
   ///
@@ -816,9 +829,10 @@
     this.height,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : assert(left == null || right == null || width == null),
        assert(top == null || bottom == null || height == null),
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// Creates a widget that animates the rectangle it occupies implicitly.
   ///
@@ -829,13 +843,14 @@
     Rect rect,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : left = rect.left,
        top = rect.top,
        width = rect.width,
        height = rect.height,
        right = null,
        bottom = null,
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// The widget below this widget in the tree.
   ///
@@ -967,9 +982,10 @@
     this.height,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : assert(start == null || end == null || width == null),
        assert(top == null || bottom == null || height == null),
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// The widget below this widget in the tree.
   ///
@@ -1121,8 +1137,9 @@
     @required this.opacity,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// The widget below this widget in the tree.
   ///
@@ -1196,12 +1213,13 @@
     this.maxLines,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : assert(style != null),
        assert(child != null),
        assert(softWrap != null),
        assert(overflow != null),
        assert(maxLines == null || maxLines > 0),
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// The widget below this widget in the tree.
   ///
@@ -1311,6 +1329,7 @@
     this.animateShadowColor = true,
     Curve curve = Curves.linear,
     @required Duration duration,
+    Duration reverseDuration,
   }) : assert(child != null),
        assert(shape != null),
        assert(clipBehavior != null),
@@ -1320,7 +1339,7 @@
        assert(shadowColor != null),
        assert(animateColor != null),
        assert(animateShadowColor != null),
-       super(key: key, curve: curve, duration: duration);
+       super(key: key, curve: curve, duration: duration, reverseDuration: reverseDuration);
 
   /// The widget below this widget in the tree.
   ///
diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart
index 662a59a..8efa674 100644
--- a/packages/flutter/test/animation/animation_controller_test.dart
+++ b/packages/flutter/test/animation/animation_controller_test.dart
@@ -4,6 +4,7 @@
 
 import 'dart:ui' as ui;
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/physics.dart';
 import 'package:flutter/semantics.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -140,6 +141,69 @@
     expect(controller.value, equals(0.0));
   });
 
+  test('Forward and reverse with different durations', () {
+    AnimationController controller = AnimationController(
+      duration: const Duration(milliseconds: 100),
+      reverseDuration: const Duration(milliseconds: 50),
+      vsync: const TestVSync(),
+    );
+
+    controller.forward();
+    tick(const Duration(milliseconds: 10));
+    tick(const Duration(milliseconds: 30));
+    expect(controller.value, closeTo(0.2, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 60));
+    expect(controller.value, closeTo(0.5, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 90));
+    expect(controller.value, closeTo(0.8, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 120));
+    expect(controller.value, closeTo(1.0, precisionErrorTolerance));
+    controller.stop();
+
+    controller.reverse();
+    tick(const Duration(milliseconds: 210));
+    tick(const Duration(milliseconds: 220));
+    expect(controller.value, closeTo(0.8, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 230));
+    expect(controller.value, closeTo(0.6, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 240));
+    expect(controller.value, closeTo(0.4, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 260));
+    expect(controller.value, closeTo(0.0, precisionErrorTolerance));
+    controller.stop();
+
+    // Swap which duration is longer.
+    controller = AnimationController(
+      duration: const Duration(milliseconds: 50),
+      reverseDuration: const Duration(milliseconds: 100),
+      vsync: const TestVSync(),
+    );
+
+    controller.forward();
+    tick(const Duration(milliseconds: 10));
+    tick(const Duration(milliseconds: 30));
+    expect(controller.value, closeTo(0.4, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 60));
+    expect(controller.value, closeTo(1.0, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 90));
+    expect(controller.value, closeTo(1.0, precisionErrorTolerance));
+    controller.stop();
+
+    controller.reverse();
+    tick(const Duration(milliseconds: 210));
+    tick(const Duration(milliseconds: 220));
+    expect(controller.value, closeTo(0.9, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 230));
+    expect(controller.value, closeTo(0.8, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 240));
+    expect(controller.value, closeTo(0.7, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 260));
+    expect(controller.value, closeTo(0.5, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 310));
+    expect(controller.value, closeTo(0.0, precisionErrorTolerance));
+    controller.stop();
+  });
+
   test('Forward only from value', () {
     final AnimationController controller = AnimationController(
       duration: const Duration(milliseconds: 100),
diff --git a/packages/flutter/test/animation/animations_test.dart b/packages/flutter/test/animation/animations_test.dart
index 6dfd76a..8d86c43 100644
--- a/packages/flutter/test/animation/animations_test.dart
+++ b/packages/flutter/test/animation/animations_test.dart
@@ -2,10 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' as ui;
+
+import 'package:flutter/foundation.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter/animation.dart';
 import 'package:flutter/widgets.dart';
 
+import '../scheduler/scheduler_tester.dart';
+
 class BogusCurve extends Curve {
   @override
   double transform(double t) => 100.0;
@@ -15,6 +20,8 @@
   setUp(() {
     WidgetsFlutterBinding.ensureInitialized();
     WidgetsBinding.instance.resetEpoch();
+    ui.window.onBeginFrame = null;
+    ui.window.onDrawFrame = null;
   });
 
   test('toString control test', () {
@@ -235,6 +242,102 @@
     expect(() { curved.value; }, throwsFlutterError);
   });
 
+  test('CurvedAnimation running with different forward and reverse durations.', () {
+    final AnimationController controller = AnimationController(
+      duration: const Duration(milliseconds: 100),
+      reverseDuration: const Duration(milliseconds: 50),
+      vsync: const TestVSync(),
+    );
+    final CurvedAnimation curved = CurvedAnimation(parent: controller, curve: Curves.linear, reverseCurve: Curves.linear);
+
+    controller.forward();
+    tick(const Duration(milliseconds: 0));
+    tick(const Duration(milliseconds: 10));
+    expect(curved.value, closeTo(0.1, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 20));
+    expect(curved.value, closeTo(0.2, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 30));
+    expect(curved.value, closeTo(0.3, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 40));
+    expect(curved.value, closeTo(0.4, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 50));
+    expect(curved.value, closeTo(0.5, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 60));
+    expect(curved.value, closeTo(0.6, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 70));
+    expect(curved.value, closeTo(0.7, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 80));
+    expect(curved.value, closeTo(0.8, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 90));
+    expect(curved.value, closeTo(0.9, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 100));
+    expect(curved.value, closeTo(1.0, precisionErrorTolerance));
+    controller.reverse();
+    tick(const Duration(milliseconds: 110));
+    expect(curved.value, closeTo(1.0, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 120));
+    expect(curved.value, closeTo(0.8, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 130));
+    expect(curved.value, closeTo(0.6, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 140));
+    expect(curved.value, closeTo(0.4, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 150));
+    expect(curved.value, closeTo(0.2, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 160));
+    expect(curved.value, closeTo(0.0, precisionErrorTolerance));
+  });
+
+  test('ReverseAnimation running with different forward and reverse durations.', () {
+    final AnimationController controller = AnimationController(
+      duration: const Duration(milliseconds: 100),
+      reverseDuration: const Duration(milliseconds: 50),
+      vsync: const TestVSync(),
+    );
+    final ReverseAnimation reversed = ReverseAnimation(
+      CurvedAnimation(
+        parent: controller,
+        curve: Curves.linear,
+        reverseCurve: Curves.linear,
+      ),
+    );
+
+    controller.forward();
+    tick(const Duration(milliseconds: 0));
+    tick(const Duration(milliseconds: 10));
+    expect(reversed.value, closeTo(0.9, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 20));
+    expect(reversed.value, closeTo(0.8, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 30));
+    expect(reversed.value, closeTo(0.7, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 40));
+    expect(reversed.value, closeTo(0.6, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 50));
+    expect(reversed.value, closeTo(0.5, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 60));
+    expect(reversed.value, closeTo(0.4, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 70));
+    expect(reversed.value, closeTo(0.3, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 80));
+    expect(reversed.value, closeTo(0.2, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 90));
+    expect(reversed.value, closeTo(0.1, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 100));
+    expect(reversed.value, closeTo(0.0, precisionErrorTolerance));
+    controller.reverse();
+    tick(const Duration(milliseconds: 110));
+    expect(reversed.value, closeTo(0.0, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 120));
+    expect(reversed.value, closeTo(0.2, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 130));
+    expect(reversed.value, closeTo(0.4, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 140));
+    expect(reversed.value, closeTo(0.6, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 150));
+    expect(reversed.value, closeTo(0.8, precisionErrorTolerance));
+    tick(const Duration(milliseconds: 160));
+    expect(reversed.value, closeTo(1.0, precisionErrorTolerance));
+  });
+
   test('TweenSequence', () {
     final AnimationController controller = AnimationController(
       vsync: const TestVSync(),