diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index 9c9cca72ad..866481b14d 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -231,6 +231,7 @@ class AnimationController extends Animation AnimationController({ double value, this.duration, + this.reverseDuration, this.debugLabel, this.lowerBound = 0.0, this.upperBound = 1.0, @@ -264,6 +265,7 @@ class AnimationController extends Animation AnimationController.unbounded({ double value = 0.0, this.duration, + this.reverseDuration, this.debugLabel, @required TickerProvider vsync, this.animationBehavior = AnimationBehavior.preserve, @@ -300,8 +302,17 @@ class AnimationController extends Animation Animation 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 @@ class AnimationController extends Animation 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 @@ class AnimationController extends Animation /// 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 @@ class AnimationController extends Animation 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 @@ class AnimationController extends Animation }()); 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 c9456b8fff..9d0b68efc8 100644 --- a/packages/flutter/lib/src/rendering/animated_size.dart +++ b/packages/flutter/lib/src/rendering/animated_size.dart @@ -76,6 +76,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { RenderAnimatedSize({ @required TickerProvider vsync, @required Duration duration, + Duration reverseDuration, Curve curve = Curves.linear, AlignmentGeometry alignment = Alignment.center, TextDirection textDirection, @@ -88,6 +89,7 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { _controller = AnimationController( vsync: vsync, duration: duration, + reverseDuration: reverseDuration, )..addListener(() { if (_controller.value != _lastValue) markNeedsLayout(); @@ -120,6 +122,14 @@ class RenderAnimatedSize extends RenderAligningShiftedBox { _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 ccb2a5bf45..26f95a3c45 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 @@ class AnimatedCrossFade extends StatefulWidget { 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 @@ class AnimatedCrossFade extends StatefulWidget { /// 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 @@ class AnimatedCrossFade extends StatefulWidget { super.debugFillProperties(properties); properties.add(EnumProperty('crossFadeState', crossFadeState)); properties.add(DiagnosticsProperty('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 @@ class _AnimatedCrossFadeState extends State with TickerProvid @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 @@ class _AnimatedCrossFadeState extends State with TickerProvid 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 @@ class _AnimatedCrossFadeState extends State with TickerProvid 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 8ef3badce5..43e4ec33b0 100644 --- a/packages/flutter/lib/src/widgets/animated_size.dart +++ b/packages/flutter/lib/src/widgets/animated_size.dart @@ -20,6 +20,7 @@ class AnimatedSize extends SingleChildRenderObjectWidget { 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 @@ class AnimatedSize extends SingleChildRenderObjectWidget { /// 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 @@ class AnimatedSize extends SingleChildRenderObjectWidget { return RenderAnimatedSize( alignment: alignment, duration: duration, + reverseDuration: reverseDuration, curve: curve, vsync: vsync, textDirection: Directionality.of(context), @@ -71,8 +79,17 @@ class AnimatedSize extends SingleChildRenderObjectWidget { 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('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 9f3a1a19c0..92042c4445 100644 --- a/packages/flutter/lib/src/widgets/animated_switcher.dart +++ b/packages/flutter/lib/src/widgets/animated_switcher.dart @@ -150,6 +150,7 @@ class AnimatedSwitcher extends StatefulWidget { 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 @@ class AnimatedSwitcher extends StatefulWidget { /// 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 @@ class AnimatedSwitcher extends StatefulWidget { 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 with TickerProviderStateMixin { @@ -333,6 +350,7 @@ class _AnimatedSwitcherState extends State with TickerProvider return; final AnimationController controller = AnimationController( duration: widget.duration, + reverseDuration: widget.reverseDuration, vsync: this, ); final Animation animation = CurvedAnimation( diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index d9868f590e..1714ef76f3 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -227,6 +227,7 @@ abstract class ImplicitlyAnimatedWidget extends StatefulWidget { Key key, this.curve = Curves.linear, @required this.duration, + this.reverseDuration, }) : assert(curve != null), assert(duration != null), super(key: key); @@ -237,6 +238,12 @@ abstract class ImplicitlyAnimatedWidget extends StatefulWidget { /// 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 createState(); @@ -244,6 +251,7 @@ abstract class ImplicitlyAnimatedWidget extends StatefulWidget { 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 @@ abstract class ImplicitlyAnimatedWidgetState super.initState(); _controller = AnimationController( duration: widget.duration, + reverseDuration: widget.reverseDuration, debugLabel: kDebugMode ? '${widget.toStringShort()}' : null, vsync: this, ); @@ -294,6 +303,7 @@ abstract class ImplicitlyAnimatedWidgetState if (widget.curve != oldWidget.curve) _updateCurve(); _controller.duration = widget.duration; + _controller.reverseDuration = widget.reverseDuration; if (_constructTweens()) { forEachTween((Tween tween, dynamic targetValue, TweenConstructor constructor) { _updateTween(tween, targetValue); @@ -489,6 +499,7 @@ class AnimatedContainer extends ImplicitlyAnimatedWidget { 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 @@ class AnimatedContainer extends ImplicitlyAnimatedWidget { ? 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 @@ class AnimatedPadding extends ImplicitlyAnimatedWidget { 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 @@ class AnimatedAlign extends ImplicitlyAnimatedWidget { 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 @@ class AnimatedPositioned extends ImplicitlyAnimatedWidget { 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 @@ class AnimatedPositioned extends ImplicitlyAnimatedWidget { 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 @@ class AnimatedPositionedDirectional extends ImplicitlyAnimatedWidget { 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 @@ class AnimatedOpacity extends ImplicitlyAnimatedWidget { @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 @@ class AnimatedDefaultTextStyle extends ImplicitlyAnimatedWidget { 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 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { 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 @@ class AnimatedPhysicalModel extends ImplicitlyAnimatedWidget { 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 662a59a883..8efa6740f1 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 @@ void main() { 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 6dfd76a37f..8d86c431d9 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 @@ void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); WidgetsBinding.instance.resetEpoch(); + ui.window.onBeginFrame = null; + ui.window.onDrawFrame = null; }); test('toString control test', () { @@ -235,6 +242,102 @@ void main() { 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(),