Revert "[PageTransitionsBuilder] Fix 'ZoomPageTransition' built more than once (#58686)" (#59992)

This reverts commit fe15d1e793cb509f0d616caf1eab19477262b804.
diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart
index 05ab2b6..2b2c523 100644
--- a/packages/flutter/lib/src/material/page_transitions_theme.dart
+++ b/packages/flutter/lib/src/material/page_transitions_theme.dart
@@ -150,19 +150,19 @@
 
 // Zooms and fades a new page in, zooming out the previous page. This transition
 // is designed to match the Android 10 activity transition.
-class _ZoomPageTransition extends StatelessWidget {
-  /// Creates a [_ZoomPageTransition].
-  ///
-  /// The [animation] and [secondaryAnimation] argument are required and must
-  /// not be null.
+class _ZoomPageTransition extends StatefulWidget {
   const _ZoomPageTransition({
     Key key,
-    @required this.animation,
-    @required this.secondaryAnimation,
+    this.animation,
+    this.secondaryAnimation,
     this.child,
-  }) : assert(animation != null),
-       assert(secondaryAnimation != null),
-       super(key: key);
+  }) : super(key: key);
+
+  // The scrim obscures the old page by becoming increasingly opaque.
+  static final Tween<double> _scrimOpacityTween = Tween<double>(
+    begin: 0.0,
+    end: 0.60,
+  );
 
   // A curve sequence that is similar to the 'fastOutExtraSlowIn' curve used in
   // the native transition.
@@ -179,207 +179,132 @@
     ),
   ];
   static final TweenSequence<double> _scaleCurveSequence = TweenSequence<double>(fastOutExtraSlowInTweenSequenceItems);
+  static final FlippedTweenSequence _flippedScaleCurveSequence = FlippedTweenSequence(fastOutExtraSlowInTweenSequenceItems);
 
-  /// The animation that drives the [child]'s entrance and exit.
-  ///
-  /// See also:
-  ///
-  ///  * [TransitionRoute.animation], which is the value given to this property
-  ///    when the [_ZoomPageTransition] is used as a page transition.
   final Animation<double> animation;
-
-  /// The animation that transitions [child] when new content is pushed on top
-  /// of it.
-  ///
-  /// See also:
-  ///
-  ///  * [TransitionRoute.secondaryAnimation], which is the value given to this
-  //     property when the [_ZoomPageTransition] is used as a page transition.
   final Animation<double> secondaryAnimation;
-
-  /// The widget below this widget in the tree.
-  ///
-  /// This widget will transition in and out as driven by [animation] and
-  /// [secondaryAnimation].
   final Widget child;
 
   @override
-  Widget build(BuildContext context) {
-    return DualTransitionBuilder(
-      animation: animation,
-      forwardBuilder: (
-        BuildContext context,
-        Animation<double> animation,
-        Widget child,
-      ) {
-        return _ZoomEnterTransition(
-          animation: animation,
-          child: child,
-        );
-      },
-      reverseBuilder: (
-        BuildContext context,
-        Animation<double> animation,
-        Widget child,
-      ) {
-        return _ZoomExitTransition(
-          animation: animation,
-          reverse: true,
-          child: child,
-        );
-      },
-      child: DualTransitionBuilder(
-        animation: ReverseAnimation(secondaryAnimation),
-        forwardBuilder: (
-          BuildContext context,
-          Animation<double> animation,
-          Widget child,
-        ) {
-          return _ZoomEnterTransition(
-            animation: animation,
-            reverse: true,
-            child: child,
-          );
-        },
-        reverseBuilder: (
-          BuildContext context,
-          Animation<double> animation,
-          Widget child,
-        ) {
-          return _ZoomExitTransition(
-            animation: animation,
-            child: child,
-          );
-        },
-        child: child,
-      ),
-    );
-  }
+  __ZoomPageTransitionState createState() => __ZoomPageTransitionState();
 }
 
-class _ZoomEnterTransition extends StatelessWidget {
-  const _ZoomEnterTransition({
-    Key key,
-    @required this.animation,
-    this.reverse = false,
-    this.child,
-  }) : assert(animation != null),
-       assert(reverse != null),
-       super(key: key);
+class __ZoomPageTransitionState extends State<_ZoomPageTransition> {
+  AnimationStatus _currentAnimationStatus;
+  AnimationStatus _lastAnimationStatus;
 
-  final Animation<double> animation;
-  final Widget child;
-  final bool reverse;
+  @override
+  void initState() {
+    super.initState();
+    widget.animation.addStatusListener((AnimationStatus animationStatus) {
+      _lastAnimationStatus = _currentAnimationStatus;
+      _currentAnimationStatus = animationStatus;
+    });
+  }
 
-  static final Animatable<double> _fadeInTransition = Tween<double>(
-    begin: 0.0,
-    end: 1.00,
-  ).chain(CurveTween(curve: const Interval(0.125, 0.250)));
+  // This check ensures that the animation reverses the original animation if
+  // the transition were interrupted midway. This prevents a disjointed
+  // experience since the reverse animation uses different fade and scaling
+  // curves.
+  bool get _transitionWasInterrupted {
+    bool wasInProgress = false;
+    bool isInProgress = false;
 
-  static final Animatable<double> _scaleDownTransition = Tween<double>(
-    begin: 1.10,
-    end: 1.00,
-  ).chain(_ZoomPageTransition._scaleCurveSequence);
-
-  static final Animatable<double> _scaleUpTransition = Tween<double>(
-    begin: 0.85,
-    end: 1.00,
-  ).chain(_ZoomPageTransition._scaleCurveSequence);
-
-  static final Animatable<double> _scrimOpacityTween = Tween<double>(
-    begin: 0.0,
-    end: 0.60,
-  ).chain(CurveTween(curve: const Interval(0.2075, 0.4175)));
+    switch (_currentAnimationStatus) {
+      case AnimationStatus.completed:
+      case AnimationStatus.dismissed:
+        isInProgress = false;
+        break;
+      case AnimationStatus.forward:
+      case AnimationStatus.reverse:
+        isInProgress = true;
+        break;
+    }
+    switch (_lastAnimationStatus) {
+      case AnimationStatus.completed:
+      case AnimationStatus.dismissed:
+        wasInProgress = false;
+        break;
+      case AnimationStatus.forward:
+      case AnimationStatus.reverse:
+        wasInProgress = true;
+        break;
+    }
+    return wasInProgress && isInProgress;
+  }
 
   @override
   Widget build(BuildContext context) {
-    double opacity = 0;
-    // The transition's scrim opacity only increases on the forward transition. In the reverse
-    // transition, the opacity should always be 0.0.
-    //
-    // Therefore, we need to only apply the scrim opacity animation when the transition
-    // is running forwards.
-    //
-    // The reason that we check that the animation's status is not `completed` instead
-    // of checking that it is `forward` is that this allows the interrupted reversal of the
-    // forward transition to smoothly fade the scrim away. This prevents a disjointed
-    // removal of the scrim.
-    if (!reverse && animation.status != AnimationStatus.completed) {
-      opacity = _scrimOpacityTween.evaluate(animation);
-    }
+    final Animation<double> _forwardScrimOpacityAnimation = widget.animation.drive(
+      _ZoomPageTransition._scrimOpacityTween
+        .chain(CurveTween(curve: const Interval(0.2075, 0.4175))));
 
-    final Animation<double> fadeTransition = reverse
-      ? kAlwaysCompleteAnimation
-      : _fadeInTransition.animate(animation);
+    final Animation<double> _forwardEndScreenScaleTransition = widget.animation.drive(
+      Tween<double>(begin: 0.85, end: 1.00)
+        .chain(_ZoomPageTransition._scaleCurveSequence));
 
-    final Animation<double> scaleTransition = (reverse
-      ? _scaleDownTransition
-      : _scaleUpTransition
-    ).animate(animation);
+    final Animation<double> _forwardStartScreenScaleTransition = widget.secondaryAnimation.drive(
+      Tween<double>(begin: 1.00, end: 1.05)
+        .chain(_ZoomPageTransition._scaleCurveSequence));
+
+    final Animation<double> _forwardEndScreenFadeTransition = widget.animation.drive(
+      Tween<double>(begin: 0.0, end: 1.00)
+        .chain(CurveTween(curve: const Interval(0.125, 0.250))));
+
+    final Animation<double> _reverseEndScreenScaleTransition = widget.secondaryAnimation.drive(
+      Tween<double>(begin: 1.00, end: 1.10)
+        .chain(_ZoomPageTransition._flippedScaleCurveSequence));
+
+    final Animation<double> _reverseStartScreenScaleTransition = widget.animation.drive(
+      Tween<double>(begin: 0.9, end: 1.0)
+        .chain(_ZoomPageTransition._flippedScaleCurveSequence));
+
+    final Animation<double> _reverseStartScreenFadeTransition = widget.animation.drive(
+      Tween<double>(begin: 0.0, end: 1.00)
+        .chain(CurveTween(curve: const Interval(1 - 0.2075, 1 - 0.0825))));
 
     return AnimatedBuilder(
-      animation: animation,
+      animation: widget.animation,
       builder: (BuildContext context, Widget child) {
-        return Container(
-          color: Colors.black.withOpacity(opacity),
-          child: child,
-        );
+        if (widget.animation.status == AnimationStatus.forward || _transitionWasInterrupted) {
+          return Container(
+            color: Colors.black.withOpacity(_forwardScrimOpacityAnimation.value),
+            child: FadeTransition(
+              opacity: _forwardEndScreenFadeTransition,
+              child: ScaleTransition(
+                scale: _forwardEndScreenScaleTransition,
+                child: child,
+              ),
+            ),
+          );
+        } else if (widget.animation.status == AnimationStatus.reverse) {
+          return ScaleTransition(
+            scale: _reverseStartScreenScaleTransition,
+            child: FadeTransition(
+              opacity: _reverseStartScreenFadeTransition,
+              child: child,
+            ),
+          );
+        }
+        return child;
       },
-      child: FadeTransition(
-        opacity: fadeTransition,
-        child: ScaleTransition(
-          scale: scaleTransition,
-          child: child,
-        ),
-      ),
-    );
-  }
-}
-
-class _ZoomExitTransition extends StatelessWidget {
-  const _ZoomExitTransition({
-    Key key,
-    @required this.animation,
-    this.reverse = false,
-    this.child,
-  }) : assert(animation != null),
-       assert(reverse != null),
-       super(key: key);
-
-  final Animation<double> animation;
-  final bool reverse;
-  final Widget child;
-
-  static final Animatable<double> _fadeOutTransition = Tween<double>(
-    begin: 1.0,
-    end: 0.0,
-  ).chain(CurveTween(curve: const Interval(0.0825, 0.2075)));
-
-  static final Animatable<double> _scaleUpTransition = Tween<double>(
-    begin: 1.00,
-    end: 1.05,
-  ).chain(_ZoomPageTransition._scaleCurveSequence);
-
-  static final Animatable<double> _scaleDownTransition = Tween<double>(
-    begin: 1.00,
-    end: 0.90,
-  ).chain(_ZoomPageTransition._scaleCurveSequence);
-
-  @override
-  Widget build(BuildContext context) {
-    final Animation<double> fadeTransition = reverse
-      ? _fadeOutTransition.animate(animation)
-      : kAlwaysCompleteAnimation;
-    final Animation<double> scaleTransition = (reverse
-      ? _scaleDownTransition
-      : _scaleUpTransition
-    ).animate(animation);
-
-    return FadeTransition(
-      opacity: fadeTransition,
-      child: ScaleTransition(
-        scale: scaleTransition,
-        child: child,
+      child: AnimatedBuilder(
+        animation: widget.secondaryAnimation,
+        builder: (BuildContext context, Widget child) {
+          if (widget.secondaryAnimation.status == AnimationStatus.forward || _transitionWasInterrupted) {
+            return ScaleTransition(
+              scale: _forwardStartScreenScaleTransition,
+              child: child,
+            );
+          } else if (widget.secondaryAnimation.status == AnimationStatus.reverse) {
+            return ScaleTransition(
+              scale: _reverseEndScreenScaleTransition,
+              child: child,
+            );
+          }
+          return child;
+        },
+        child: widget.child,
       ),
     );
   }
diff --git a/packages/flutter/lib/src/widgets/dual_transition_builder.dart b/packages/flutter/lib/src/widgets/dual_transition_builder.dart
deleted file mode 100644
index 239aa11..0000000
--- a/packages/flutter/lib/src/widgets/dual_transition_builder.dart
+++ /dev/null
@@ -1,203 +0,0 @@
-// Copyright 2014 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// @dart = 2.8
-
-import 'basic.dart';
-import 'framework.dart';
-
-/// Builder callback used by [DualTransitionBuilder].
-///
-/// The builder is expected to return a transition powered by the provided
-/// `animation` and wrapping the provided `child`.
-///
-/// The `animation` provided to the builder always runs forward from 0.0 to 1.0.
-typedef AnimatedTransitionBuilder = Widget Function(
-  BuildContext context,
-  Animation<double> animation,
-  Widget child,
-);
-
-/// A transition builder that animates its [child] based on the
-/// [AnimationStatus] of the provided [animation].
-///
-/// This widget can be used to specify different enter and exit transitions for
-/// a [child].
-///
-/// While the [animation] runs forward, the [child] is animated according to
-/// [forwardBuilder] and while the [animation] is running in reverse, it is
-/// animated according to [reverseBuilder].
-///
-/// Using this builder allows the widget tree to maintain its shape by nesting
-/// the enter and exit transitions. This ensures that no state information of
-/// any descendant widget is lost when the transition starts or completes.
-class DualTransitionBuilder extends StatefulWidget {
-  /// Creates a [DualTransitionBuilder].
-  ///
-  /// The [animation], [forwardBuilder], and [reverseBuilder] arguments are
-  /// required and must not be null.
-  const DualTransitionBuilder({
-    Key key,
-    @required this.animation,
-    @required this.forwardBuilder,
-    @required this.reverseBuilder,
-    this.child,
-  }) : assert(animation != null),
-       assert(forwardBuilder != null),
-       assert(reverseBuilder != null),
-       super(key: key);
-
-  /// The animation that drives the [child]'s transition.
-  ///
-  /// When this animation runs forward, the [child] transitions as specified by
-  /// [forwardBuilder]. When it runs in reverse, the child transitions according
-  /// to [reverseBuilder].
-  final Animation<double> animation;
-
-  /// A builder for the transition that makes [child] appear on screen.
-  ///
-  /// The [child] should be fully visible when the provided `animation` reaches
-  /// 1.0.
-  ///
-  /// The `animation` provided to this builder is running forward from 0.0 to
-  /// 1.0 when [animation] runs _forward_. When [animation] runs in reverse,
-  /// the given `animation` is set to [kAlwaysCompleteAnimation].
-  ///
-  /// See also:
-  ///
-  ///  * [reverseBuilder], which builds the transition for making the [child]
-  ///   disappear from the screen.
-  final AnimatedTransitionBuilder forwardBuilder;
-
-  /// A builder for a transition that makes [child] disappear from the screen.
-  ///
-  /// The [child] should be fully invisible when the provided `animation`
-  /// reaches 1.0.
-  ///
-  /// The `animation` provided to this builder is running forward from 0.0 to
-  /// 1.0 when [animation] runs in _reverse_. When [animation] runs forward,
-  /// the given `animation` is set to [kAlwaysDismissedAnimation].
-  ///
-  /// See also:
-  ///
-  ///  * [forwardBuilder], which builds the transition for making the [child]
-  ///    appear on screen.
-  final AnimatedTransitionBuilder reverseBuilder;
-
-  /// The widget below this [DualTransitionBuilder] in the tree.
-  ///
-  /// This child widget will be wrapped by the transitions built by
-  /// [forwardBuilder] and [reverseBuilder].
-  final Widget child;
-
-  @override
-  State<DualTransitionBuilder> createState() => _DualTransitionBuilderState();
-}
-
-class _DualTransitionBuilderState extends State<DualTransitionBuilder> {
-  AnimationStatus _effectiveAnimationStatus;
-  final ProxyAnimation _forwardAnimation = ProxyAnimation();
-  final ProxyAnimation _reverseAnimation = ProxyAnimation();
-
-  @override
-  void initState() {
-    super.initState();
-    _effectiveAnimationStatus = widget.animation.status;
-    widget.animation.addStatusListener(_animationListener);
-    _updateAnimations();
-  }
-
-  void _animationListener(AnimationStatus animationStatus) {
-    final AnimationStatus oldEffective = _effectiveAnimationStatus;
-    _effectiveAnimationStatus = _calculateEffectiveAnimationStatus(
-      lastEffective: _effectiveAnimationStatus,
-      current: animationStatus,
-    );
-    if (oldEffective != _effectiveAnimationStatus) {
-      _updateAnimations();
-    }
-  }
-
-  @override
-  void didUpdateWidget(DualTransitionBuilder oldWidget) {
-    super.didUpdateWidget(oldWidget);
-    if (oldWidget.animation != widget.animation) {
-      oldWidget.animation.removeStatusListener(_animationListener);
-      widget.animation.addStatusListener(_animationListener);
-      _animationListener(widget.animation.status);
-    }
-  }
-
-  // When a transition is interrupted midway we just want to play the ongoing
-  // animation in reverse. Switching to the actual reverse transition would
-  // yield a disjoint experience since the forward and reverse transitions are
-  // very different.
-  AnimationStatus _calculateEffectiveAnimationStatus({
-    @required AnimationStatus lastEffective,
-    @required AnimationStatus current,
-  }) {
-    assert(current != null);
-    assert(lastEffective != null);
-    switch (current) {
-      case AnimationStatus.dismissed:
-      case AnimationStatus.completed:
-        return current;
-      case AnimationStatus.forward:
-        switch (lastEffective) {
-          case AnimationStatus.dismissed:
-          case AnimationStatus.completed:
-          case AnimationStatus.forward:
-            return current;
-          case AnimationStatus.reverse:
-            return lastEffective;
-        }
-        break;
-      case AnimationStatus.reverse:
-        switch (lastEffective) {
-          case AnimationStatus.dismissed:
-          case AnimationStatus.completed:
-          case AnimationStatus.reverse:
-            return current;
-          case AnimationStatus.forward:
-            return lastEffective;
-        }
-        break;
-    }
-    return null; // unreachable
-  }
-
-  void _updateAnimations() {
-    switch (_effectiveAnimationStatus) {
-      case AnimationStatus.dismissed:
-      case AnimationStatus.forward:
-        _forwardAnimation.parent = widget.animation;
-        _reverseAnimation.parent = kAlwaysDismissedAnimation;
-        break;
-      case AnimationStatus.reverse:
-      case AnimationStatus.completed:
-        _forwardAnimation.parent = kAlwaysCompleteAnimation;
-        _reverseAnimation.parent = ReverseAnimation(widget.animation);
-        break;
-    }
-  }
-
-  @override
-  void dispose() {
-    widget.animation.removeStatusListener(_animationListener);
-    super.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return widget.forwardBuilder(
-      context,
-      _forwardAnimation,
-      widget.reverseBuilder(
-        context,
-        _reverseAnimation,
-        widget.child,
-      ),
-    );
-  }
-}
diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart
index 1805f6e..d8cc265 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -37,7 +37,6 @@
 export 'src/widgets/disposable_build_context.dart';
 export 'src/widgets/drag_target.dart';
 export 'src/widgets/draggable_scrollable_sheet.dart';
-export 'src/widgets/dual_transition_builder.dart';
 export 'src/widgets/editable_text.dart';
 export 'src/widgets/fade_in_image.dart';
 export 'src/widgets/focus_manager.dart';
diff --git a/packages/flutter/test/material/page_transitions_theme_test.dart b/packages/flutter/test/material/page_transitions_theme_test.dart
index ba1efc3..e823432 100644
--- a/packages/flutter/test/material/page_transitions_theme_test.dart
+++ b/packages/flutter/test/material/page_transitions_theme_test.dart
@@ -161,50 +161,4 @@
     expect(find.text('page b'), findsOneWidget);
     expect(findZoomPageTransition(), findsOneWidget);
   }, variant: TargetPlatformVariant.only(TargetPlatform.android));
-
-  testWidgets('_ZoomPageTransition only cause child widget built once', (WidgetTester tester) async {
-    // Regression test for https://github.com/flutter/flutter/issues/58345
-
-    int builtCount = 0;
-
-    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
-      '/': (BuildContext context) => Material(
-        child: FlatButton(
-          child: const Text('push'),
-          onPressed: () { Navigator.of(context).pushNamed('/b'); },
-        ),
-      ),
-      '/b': (BuildContext context) => StatefulBuilder(
-        builder: (BuildContext context, StateSetter setState) {
-          builtCount++; // Increase [builtCount] each time the widget build
-          return FlatButton(
-            child: const Text('pop'),
-            onPressed: () { Navigator.pop(context); },
-          );
-        },
-      ),
-    };
-
-    await tester.pumpWidget(
-      MaterialApp(
-        theme: ThemeData(
-          pageTransitionsTheme: const PageTransitionsTheme(
-            builders: <TargetPlatform, PageTransitionsBuilder>{
-              TargetPlatform.android: ZoomPageTransitionsBuilder(), // creates a _ZoomPageTransition
-            },
-          ),
-        ),
-        routes: routes,
-      ),
-    );
-
-    // No matter push or pop was called, the child widget should built only once.
-    await tester.tap(find.text('push'));
-    await tester.pumpAndSettle();
-    expect(builtCount, 1);
-
-    await tester.tap(find.text('pop'));
-    await tester.pumpAndSettle();
-    expect(builtCount, 1);
-  }, variant: TargetPlatformVariant.only(TargetPlatform.android));
 }
diff --git a/packages/flutter/test/widgets/dual_transition_builder_test.dart b/packages/flutter/test/widgets/dual_transition_builder_test.dart
deleted file mode 100644
index 398173c..0000000
--- a/packages/flutter/test/widgets/dual_transition_builder_test.dart
+++ /dev/null
@@ -1,302 +0,0 @@
-// Copyright 2014 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// @dart = 2.8
-
-import 'package:flutter_test/flutter_test.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
-
-import 'package:flutter/src/widgets/dual_transition_builder.dart';
-
-void main() {
-  testWidgets('runs animations', (WidgetTester tester) async {
-    final AnimationController controller = AnimationController(
-      vsync: const TestVSync(),
-      duration: const Duration(milliseconds: 300),
-    );
-
-    await tester.pumpWidget(Center(
-      child: DualTransitionBuilder(
-        animation: controller,
-        forwardBuilder: (
-          BuildContext context,
-          Animation<double> animation,
-          Widget child,
-        ) {
-          return ScaleTransition(
-            scale: animation,
-            child: child,
-          );
-        },
-        reverseBuilder: (
-          BuildContext context,
-          Animation<double> animation,
-          Widget child,
-        ) {
-          return FadeTransition(
-            opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
-            child: child,
-          );
-        },
-        child: Container(
-          color: Colors.green,
-          height: 100,
-          width: 100,
-        ),
-      ),
-    ));
-    expect(_getScale(tester), 0.0);
-    expect(_getOpacity(tester), 1.0);
-
-    controller.forward();
-    await tester.pump();
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(_getScale(tester), 0.5);
-    expect(_getOpacity(tester), 1.0);
-
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 1.0);
-
-    await tester.pumpAndSettle();
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 1.0);
-
-    controller.reverse();
-    await tester.pump();
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 0.5);
-
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 0.0);
-
-    await tester.pumpAndSettle();
-    expect(_getScale(tester), 0.0);
-    expect(_getOpacity(tester), 1.0);
-  });
-
-  testWidgets('keeps state', (WidgetTester tester) async {
-    final AnimationController controller = AnimationController(
-      vsync: const TestVSync(),
-      duration: const Duration(milliseconds: 300),
-    );
-
-    await tester.pumpWidget(Directionality(
-      textDirection: TextDirection.ltr,
-      child: Center(
-        child: DualTransitionBuilder(
-          animation: controller,
-          forwardBuilder: (
-            BuildContext context,
-            Animation<double> animation,
-            Widget child,
-          ) {
-            return ScaleTransition(
-              scale: animation,
-              child: child,
-            );
-          },
-          reverseBuilder: (
-            BuildContext context,
-            Animation<double> animation,
-            Widget child,
-          ) {
-            return FadeTransition(
-              opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
-              child: child,
-            );
-          },
-          child: const _StatefulTestWidget(name: 'Foo'),
-        ),
-      ),
-    ));
-    final State<StatefulWidget> state =
-        tester.state(find.byType(_StatefulTestWidget));
-    expect(state, isNotNull);
-
-    controller.forward();
-    await tester.pump();
-    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
-
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
-
-    await tester.pumpAndSettle();
-    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
-
-    controller.reverse();
-    await tester.pump();
-    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
-
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
-
-    await tester.pumpAndSettle();
-    expect(state, same(tester.state(find.byType(_StatefulTestWidget))));
-  });
-
-  testWidgets('does not jump when interrupted - forward',
-      (WidgetTester tester) async {
-    final AnimationController controller = AnimationController(
-      vsync: const TestVSync(),
-      duration: const Duration(milliseconds: 300),
-    );
-    await tester.pumpWidget(Center(
-      child: DualTransitionBuilder(
-        animation: controller,
-        forwardBuilder: (
-          BuildContext context,
-          Animation<double> animation,
-          Widget child,
-        ) {
-          return ScaleTransition(
-            scale: animation,
-            child: child,
-          );
-        },
-        reverseBuilder: (
-          BuildContext context,
-          Animation<double> animation,
-          Widget child,
-        ) {
-          return FadeTransition(
-            opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
-            child: child,
-          );
-        },
-        child: Container(
-          color: Colors.green,
-          height: 100,
-          width: 100,
-        ),
-      ),
-    ));
-    expect(_getScale(tester), 0.0);
-    expect(_getOpacity(tester), 1.0);
-
-    controller.forward();
-    await tester.pump();
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(_getScale(tester), 0.5);
-    expect(_getOpacity(tester), 1.0);
-
-    controller.reverse();
-    expect(_getScale(tester), 0.5);
-    expect(_getOpacity(tester), 1.0);
-    await tester.pump();
-    expect(_getScale(tester), 0.5);
-    expect(_getOpacity(tester), 1.0);
-
-    await tester.pump(const Duration(milliseconds: 75));
-    expect(_getScale(tester), 0.25);
-    expect(_getOpacity(tester), 1.0);
-
-    await tester.pump(const Duration(milliseconds: 75));
-    expect(_getScale(tester), 0.0);
-    expect(_getOpacity(tester), 1.0);
-
-    await tester.pumpAndSettle();
-    expect(_getScale(tester), 0.0);
-    expect(_getOpacity(tester), 1.0);
-  });
-
-  testWidgets('does not jump when interrupted - reverse',
-      (WidgetTester tester) async {
-    final AnimationController controller = AnimationController(
-      value: 1.0,
-      vsync: const TestVSync(),
-      duration: const Duration(milliseconds: 300),
-    );
-    await tester.pumpWidget(Center(
-      child: DualTransitionBuilder(
-        animation: controller,
-        forwardBuilder: (
-          BuildContext context,
-          Animation<double> animation,
-          Widget child,
-        ) {
-          return ScaleTransition(
-            scale: animation,
-            child: child,
-          );
-        },
-        reverseBuilder: (
-          BuildContext context,
-          Animation<double> animation,
-          Widget child,
-        ) {
-          return FadeTransition(
-            opacity: Tween<double>(begin: 1.0, end: 0.0).animate(animation),
-            child: child,
-          );
-        },
-        child: Container(
-          color: Colors.green,
-          height: 100,
-          width: 100,
-        ),
-      ),
-    ));
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 1.0);
-
-    controller.reverse();
-    await tester.pump();
-    await tester.pump(const Duration(milliseconds: 150));
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 0.5);
-
-    controller.forward();
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 0.5);
-    await tester.pump();
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 0.5);
-
-    await tester.pump(const Duration(milliseconds: 75));
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 0.75);
-
-    await tester.pump(const Duration(milliseconds: 75));
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 1.0);
-
-    await tester.pumpAndSettle();
-    expect(_getScale(tester), 1.0);
-    expect(_getOpacity(tester), 1.0);
-  });
-}
-
-double _getScale(WidgetTester tester) {
-  final ScaleTransition scale = tester.widget(find.byType(ScaleTransition));
-  return scale.scale.value;
-}
-
-double _getOpacity(WidgetTester tester) {
-  final FadeTransition scale = tester.widget(find.byType(FadeTransition));
-  return scale.opacity.value;
-}
-
-class _StatefulTestWidget extends StatefulWidget {
-  const _StatefulTestWidget({Key key, this.name}) : super(key: key);
-
-  final String name;
-
-  @override
-  State<_StatefulTestWidget> createState() => _StatefulTestWidgetState();
-}
-
-class _StatefulTestWidgetState extends State<_StatefulTestWidget> {
-  @override
-  Widget build(BuildContext context) {
-    return Text(widget.name);
-  }
-}