diff --git a/packages/animations/lib/animations.dart b/packages/animations/lib/animations.dart index ce0beba545c5..3b48f3e5cde3 100644 --- a/packages/animations/lib/animations.dart +++ b/packages/animations/lib/animations.dart @@ -5,4 +5,4 @@ export 'src/fade_through_transition.dart'; export 'src/open_container.dart'; export 'src/page_transition_switcher.dart'; -export 'src/shared_z_axis_transition.dart'; +export 'src/shared_axis_transition.dart'; diff --git a/packages/animations/lib/src/shared_z_axis_transition.dart b/packages/animations/lib/src/shared_axis_transition.dart similarity index 58% rename from packages/animations/lib/src/shared_z_axis_transition.dart rename to packages/animations/lib/src/shared_axis_transition.dart index a678ec6ea4a1..3d8cc6121d72 100644 --- a/packages/animations/lib/src/shared_z_axis_transition.dart +++ b/packages/animations/lib/src/shared_axis_transition.dart @@ -9,18 +9,27 @@ import 'package:flutter/widgets.dart'; import 'utils/curves.dart'; -/// An in-place fade and scale transition used by [PageTransitionsTheme] -/// to create a page transition with [SharedZAxisTransition]. +/// Determines which type of shared axis transition is used. +enum SharedAxisTransitionType { + /// Creates a shared axis vertical (y-axis) page transition. + vertical, + + /// Creates a shared axis horizontal (x-axis) page transition. + horizontal, + + /// Creates a shared axis scaled (z-axis) page transition. + scaled, +} + +/// Used by [PageTransitionsTheme] to define a page route transition animation +/// in which outgoing and incoming elements share a fade transition. /// -/// The shared axis pattern provides the transition animation -/// between UI elements that have a spatial or navigational -/// relationship. For example, transitioning from one page of a -/// sign-up page to the next one. +/// The shared axis pattern provides the transition animation between UI elements +/// that have a spatial or navigational relationship. For example, +/// transitioning from one page of a sign up page to the next one. /// -/// In this particular transition, the outgoing widget expands and -/// fades away while the incoming widget shrinks and fades into place. /// -/// The following example shows how the SharedZAxisPageTransitionsBuilder can +/// The following example shows how the SharedAxisPageTransitionsBuilder can /// be used in a [PageTransitionsTheme] to change the default transitions /// of [MaterialPageRoute]s. /// @@ -29,8 +38,12 @@ import 'utils/curves.dart'; /// theme: ThemeData( /// pageTransitionsTheme: PageTransitionsTheme( /// builders: { -/// TargetPlatform.android: SharedZAxisPageTransitionsBuilder(), -/// TargetPlatform.iOS: SharedZAxisPageTransitionsBuilder(), +/// TargetPlatform.android: SharedAxisPageTransitionsBuilder( +/// transitionType: SharedAxisTransitionType.horizontal, +/// ), +/// TargetPlatform.iOS: SharedAxisPageTransitionsBuilder( +/// transitionType: SharedAxisTransitionType.horizontal, +/// ), /// }, /// ), /// ), @@ -39,7 +52,7 @@ import 'utils/curves.dart'; /// return Container( /// color: Colors.red, /// child: Center( -/// child: MaterialButton( +/// child: RaisedButton( /// child: Text('Push route'), /// onPressed: () { /// Navigator.of(context).pushNamed('/a'); @@ -48,7 +61,7 @@ import 'utils/curves.dart'; /// ), /// ); /// }, -/// '/a': (BuildContext context) { +/// '/a' : (BuildContext context) { /// return Container( /// color: Colors.blue, /// child: Center( @@ -64,9 +77,14 @@ import 'utils/curves.dart'; /// }, /// ); /// ``` -class SharedZAxisPageTransitionsBuilder extends PageTransitionsBuilder { - /// Construct a [SharedZAxisPageTransitionsBuilder]. - const SharedZAxisPageTransitionsBuilder(); +class SharedAxisPageTransitionsBuilder extends PageTransitionsBuilder { + /// Construct a [SharedAxisPageTransitionsBuilder]. + const SharedAxisPageTransitionsBuilder({ + this.transitionType, + }); + + /// Determines which [SharedAxisTransitionType] to build. + final SharedAxisTransitionType transitionType; @override Widget buildTransitions( @@ -76,29 +94,31 @@ class SharedZAxisPageTransitionsBuilder extends PageTransitionsBuilder { Animation secondaryAnimation, Widget child, ) { - return SharedZAxisTransition( + return SharedAxisTransition( animation: animation, secondaryAnimation: secondaryAnimation, + transitionType: transitionType, child: child, ); } } -/// Defines a in-place transition in which the outgoing widget expands -/// and fades away while the incoming widget shrinks and fades into place. +/// Defines a transition in which outgoing and incoming elements share a fade +/// transition. /// /// The shared axis pattern provides the transition animation between UI elements /// that have a spatial or navigational relationship. For example, -/// transitioning from one page of a sign-up page to the next one. +/// transitioning from one page of a sign up page to the next one. /// -/// Consider using [SharedZAxisTransition] within a +/// Consider using [SharedAxisTransition] within a /// [PageTransitionsTheme] if you want to apply this kind of transition to -/// all [MaterialPageRoute] transitions within a Navigator (see -/// [SharedZAxisPageTransitionsBuilder] for some example code). +/// [MaterialPageRoute] transitions within a Navigator (see +/// [SharedAxisPageTransitionsBuilder] for example code). /// /// This transition can also be used directly in a /// [PageTransitionSwitcher.transitionBuilder] to transition /// from one widget to another as seen in the following example: +/// /// ```dart /// int _selectedIndex = 0; /// @@ -117,9 +137,10 @@ class SharedZAxisPageTransitionsBuilder extends PageTransitionsBuilder { /// Animation primaryAnimation, /// Animation secondaryAnimation, /// ) { -/// return SharedZAxisTransition( +/// return SharedAxisTransition( /// animation: primaryAnimation, /// secondaryAnimation: secondaryAnimation, +/// transitionType: SharedAxisTransitionType.horizontal, /// child: child, /// ); /// }, @@ -156,18 +177,18 @@ class SharedZAxisPageTransitionsBuilder extends PageTransitionsBuilder { /// ); /// } /// ``` -class SharedZAxisTransition extends StatefulWidget { - /// Creates a [SharedZAxisTransition]. +class SharedAxisTransition extends StatefulWidget { + /// Creates a [SharedAxisTransition]. /// - /// The [animation] and [secondaryAnimation] arguments are required and must + /// The [animation] and [secondaryAnimation] argument are required and must /// not be null. - const SharedZAxisTransition({ + const SharedAxisTransition({ Key key, @required this.animation, @required this.secondaryAnimation, + @required this.transitionType, this.child, - }) : assert(animation != null), - assert(secondaryAnimation != null), + }) : assert(transitionType != null), super(key: key); /// The animation that drives the [child]'s entrance and exit. @@ -187,6 +208,14 @@ class SharedZAxisTransition extends StatefulWidget { /// property when the it is used as a page transition. final Animation secondaryAnimation; + /// Determines which type of shared axis transition is used. + /// + /// See also: + /// + /// * [SharedAxisTransitionType], which defines and describes all shared + /// axis transition types. + final SharedAxisTransitionType transitionType; + /// The widget below this widget in the tree. /// /// This widget will transition in and out as driven by [animation] and @@ -194,10 +223,10 @@ class SharedZAxisTransition extends StatefulWidget { final Widget child; @override - _SharedZAxisTransitionState createState() => _SharedZAxisTransitionState(); + _SharedAxisTransitionState createState() => _SharedAxisTransitionState(); } -class _SharedZAxisTransitionState extends State { +class _SharedAxisTransitionState extends State { AnimationStatus _effectiveAnimationStatus; AnimationStatus _effectiveSecondaryAnimationStatus; @@ -262,28 +291,20 @@ class _SharedZAxisTransitionState extends State { return null; // unreachable } - void _updateAnimationListener( - Animation oldAnimation, - Animation animation, - ) { - if (oldAnimation != animation) { - oldAnimation.removeStatusListener(_animationListener); - animation.addStatusListener(_animationListener); - _animationListener(animation.status); - } - } - @override - void didUpdateWidget(SharedZAxisTransition oldWidget) { + void didUpdateWidget(SharedAxisTransition oldWidget) { super.didUpdateWidget(oldWidget); - _updateAnimationListener( - oldWidget.animation, - widget.animation, - ); - _updateAnimationListener( - oldWidget.secondaryAnimation, - widget.secondaryAnimation, - ); + if (oldWidget.animation != widget.animation) { + oldWidget.animation.removeStatusListener(_animationListener); + widget.animation.addStatusListener(_animationListener); + _animationListener(widget.animation.status); + } + if (oldWidget.secondaryAnimation != widget.secondaryAnimation) { + oldWidget.secondaryAnimation + .removeStatusListener(_secondaryAnimationListener); + widget.secondaryAnimation.addStatusListener(_secondaryAnimationListener); + _secondaryAnimationListener(widget.secondaryAnimation.status); + } } @override @@ -293,13 +314,16 @@ class _SharedZAxisTransitionState extends State { super.dispose(); } + static final Tween _flippedTween = Tween( + begin: 1.0, + end: 0.0, + ); + static Animation _flip(Animation animation) { + return _flippedTween.animate(animation); + } + @override Widget build(BuildContext context) { - final Tween flippedTween = Tween( - begin: 1.0, - end: 0.0, - ); - return AnimatedBuilder( animation: widget.animation, builder: (BuildContext context, Widget child) { @@ -308,15 +332,15 @@ class _SharedZAxisTransitionState extends State { case AnimationStatus.forward: return _EnterTransition( animation: widget.animation, + transitionType: widget.transitionType, child: child, ); case AnimationStatus.dismissed: case AnimationStatus.reverse: case AnimationStatus.completed: return _ExitTransition( - animation: flippedTween.animate( - widget.animation, - ), + animation: _flip(widget.animation), + transitionType: widget.transitionType, child: child, ); } @@ -330,15 +354,15 @@ class _SharedZAxisTransitionState extends State { case AnimationStatus.forward: return _ExitTransition( animation: widget.secondaryAnimation, + transitionType: widget.transitionType, child: child, ); case AnimationStatus.dismissed: case AnimationStatus.reverse: case AnimationStatus.completed: return _EnterTransition( - animation: flippedTween.animate( - widget.secondaryAnimation, - ), + animation: _flip(widget.secondaryAnimation), + transitionType: widget.transitionType, child: child, ); } @@ -353,10 +377,12 @@ class _SharedZAxisTransitionState extends State { class _EnterTransition extends StatelessWidget { const _EnterTransition({ this.animation, + this.transitionType, this.child, }); final Animation animation; + final SharedAxisTransitionType transitionType; final Widget child; static Animatable fadeInTransition = CurveTween( @@ -370,23 +396,58 @@ class _EnterTransition extends StatelessWidget { @override Widget build(BuildContext context) { - return FadeTransition( - opacity: fadeInTransition.animate(animation), - child: ScaleTransition( - scale: scaleInTransition.animate(animation), - child: child, - ), - ); + switch (transitionType) { + case SharedAxisTransitionType.horizontal: + final Animatable slideInTransition = Tween( + begin: const Offset(30, 0.0), + end: Offset.zero, + ).chain(CurveTween(curve: standardEasing)); + + return FadeTransition( + opacity: fadeInTransition.animate(animation), + child: Transform.translate( + offset: slideInTransition.evaluate(animation), + child: child, + ), + ); + break; + case SharedAxisTransitionType.vertical: + final Animatable slideInTransition = Tween( + begin: const Offset(0.0, 30), + end: Offset.zero, + ).chain(CurveTween(curve: standardEasing)); + + return FadeTransition( + opacity: fadeInTransition.animate(animation), + child: Transform.translate( + offset: slideInTransition.evaluate(animation), + child: child, + ), + ); + break; + case SharedAxisTransitionType.scaled: + return FadeTransition( + opacity: fadeInTransition.animate(animation), + child: ScaleTransition( + scale: scaleInTransition.animate(animation), + child: child, + ), + ); + break; + } + return null; // unreachable } } class _ExitTransition extends StatelessWidget { const _ExitTransition({ this.animation, + this.transitionType, this.child, }); final Animation animation; + final SharedAxisTransitionType transitionType; final Widget child; static Animatable fadeOutTransition = FlippedCurveTween( @@ -400,15 +461,54 @@ class _ExitTransition extends StatelessWidget { @override Widget build(BuildContext context) { - return FadeTransition( - opacity: fadeOutTransition.animate(animation), - child: Container( - color: Theme.of(context).canvasColor, - child: ScaleTransition( - scale: scaleOutTransition.animate(animation), - child: child, - ), - ), - ); + switch (transitionType) { + case SharedAxisTransitionType.horizontal: + final Animatable slideOutTransition = Tween( + begin: Offset.zero, + end: const Offset(30, 0.0), + ).chain(CurveTween(curve: standardEasing)); + + return FadeTransition( + opacity: fadeOutTransition.animate(animation), + child: Container( + color: Theme.of(context).canvasColor, + child: Transform.translate( + offset: slideOutTransition.evaluate(animation), + child: child, + ), + ), + ); + break; + case SharedAxisTransitionType.vertical: + final Animatable slideOutTransition = Tween( + begin: Offset.zero, + end: const Offset(0.0, 30), + ).chain(CurveTween(curve: standardEasing)); + + return FadeTransition( + opacity: fadeOutTransition.animate(animation), + child: Container( + color: Theme.of(context).canvasColor, + child: Transform.translate( + offset: slideOutTransition.evaluate(animation), + child: child, + ), + ), + ); + break; + case SharedAxisTransitionType.scaled: + return FadeTransition( + opacity: fadeOutTransition.animate(animation), + child: Container( + color: Theme.of(context).canvasColor, + child: ScaleTransition( + scale: scaleOutTransition.animate(animation), + child: child, + ), + ), + ); + break; + } + return null; // unreachable } } diff --git a/packages/animations/pubspec.yaml b/packages/animations/pubspec.yaml index 6bfc61343745..b778a35daf78 100644 --- a/packages/animations/pubspec.yaml +++ b/packages/animations/pubspec.yaml @@ -13,3 +13,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + vector_math: ^2.0.8 diff --git a/packages/animations/test/shared_axis_transition_test.dart b/packages/animations/test/shared_axis_transition_test.dart new file mode 100644 index 000000000000..10613971fe09 --- /dev/null +++ b/packages/animations/test/shared_axis_transition_test.dart @@ -0,0 +1,1520 @@ +// Copyright 2019 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. + +import 'package:animations/src/shared_axis_transition.dart'; +import 'package:flutter/animation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; +import 'package:vector_math/vector_math_64.dart'; + +void main() { + group('SharedAxisTransitionType.horizontal', () { + testWidgets( + 'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition', + (WidgetTester tester) async { + final AnimationController animation = AnimationController( + vsync: const TestVSync(), + ); + final AnimationController secondaryAnimation = AnimationController( + vsync: const TestVSync(), + ); + + await tester.pumpWidget( + const SharedAxisPageTransitionsBuilder( + transitionType: SharedAxisTransitionType.horizontal, + ).buildTransitions( + null, + null, + animation, + secondaryAnimation, + const Placeholder(), + ), + ); + + expect(find.byType(SharedAxisTransition), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition runs forward', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.horizontal, + ), + ); + + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 0.0, + ); + expect(_getOpacity(bottomRoute, tester), 1.0); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + await tester.pump(); + + // Bottom route is not offset and fully visible. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 0.0, + ); + expect(_getOpacity(bottomRoute, tester), 1.0); + // Top route is offset to the right by 30.0 pixels + // and not visible yet. + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 30.0, + ); + expect(_getOpacity(topRoute, tester), 0.0); + + // Jump 3/10ths of the way through the transition, bottom route + // should be be completely faded out while the top route + // is also completely faded out. + // Transition time: 300ms, 3/10 * 300ms = 90ms + await tester.pump(const Duration(milliseconds: 90)); + + // Bottom route is now invisible + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is still invisible, but moving towards the left. + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + double topOffset = _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ); + expect(topOffset, lessThan(30.0)); + expect(topOffset, greaterThan(0.0)); + + // Jump to the middle of fading in + await tester.pump(const Duration(milliseconds: 90)); + // Bottom route is still invisible + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is fading in + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), greaterThan(0)); + expect(_getOpacity(topRoute, tester), lessThan(1.0)); + topOffset = _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ); + expect(topOffset, greaterThan(0.0)); + expect(topOffset, lessThan(30.0)); + + // Jump to the end of the transition + await tester.pump(const Duration(milliseconds: 120)); + // Bottom route is not visible. + expect(find.text(bottomRoute), findsOneWidget); + + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 30.0, + ); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route has no offset and is visible. + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 0.0, + ); + expect(_getOpacity(topRoute, tester), 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(bottomRoute), findsNothing); + expect(find.text(topRoute), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition runs in reverse', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.horizontal, + ), + ); + + navigator.currentState.pushNamed('/a'); + await tester.pumpAndSettle(); + + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 0.0, + ); + expect(_getOpacity(topRoute, tester), 1.0); + expect(find.text(bottomRoute), findsNothing); + + navigator.currentState.pop(); + await tester.pump(); + + // Top route is is not offset and fully visible. + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 0.0, + ); + expect(_getOpacity(topRoute, tester), 1.0); + // Bottom route is offset to the right and is not visible yet. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 30.0, + ); + expect(_getOpacity(bottomRoute, tester), 0.0); + + // Jump 3/10ths of the way through the transition, bottom route + // should be be completely faded out while the top route + // is also completely faded out. + // Transition time: 300ms, 3/10 * 300ms = 90ms + await tester.pump(const Duration(milliseconds: 90)); + + // Top route is now invisible + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + // Bottom route is still invisible, but moving towards the left. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), + moreOrLessEquals(0, epsilon: 0.005)); + double bottomOffset = _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ); + expect(bottomOffset, greaterThan(0.0)); + expect(bottomOffset, lessThan(30.0)); + + // Jump to the middle of fading in + await tester.pump(const Duration(milliseconds: 90)); + // Top route is still invisible + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + // Bottom route is fading in + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), greaterThan(0)); + expect(_getOpacity(bottomRoute, tester), lessThan(1.0)); + bottomOffset = _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ); + expect(bottomOffset, greaterThan(0.0)); + expect(bottomOffset, lessThan(30.0)); + + // Jump to the end of the transition + await tester.pump(const Duration(milliseconds: 120)); + // Top route is not visible and is offset to the right. + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 30.0, + ); + expect(_getOpacity(topRoute, tester), 0.0); + // Bottom route is not offset and is visible. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 0.0, + ); + expect(_getOpacity(bottomRoute, tester), 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(topRoute), findsNothing); + expect(find.text(bottomRoute), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition does not jump when interrupted', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.horizontal, + ), + ); + expect(find.text(bottomRoute), findsOneWidget); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + + // Jump to halfway point of transition. + await tester.pump(const Duration(milliseconds: 150)); + // Bottom route is fully faded out. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + final double halfwayBottomOffset = _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ); + expect(halfwayBottomOffset, greaterThan(0.0)); + expect(halfwayBottomOffset, lessThan(30.0)); + + // Top route is fading/coming in. + expect(find.text(topRoute), findsOneWidget); + final double halfwayTopOffset = _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ); + final double halfwayTopOpacity = _getOpacity(topRoute, tester); + expect(halfwayTopOffset, greaterThan(0.0)); + expect(halfwayTopOffset, lessThan(30.0)); + expect(halfwayTopOpacity, greaterThan(0.0)); + expect(halfwayTopOpacity, lessThan(1.0)); + + // Interrupt the transition with a pop. + navigator.currentState.pop(); + await tester.pump(); + + // Nothing should change. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + halfwayBottomOffset, + ); + expect(_getOpacity(bottomRoute, tester), 0.0); + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + halfwayTopOffset, + ); + expect(_getOpacity(topRoute, tester), halfwayTopOpacity); + + // Jump to the 1/4 (75 ms) point of transition + await tester.pump(const Duration(milliseconds: 75)); + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + greaterThan(0.0), + ); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + lessThan(30.0), + ); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + lessThan(halfwayBottomOffset), + ); + expect(_getOpacity(bottomRoute, tester), greaterThan(0.0)); + expect(_getOpacity(bottomRoute, tester), lessThan(1.0)); + + // Jump to the end. + await tester.pump(const Duration(milliseconds: 75)); + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 0.0, + ); + expect(_getOpacity(bottomRoute, tester), 1.0); + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.horizontal, + ), + 30.0, + ); + expect(_getOpacity(topRoute, tester), 0.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(topRoute), findsNothing); + expect(find.text(bottomRoute), findsOneWidget); + }, + ); + + testWidgets( + 'State is not lost when transitioning', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + contentBuilder: (RouteSettings settings) { + return _StatefulTestWidget( + key: ValueKey(settings.name), + name: settings.name, + ); + }, + transitionType: SharedAxisTransitionType.horizontal, + ), + ); + + final _StatefulTestWidgetState bottomState = tester.state( + find.byKey(const ValueKey(bottomRoute)), + ); + expect(bottomState.widget.name, bottomRoute); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + await tester.pump(); + + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + final _StatefulTestWidgetState topState = tester.state( + find.byKey(const ValueKey(topRoute)), + ); + expect(topState.widget.name, topRoute); + + await tester.pump(const Duration(milliseconds: 150)); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey( + const ValueKey(bottomRoute), + skipOffstage: false, + )), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + navigator.currentState.pop(); + await tester.pump(); + + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pump(const Duration(milliseconds: 150)); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect(find.byKey(const ValueKey(topRoute)), findsNothing); + }, + ); + }); + + group('SharedAxisTransitionType.vertical', () { + testWidgets( + 'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition', + (WidgetTester tester) async { + final AnimationController animation = AnimationController( + vsync: const TestVSync(), + ); + final AnimationController secondaryAnimation = AnimationController( + vsync: const TestVSync(), + ); + + await tester.pumpWidget( + const SharedAxisPageTransitionsBuilder( + transitionType: SharedAxisTransitionType.vertical, + ).buildTransitions( + null, + null, + animation, + secondaryAnimation, + const Placeholder(), + ), + ); + + expect(find.byType(SharedAxisTransition), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition runs forward', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.vertical, + ), + ); + + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 0.0, + ); + expect(_getOpacity(bottomRoute, tester), 1.0); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + await tester.pump(); + + // Bottom route is not offset and fully visible. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 0.0, + ); + expect(_getOpacity(bottomRoute, tester), 1.0); + // Top route is offset to the right by 30.0 pixels + // and not visible yet. + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 30.0, + ); + expect(_getOpacity(topRoute, tester), 0.0); + + // Jump 3/10ths of the way through the transition, bottom route + // should be be completely faded out while the top route + // is also completely faded out. + // Transition time: 300ms, 3/10 * 300ms = 90ms + await tester.pump(const Duration(milliseconds: 90)); + + // Bottom route is now invisible + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is still invisible, but moving towards the left. + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + double topOffset = _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ); + expect(topOffset, lessThan(30.0)); + expect(topOffset, greaterThan(0.0)); + + // Jump to the middle of fading in + await tester.pump(const Duration(milliseconds: 90)); + // Bottom route is still invisible + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is fading in + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), greaterThan(0)); + expect(_getOpacity(topRoute, tester), lessThan(1.0)); + topOffset = _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ); + expect(topOffset, greaterThan(0.0)); + expect(topOffset, lessThan(30.0)); + + // Jump to the end of the transition + await tester.pump(const Duration(milliseconds: 120)); + // Bottom route is not visible. + expect(find.text(bottomRoute), findsOneWidget); + + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 30.0, + ); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route has no offset and is visible. + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 0.0, + ); + expect(_getOpacity(topRoute, tester), 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(bottomRoute), findsNothing); + expect(find.text(topRoute), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition runs in reverse', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.vertical, + ), + ); + + navigator.currentState.pushNamed('/a'); + await tester.pumpAndSettle(); + + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 0.0, + ); + expect(_getOpacity(topRoute, tester), 1.0); + expect(find.text(bottomRoute), findsNothing); + + navigator.currentState.pop(); + await tester.pump(); + + // Top route is is not offset and fully visible. + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 0.0, + ); + expect(_getOpacity(topRoute, tester), 1.0); + // Bottom route is offset to the right and is not visible yet. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 30.0, + ); + expect(_getOpacity(bottomRoute, tester), 0.0); + + // Jump 3/10ths of the way through the transition, bottom route + // should be be completely faded out while the top route + // is also completely faded out. + // Transition time: 300ms, 3/10 * 300ms = 90ms + await tester.pump(const Duration(milliseconds: 90)); + + // Top route is now invisible + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + // Bottom route is still invisible, but moving towards the left. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getOpacity(bottomRoute, tester), + moreOrLessEquals(0, epsilon: 0.005), + ); + double bottomOffset = _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ); + expect(bottomOffset, greaterThan(0.0)); + expect(bottomOffset, lessThan(30.0)); + + // Jump to the middle of fading in + await tester.pump(const Duration(milliseconds: 90)); + // Top route is still invisible + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + // Bottom route is fading in + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), greaterThan(0)); + expect(_getOpacity(bottomRoute, tester), lessThan(1.0)); + bottomOffset = _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ); + expect(bottomOffset, greaterThan(0.0)); + expect(bottomOffset, lessThan(30.0)); + + // Jump to the end of the transition + await tester.pump(const Duration(milliseconds: 120)); + // Top route is not visible and is offset to the right. + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 30.0, + ); + expect(_getOpacity(topRoute, tester), 0.0); + // Bottom route is not offset and is visible. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 0.0, + ); + expect(_getOpacity(bottomRoute, tester), 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(topRoute), findsNothing); + expect(find.text(bottomRoute), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition does not jump when interrupted', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.vertical, + ), + ); + expect(find.text(bottomRoute), findsOneWidget); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + + // Jump to halfway point of transition. + await tester.pump(const Duration(milliseconds: 150)); + // Bottom route is fully faded out. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + final double halfwayBottomOffset = _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ); + expect(halfwayBottomOffset, greaterThan(0.0)); + expect(halfwayBottomOffset, lessThan(30.0)); + + // Top route is fading/coming in. + expect(find.text(topRoute), findsOneWidget); + final double halfwayTopOffset = _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ); + final double halfwayTopOpacity = _getOpacity(topRoute, tester); + expect(halfwayTopOffset, greaterThan(0.0)); + expect(halfwayTopOffset, lessThan(30.0)); + expect(halfwayTopOpacity, greaterThan(0.0)); + expect(halfwayTopOpacity, lessThan(1.0)); + + // Interrupt the transition with a pop. + navigator.currentState.pop(); + await tester.pump(); + + // Nothing should change. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + halfwayBottomOffset, + ); + expect(_getOpacity(bottomRoute, tester), 0.0); + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ), + halfwayTopOffset, + ); + expect(_getOpacity(topRoute, tester), halfwayTopOpacity); + + // Jump to the 1/4 (75 ms) point of transition + await tester.pump(const Duration(milliseconds: 75)); + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + greaterThan(0.0), + ); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + lessThan(30.0), + ); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + lessThan(halfwayBottomOffset), + ); + expect(_getOpacity(bottomRoute, tester), greaterThan(0.0)); + expect(_getOpacity(bottomRoute, tester), lessThan(1.0)); + + // Jump to the end. + await tester.pump(const Duration(milliseconds: 75)); + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getTranslationOffset( + bottomRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 0.0, + ); + expect(_getOpacity(bottomRoute, tester), 1.0); + expect(find.text(topRoute), findsOneWidget); + expect( + _getTranslationOffset( + topRoute, + tester, + SharedAxisTransitionType.vertical, + ), + 30.0, + ); + expect(_getOpacity(topRoute, tester), 0.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(topRoute), findsNothing); + expect(find.text(bottomRoute), findsOneWidget); + }, + ); + + testWidgets( + 'State is not lost when transitioning', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + contentBuilder: (RouteSettings settings) { + return _StatefulTestWidget( + key: ValueKey(settings.name), + name: settings.name, + ); + }, + transitionType: SharedAxisTransitionType.vertical, + ), + ); + + final _StatefulTestWidgetState bottomState = tester.state( + find.byKey(const ValueKey(bottomRoute)), + ); + expect(bottomState.widget.name, bottomRoute); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + await tester.pump(); + + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + final _StatefulTestWidgetState topState = tester.state( + find.byKey(const ValueKey(topRoute)), + ); + expect(topState.widget.name, topRoute); + + await tester.pump(const Duration(milliseconds: 150)); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey( + const ValueKey(bottomRoute), + skipOffstage: false, + )), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + navigator.currentState.pop(); + await tester.pump(); + + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pump(const Duration(milliseconds: 150)); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect(find.byKey(const ValueKey(topRoute)), findsNothing); + }, + ); + }); + + group('SharedAxisTransitionType.scaled', () { + testWidgets( + 'SharedAxisPageTransitionsBuilder builds a SharedAxisTransition', + (WidgetTester tester) async { + final AnimationController animation = AnimationController( + vsync: const TestVSync(), + ); + final AnimationController secondaryAnimation = AnimationController( + vsync: const TestVSync(), + ); + + await tester.pumpWidget( + const SharedAxisPageTransitionsBuilder( + transitionType: SharedAxisTransitionType.scaled, + ).buildTransitions( + null, + null, + animation, + secondaryAnimation, + const Placeholder(), + ), + ); + + expect(find.byType(SharedAxisTransition), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition runs forward', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.scaled, + ), + ); + + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 1.0); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + await tester.pump(); + + // Bottom route is full size and fully visible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 1.0); + // Top route is at 80% of full size and not visible yet. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 0.8); + expect(_getOpacity(topRoute, tester), 0.0); + + // Jump 3/10ths of the way through the transition, bottom route + // should be be completely faded out while the top route + // is also completely faded out. + // Transition time: 300ms, 3/10 * 300ms = 90ms + await tester.pump(const Duration(milliseconds: 90)); + + // Bottom route is now invisible + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is still invisible, but scaling up. + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + double topScale = _getScale(topRoute, tester); + expect(topScale, greaterThan(0.8)); + expect(topScale, lessThan(1.0)); + + // Jump to the middle of fading in + await tester.pump(const Duration(milliseconds: 90)); + // Bottom route is still invisible + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route is fading in + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), greaterThan(0)); + expect(_getOpacity(topRoute, tester), lessThan(1.0)); + topScale = _getScale(topRoute, tester); + expect(topScale, greaterThan(0.8)); + expect(topScale, lessThan(1.0)); + + // Jump to the end of the transition + await tester.pump(const Duration(milliseconds: 120)); + // Bottom route is not visible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.1); + expect(_getOpacity(bottomRoute, tester), 0.0); + // Top route fully scaled in and visible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(bottomRoute), findsNothing); + expect(find.text(topRoute), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition runs in reverse', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.scaled, + ), + ); + + navigator.currentState.pushNamed(topRoute); + await tester.pumpAndSettle(); + + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 1.0); + expect(find.text(bottomRoute), findsNothing); + + navigator.currentState.pop(); + await tester.pump(); + + // Top route is full size and fully visible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.0); + expect(_getOpacity(topRoute, tester), 1.0); + // Bottom route is at 80% of full size and not visible yet. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 0.8); + expect(_getOpacity(bottomRoute, tester), 0.0); + + // Jump 3/10ths of the way through the transition, bottom route + // should be be completely faded out while the top route + // is also completely faded out. + // Transition time: 300ms, 3/10 * 300ms = 90ms + await tester.pump(const Duration(milliseconds: 90)); + + // Bottom route is now invisible + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + // Top route is still invisible, but scaling up. + expect(find.text(bottomRoute), findsOneWidget); + expect( + _getOpacity(bottomRoute, tester), + moreOrLessEquals(0, epsilon: 0.005), + ); + double bottomScale = _getScale(bottomRoute, tester); + expect(bottomScale, greaterThan(0.8)); + expect(bottomScale, lessThan(1.0)); + + // Jump to the middle of fading in + await tester.pump(const Duration(milliseconds: 90)); + // Top route is still invisible + expect(find.text(topRoute), findsOneWidget); + expect(_getOpacity(topRoute, tester), 0.0); + // Bottom route is fading in + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), greaterThan(0)); + expect(_getOpacity(bottomRoute, tester), lessThan(1.0)); + bottomScale = _getScale(bottomRoute, tester); + expect(bottomScale, greaterThan(0.8)); + expect(bottomScale, lessThan(1.0)); + + // Jump to the end of the transition + await tester.pump(const Duration(milliseconds: 120)); + // Top route is not visible. + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 1.1); + expect(_getOpacity(topRoute, tester), 0.0); + // Bottom route fully scaled in and visible. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 1.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(topRoute), findsNothing); + expect(find.text(bottomRoute), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition does not jump when interrupted', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.scaled, + ), + ); + expect(find.text(bottomRoute), findsOneWidget); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + + // Jump to halfway point of transition. + await tester.pump(const Duration(milliseconds: 150)); + // Bottom route is fully faded out. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getOpacity(bottomRoute, tester), 0.0); + final double halfwayBottomScale = _getScale(bottomRoute, tester); + expect(halfwayBottomScale, greaterThan(1.0)); + expect(halfwayBottomScale, lessThan(1.1)); + + // Top route is fading/scaling in. + expect(find.text(topRoute), findsOneWidget); + final double halfwayTopScale = _getScale(topRoute, tester); + final double halfwayTopOpacity = _getOpacity(topRoute, tester); + expect(halfwayTopScale, greaterThan(0.8)); + expect(halfwayTopScale, lessThan(1.0)); + expect(halfwayTopOpacity, greaterThan(0.0)); + expect(halfwayTopOpacity, lessThan(1.0)); + + // Interrupt the transition with a pop. + navigator.currentState.pop(); + await tester.pump(); + + // Nothing should change. + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), halfwayBottomScale); + expect(_getOpacity(bottomRoute, tester), 0.0); + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), halfwayTopScale); + expect(_getOpacity(topRoute, tester), halfwayTopOpacity); + + // Jump to the 1/4 (75 ms) point of transition + await tester.pump(const Duration(milliseconds: 75)); + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), greaterThan(1.0)); + expect(_getScale(bottomRoute, tester), lessThan(1.1)); + expect(_getScale(bottomRoute, tester), lessThan(halfwayBottomScale)); + expect(_getOpacity(bottomRoute, tester), greaterThan(0.0)); + expect(_getOpacity(bottomRoute, tester), lessThan(1.0)); + + // Jump to the end. + await tester.pump(const Duration(milliseconds: 75)); + expect(find.text(bottomRoute), findsOneWidget); + expect(_getScale(bottomRoute, tester), 1.0); + expect(_getOpacity(bottomRoute, tester), 1.0); + expect(find.text(topRoute), findsOneWidget); + expect(_getScale(topRoute, tester), 0.80); + expect(_getOpacity(topRoute, tester), 0.0); + + await tester.pump(const Duration(milliseconds: 1)); + expect(find.text(topRoute), findsNothing); + expect(find.text(bottomRoute), findsOneWidget); + }, + ); + + testWidgets( + 'SharedAxisTransition properly disposes animation', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.scaled, + ), + ); + expect(find.text(bottomRoute), findsOneWidget); + expect(find.text(topRoute), findsNothing); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + + // Jump to halfway point of transition. + await tester.pump(const Duration(milliseconds: 150)); + expect(find.byType(SharedAxisTransition), findsNWidgets(2)); + + // Rebuild the app without the transition. + await tester.pumpWidget( + MaterialApp( + navigatorKey: navigator, + home: const Material( + child: Text('abc'), + ), + ), + ); + await tester.pump(); + // Transitions should have been disposed of. + expect(find.byType(SharedAxisTransition), findsNothing); + }, + ); + + testWidgets( + 'State is not lost when transitioning', + (WidgetTester tester) async { + final GlobalKey navigator = GlobalKey(); + const String bottomRoute = '/'; + const String topRoute = '/a'; + + await tester.pumpWidget( + _TestWidget( + navigatorKey: navigator, + transitionType: SharedAxisTransitionType.scaled, + contentBuilder: (RouteSettings settings) { + return _StatefulTestWidget( + key: ValueKey(settings.name), + name: settings.name, + ); + }, + ), + ); + + final _StatefulTestWidgetState bottomState = tester.state( + find.byKey(const ValueKey(bottomRoute)), + ); + expect(bottomState.widget.name, bottomRoute); + + navigator.currentState.pushNamed(topRoute); + await tester.pump(); + await tester.pump(); + + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + final _StatefulTestWidgetState topState = tester.state( + find.byKey(const ValueKey(topRoute)), + ); + expect(topState.widget.name, topRoute); + + await tester.pump(const Duration(milliseconds: 150)); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey( + const ValueKey(bottomRoute), + skipOffstage: false, + )), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + navigator.currentState.pop(); + await tester.pump(); + + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pump(const Duration(milliseconds: 150)); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect( + tester.state(find.byKey(const ValueKey(topRoute))), + topState, + ); + + await tester.pumpAndSettle(); + expect( + tester.state(find.byKey(const ValueKey(bottomRoute))), + bottomState, + ); + expect(find.byKey(const ValueKey(topRoute)), findsNothing); + }, + ); + }); +} + +double _getOpacity(String key, WidgetTester tester) { + final Finder finder = find.ancestor( + of: find.byKey(ValueKey(key)), + matching: find.byType(FadeTransition), + ); + return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { + final FadeTransition transition = widget; + return a * transition.opacity.value; + }); +} + +double _getTranslationOffset( + String key, + WidgetTester tester, + SharedAxisTransitionType transitionType, +) { + final Finder finder = find.ancestor( + of: find.byKey(ValueKey(key)), + matching: find.byType(Transform), + ); + + switch (transitionType) { + case SharedAxisTransitionType.horizontal: + return tester.widgetList(finder).fold(0.0, + (double a, Widget widget) { + final Transform transition = widget; + final Vector3 translation = transition.transform.getTranslation(); + return a + translation.x; + }); + break; + case SharedAxisTransitionType.vertical: + return tester.widgetList(finder).fold(0.0, + (double a, Widget widget) { + final Transform transition = widget; + final Vector3 translation = transition.transform.getTranslation(); + return a + translation.y; + }); + break; + case SharedAxisTransitionType.scaled: + // SharedAxisTransitionType.scaled should not return a translation + // offset. + return null; + break; + } + return null; // unreachable +} + +double _getScale(String key, WidgetTester tester) { + final Finder finder = find.ancestor( + of: find.byKey(ValueKey(key)), + matching: find.byType(ScaleTransition), + ); + return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { + final ScaleTransition transition = widget; + return a * transition.scale.value; + }); +} + +class _TestWidget extends StatelessWidget { + const _TestWidget({ + this.navigatorKey, + this.contentBuilder, + this.transitionType, + }); + + final Key navigatorKey; + final _ContentBuilder contentBuilder; + final SharedAxisTransitionType transitionType; + + @override + Widget build(BuildContext context) { + return MaterialApp( + navigatorKey: navigatorKey, + theme: ThemeData( + platform: TargetPlatform.android, + pageTransitionsTheme: PageTransitionsTheme( + builders: { + TargetPlatform.android: SharedAxisPageTransitionsBuilder( + transitionType: transitionType, + ), + }, + ), + ), + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + settings: settings, + builder: (BuildContext context) { + return contentBuilder != null + ? contentBuilder(settings) + : Container( + child: Center( + key: ValueKey(settings.name), + child: Text(settings.name), + ), + ); + }, + ); + }, + ); + } +} + +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); + } +} + +typedef _ContentBuilder = Widget Function(RouteSettings settings); diff --git a/packages/animations/test/shared_z_axis_transition_test.dart b/packages/animations/test/shared_z_axis_transition_test.dart deleted file mode 100644 index ff30fb550ea2..000000000000 --- a/packages/animations/test/shared_z_axis_transition_test.dart +++ /dev/null @@ -1,467 +0,0 @@ -// Copyright 2019 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. - -import 'package:animations/src/shared_z_axis_transition.dart'; -import 'package:flutter/animation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/widgets.dart'; - -void main() { - testWidgets( - 'SharedZAxisPageTransitionsBuilder builds a SharedZAxisTransition', - (WidgetTester tester) async { - final AnimationController animation = AnimationController( - vsync: const TestVSync(), - ); - final AnimationController secondaryAnimation = AnimationController( - vsync: const TestVSync(), - ); - - await tester.pumpWidget( - const SharedZAxisPageTransitionsBuilder().buildTransitions( - null, - null, - animation, - secondaryAnimation, - const Placeholder(), - ), - ); - - expect(find.byType(SharedZAxisTransition), findsOneWidget); - }, - ); - - testWidgets( - 'SharedZAxisTransition runs forward', - (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - const String bottomRoute = '/'; - const String topRoute = '/a'; - - await tester.pumpWidget( - _TestWidget(navigatorKey: navigator), - ); - - expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), 1.0); - expect(_getOpacity(bottomRoute, tester), 1.0); - expect(find.text(topRoute), findsNothing); - - navigator.currentState.pushNamed(topRoute); - await tester.pump(); - await tester.pump(); - - // Bottom route is full size and fully visible. - expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), 1.0); - expect(_getOpacity(bottomRoute, tester), 1.0); - // Top route is at 80% of full size and not visible yet. - expect(find.text(topRoute), findsOneWidget); - expect(_getScale(topRoute, tester), 0.8); - expect(_getOpacity(topRoute, tester), 0.0); - - // Jump 3/10ths of the way through the transition, bottom route - // should be be completely faded out while the top route - // is also completely faded out. - // Transition time: 300ms, 3/10 * 300ms = 90ms - await tester.pump(const Duration(milliseconds: 90)); - - // Bottom route is now invisible - expect(find.text(bottomRoute), findsOneWidget); - expect(_getOpacity(bottomRoute, tester), 0.0); - // Top route is still invisible, but scaling up. - expect(find.text(topRoute), findsOneWidget); - expect(_getOpacity(topRoute, tester), 0.0); - double topScale = _getScale(topRoute, tester); - expect(topScale, greaterThan(0.8)); - expect(topScale, lessThan(1.0)); - - // Jump to the middle of fading in - await tester.pump(const Duration(milliseconds: 90)); - // Bottom route is still invisible - expect(find.text(bottomRoute), findsOneWidget); - expect(_getOpacity(bottomRoute, tester), 0.0); - // Top route is fading in - expect(find.text(topRoute), findsOneWidget); - expect(_getOpacity(topRoute, tester), greaterThan(0)); - expect(_getOpacity(topRoute, tester), lessThan(1.0)); - topScale = _getScale(topRoute, tester); - expect(topScale, greaterThan(0.8)); - expect(topScale, lessThan(1.0)); - - // Jump to the end of the transition - await tester.pump(const Duration(milliseconds: 120)); - // Bottom route is not visible. - expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), 1.1); - expect(_getOpacity(bottomRoute, tester), 0.0); - // Top route fully scaled in and visible. - expect(find.text(topRoute), findsOneWidget); - expect(_getScale(topRoute, tester), 1.0); - expect(_getOpacity(topRoute, tester), 1.0); - - await tester.pump(const Duration(milliseconds: 1)); - expect(find.text(bottomRoute), findsNothing); - expect(find.text(topRoute), findsOneWidget); - }, - ); - - testWidgets( - 'SharedZAxisTransition runs in reverse', - (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - const String bottomRoute = '/'; - const String topRoute = '/a'; - - await tester.pumpWidget( - _TestWidget(navigatorKey: navigator), - ); - - navigator.currentState.pushNamed(topRoute); - await tester.pumpAndSettle(); - - expect(find.text(topRoute), findsOneWidget); - expect(_getScale(topRoute, tester), 1.0); - expect(_getOpacity(topRoute, tester), 1.0); - expect(find.text(bottomRoute), findsNothing); - - navigator.currentState.pop(); - await tester.pump(); - - // Top route is full size and fully visible. - expect(find.text(topRoute), findsOneWidget); - expect(_getScale(topRoute, tester), 1.0); - expect(_getOpacity(topRoute, tester), 1.0); - // Bottom route is at 80% of full size and not visible yet. - expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), 0.8); - expect(_getOpacity(bottomRoute, tester), 0.0); - - // Jump 3/10ths of the way through the transition, bottom route - // should be be completely faded out while the top route - // is also completely faded out. - // Transition time: 300ms, 3/10 * 300ms = 90ms - await tester.pump(const Duration(milliseconds: 90)); - - // Bottom route is now invisible - expect(find.text(topRoute), findsOneWidget); - expect(_getOpacity(topRoute, tester), 0.0); - // Top route is still invisible, but scaling up. - expect(find.text(bottomRoute), findsOneWidget); - expect( - _getOpacity(bottomRoute, tester), - moreOrLessEquals(0, epsilon: 0.005), - ); - double bottomScale = _getScale(bottomRoute, tester); - expect(bottomScale, greaterThan(0.8)); - expect(bottomScale, lessThan(1.0)); - - // Jump to the middle of fading in - await tester.pump(const Duration(milliseconds: 90)); - // Top route is still invisible - expect(find.text(topRoute), findsOneWidget); - expect(_getOpacity(topRoute, tester), 0.0); - // Bottom route is fading in - expect(find.text(bottomRoute), findsOneWidget); - expect(_getOpacity(bottomRoute, tester), greaterThan(0)); - expect(_getOpacity(bottomRoute, tester), lessThan(1.0)); - bottomScale = _getScale(bottomRoute, tester); - expect(bottomScale, greaterThan(0.8)); - expect(bottomScale, lessThan(1.0)); - - // Jump to the end of the transition - await tester.pump(const Duration(milliseconds: 120)); - // Top route is not visible. - expect(find.text(topRoute), findsOneWidget); - expect(_getScale(topRoute, tester), 1.1); - expect(_getOpacity(topRoute, tester), 0.0); - // Bottom route fully scaled in and visible. - expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), 1.0); - expect(_getOpacity(bottomRoute, tester), 1.0); - - await tester.pump(const Duration(milliseconds: 1)); - expect(find.text(topRoute), findsNothing); - expect(find.text(bottomRoute), findsOneWidget); - }, - ); - - testWidgets( - 'SharedZAxisTransition does not jump when interrupted', - (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - const String bottomRoute = '/'; - const String topRoute = '/a'; - - await tester.pumpWidget( - _TestWidget( - navigatorKey: navigator, - ), - ); - expect(find.text(bottomRoute), findsOneWidget); - expect(find.text(topRoute), findsNothing); - - navigator.currentState.pushNamed(topRoute); - await tester.pump(); - - // Jump to halfway point of transition. - await tester.pump(const Duration(milliseconds: 150)); - // Bottom route is fully faded out. - expect(find.text(bottomRoute), findsOneWidget); - expect(_getOpacity(bottomRoute, tester), 0.0); - final double halfwayBottomScale = _getScale(bottomRoute, tester); - expect(halfwayBottomScale, greaterThan(1.0)); - expect(halfwayBottomScale, lessThan(1.1)); - - // Top route is fading/scaling in. - expect(find.text(topRoute), findsOneWidget); - final double halfwayTopScale = _getScale(topRoute, tester); - final double halfwayTopOpacity = _getOpacity(topRoute, tester); - expect(halfwayTopScale, greaterThan(0.8)); - expect(halfwayTopScale, lessThan(1.0)); - expect(halfwayTopOpacity, greaterThan(0.0)); - expect(halfwayTopOpacity, lessThan(1.0)); - - // Interrupt the transition with a pop. - navigator.currentState.pop(); - await tester.pump(); - - // Nothing should change. - expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), halfwayBottomScale); - expect(_getOpacity(bottomRoute, tester), 0.0); - expect(find.text(topRoute), findsOneWidget); - expect(_getScale(topRoute, tester), halfwayTopScale); - expect(_getOpacity(topRoute, tester), halfwayTopOpacity); - - // Jump to the 1/4 (75 ms) point of transition - await tester.pump(const Duration(milliseconds: 75)); - expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), greaterThan(1.0)); - expect(_getScale(bottomRoute, tester), lessThan(1.1)); - expect(_getScale(bottomRoute, tester), lessThan(halfwayBottomScale)); - expect(_getOpacity(bottomRoute, tester), greaterThan(0.0)); - expect(_getOpacity(bottomRoute, tester), lessThan(1.0)); - - // Jump to the end. - await tester.pump(const Duration(milliseconds: 75)); - expect(find.text(bottomRoute), findsOneWidget); - expect(_getScale(bottomRoute, tester), 1.0); - expect(_getOpacity(bottomRoute, tester), 1.0); - expect(find.text(topRoute), findsOneWidget); - expect(_getScale(topRoute, tester), 0.80); - expect(_getOpacity(topRoute, tester), 0.0); - - await tester.pump(const Duration(milliseconds: 1)); - expect(find.text(topRoute), findsNothing); - expect(find.text(bottomRoute), findsOneWidget); - }, - ); - - testWidgets( - 'SharedZAxisTransition properly disposes animation', - (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - const String bottomRoute = '/'; - const String topRoute = '/a'; - - await tester.pumpWidget( - _TestWidget( - navigatorKey: navigator, - ), - ); - expect(find.text(bottomRoute), findsOneWidget); - expect(find.text(topRoute), findsNothing); - - navigator.currentState.pushNamed(topRoute); - await tester.pump(); - - // Jump to halfway point of transition. - await tester.pump(const Duration(milliseconds: 150)); - expect(find.byType(SharedZAxisTransition), findsNWidgets(2)); - - // Rebuild the app without the transition. - await tester.pumpWidget( - MaterialApp( - navigatorKey: navigator, - home: const Material( - child: Text('abc'), - ), - ), - ); - await tester.pump(); - // Transitions should have been disposed of. - expect(find.byType(SharedZAxisTransition), findsNothing); - }, - ); - - testWidgets( - 'State is not lost when transitioning', - (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - const String bottomRoute = '/'; - const String topRoute = '/a'; - - await tester.pumpWidget( - _TestWidget( - navigatorKey: navigator, - contentBuilder: (RouteSettings settings) { - return _StatefulTestWidget( - key: ValueKey(settings.name), - name: settings.name, - ); - }, - ), - ); - - final _StatefulTestWidgetState bottomState = tester.state( - find.byKey(const ValueKey(bottomRoute)), - ); - expect(bottomState.widget.name, bottomRoute); - - navigator.currentState.pushNamed(topRoute); - await tester.pump(); - await tester.pump(); - - expect( - tester.state(find.byKey(const ValueKey(bottomRoute))), - bottomState, - ); - final _StatefulTestWidgetState topState = tester.state( - find.byKey(const ValueKey(topRoute)), - ); - expect(topState.widget.name, topRoute); - - await tester.pump(const Duration(milliseconds: 150)); - expect( - tester.state(find.byKey(const ValueKey(bottomRoute))), - bottomState, - ); - expect( - tester.state(find.byKey(const ValueKey(topRoute))), - topState, - ); - - await tester.pumpAndSettle(); - expect( - tester.state(find.byKey( - const ValueKey(bottomRoute), - skipOffstage: false, - )), - bottomState, - ); - expect( - tester.state(find.byKey(const ValueKey(topRoute))), - topState, - ); - - navigator.currentState.pop(); - await tester.pump(); - - expect( - tester.state(find.byKey(const ValueKey(bottomRoute))), - bottomState, - ); - expect( - tester.state(find.byKey(const ValueKey(topRoute))), - topState, - ); - - await tester.pump(const Duration(milliseconds: 150)); - expect( - tester.state(find.byKey(const ValueKey(bottomRoute))), - bottomState, - ); - expect( - tester.state(find.byKey(const ValueKey(topRoute))), - topState, - ); - - await tester.pumpAndSettle(); - expect( - tester.state(find.byKey(const ValueKey(bottomRoute))), - bottomState, - ); - expect(find.byKey(const ValueKey(topRoute)), findsNothing); - }, - ); -} - -double _getOpacity(String key, WidgetTester tester) { - final Finder finder = find.ancestor( - of: find.byKey(ValueKey(key)), - matching: find.byType(FadeTransition), - ); - return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { - final FadeTransition transition = widget; - return a * transition.opacity.value; - }); -} - -double _getScale(String key, WidgetTester tester) { - final Finder finder = find.ancestor( - of: find.byKey(ValueKey(key)), - matching: find.byType(ScaleTransition), - ); - return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { - final ScaleTransition transition = widget; - return a * transition.scale.value; - }); -} - -class _TestWidget extends StatelessWidget { - const _TestWidget({this.navigatorKey, this.contentBuilder}); - - final Key navigatorKey; - final _ContentBuilder contentBuilder; - - @override - Widget build(BuildContext context) { - return MaterialApp( - navigatorKey: navigatorKey, - theme: ThemeData( - platform: TargetPlatform.android, - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: SharedZAxisPageTransitionsBuilder(), - }, - ), - ), - onGenerateRoute: (RouteSettings settings) { - return MaterialPageRoute( - settings: settings, - builder: (BuildContext context) { - return contentBuilder != null - ? contentBuilder(settings) - : Container( - child: Center( - key: ValueKey(settings.name), - child: Text(settings.name), - ), - ); - }, - ); - }, - ); - } -} - -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); - } -} - -typedef _ContentBuilder = Widget Function(RouteSettings settings);