From 51e24a3561ee4f50a2b4ddeb36b1f7ea59d768d2 Mon Sep 17 00:00:00 2001 From: Shi-Hao Hong Date: Thu, 9 Jan 2020 09:31:38 -0800 Subject: [PATCH] Implement reverseTransitionDuration for TransitionRoute (#48274) * Implement reverseTransitionDuration in TransitionRoute --- packages/flutter/lib/src/widgets/routes.dart | 15 +- .../flutter/test/widgets/routes_test.dart | 199 +++++++++++++++++- 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index b9041f7283..2ad8276c57 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -91,9 +91,20 @@ abstract class TransitionRoute extends OverlayRoute { Future get completed => _transitionCompleter.future; final Completer _transitionCompleter = Completer(); - /// The duration the transition lasts. + /// The duration the transition going forwards. + /// + /// See also: + /// + /// * [reverseTransitionDuration], which controls the duration of the + /// transition when it is in reverse. Duration get transitionDuration; + /// The duration the transition going in reverse. + /// + /// By default, the reverse transition duration is set to the value of + /// the forwards [transitionDuration]. + Duration get reverseTransitionDuration => transitionDuration; + /// Whether the route obscures previous routes when the transition is complete. /// /// When an opaque route's entrance transition is complete, the routes behind @@ -127,9 +138,11 @@ abstract class TransitionRoute extends OverlayRoute { AnimationController createAnimationController() { assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); final Duration duration = transitionDuration; + final Duration reverseDuration = reverseTransitionDuration; assert(duration != null && duration >= Duration.zero); return AnimationController( duration: duration, + reverseDuration: reverseDuration, debugLabel: debugLabel, vsync: navigator, ); diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart index 32bde1e1a1..d30947369b 100644 --- a/packages/flutter/test/widgets/routes_test.dart +++ b/packages/flutter/test/widgets/routes_test.dart @@ -534,7 +534,7 @@ void main() { expect(focusNode.hasPrimaryFocus, isTrue); }); - group('TrasitionRoute', () { + group('TransitionRoute', () { testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async { final GlobalKey navigator = GlobalKey(); await tester.pumpWidget( @@ -863,9 +863,206 @@ void main() { expect(rootObserver.dialogCount, 0); expect(nestedObserver.dialogCount, 1); }); + + testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async { + final GlobalKey containerKey = GlobalKey(); + + // Default MaterialPageRoute transition duration should be 300ms. + await tester.pumpWidget(MaterialApp( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext innerContext) { + return Container( + key: containerKey, + color: Colors.green, + ); + }, + ), + ); + }, + child: const Text('Open page'), + ); + }, + ); + }, + )); + + // Open the new route. + await tester.tap(find.byType(RaisedButton)); + await tester.pumpAndSettle(); + expect(find.text('Open page'), findsNothing); + expect(find.byKey(containerKey), findsOneWidget); + + // Pop the new route. + tester.state(find.byType(Navigator)).pop(); + await tester.pump(); + expect(find.byKey(containerKey), findsOneWidget); + + // Container should be present halfway through the transition. + await tester.pump(const Duration(milliseconds: 150)); + expect(find.byKey(containerKey), findsOneWidget); + + // Container should be present at the very end of the transition. + await tester.pump(const Duration(milliseconds: 150)); + expect(find.byKey(containerKey), findsOneWidget); + + // Container have transitioned out after 300ms. + await tester.pump(const Duration(milliseconds: 1)); + expect(find.byKey(containerKey), findsNothing); + }); + + testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async { + final GlobalKey containerKey = GlobalKey(); + await tester.pumpWidget(MaterialApp( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + Navigator.of(context).push( + ModifiedReverseTransitionDurationRoute( + builder: (BuildContext innerContext) { + return Container( + key: containerKey, + color: Colors.green, + ); + }, + // modified value, default MaterialPageRoute transition duration should be 300ms. + reverseTransitionDuration: const Duration(milliseconds: 150), + ), + ); + }, + child: const Text('Open page'), + ); + }, + ); + }, + )); + + // Open the new route. + await tester.tap(find.byType(RaisedButton)); + await tester.pumpAndSettle(); + expect(find.text('Open page'), findsNothing); + expect(find.byKey(containerKey), findsOneWidget); + + // Pop the new route. + tester.state(find.byType(Navigator)).pop(); + await tester.pump(); + expect(find.byKey(containerKey), findsOneWidget); + + // Container should be present halfway through the transition. + await tester.pump(const Duration(milliseconds: 75)); + expect(find.byKey(containerKey), findsOneWidget); + + // Container should be present at the very end of the transition. + await tester.pump(const Duration(milliseconds: 75)); + expect(find.byKey(containerKey), findsOneWidget); + + // Container have transitioned out after 150ms. + await tester.pump(const Duration(milliseconds: 1)); + expect(find.byKey(containerKey), findsNothing); + }); + + testWidgets('custom reverseTransitionDuration does not result in interrupted animations', (WidgetTester tester) async { + final GlobalKey containerKey = GlobalKey(); + await tester.pumpWidget(MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), // use a fade transition + }, + ), + ), + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return RaisedButton( + onPressed: () { + Navigator.of(context).push( + ModifiedReverseTransitionDurationRoute( + builder: (BuildContext innerContext) { + return Container( + key: containerKey, + color: Colors.green, + ); + }, + // modified value, default MaterialPageRoute transition duration should be 300ms. + reverseTransitionDuration: const Duration(milliseconds: 150), + ), + ); + }, + child: const Text('Open page'), + ); + }, + ); + }, + )); + + // Open the new route. + await tester.tap(find.byType(RaisedButton)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); // jump partway through the forward transition + expect(find.byKey(containerKey), findsOneWidget); + + // Gets the opacity of the fade transition while animating forwards. + final double topFadeTransitionOpacity = _getOpacity(containerKey, tester); + + // Pop the new route mid-transition. + tester.state(find.byType(Navigator)).pop(); + await tester.pump(); + + // Transition should not jump. In other words, the fade transition + // opacity before and after animation changes directions should remain + // the same. + expect(_getOpacity(containerKey, tester), topFadeTransitionOpacity); + + // Reverse transition duration should be: + // Forward transition elapsed time: 200ms / 300ms = 2 / 3 + // Reverse transition remaining time: 150ms * 2 / 3 = 100ms + + // Container should be present at the very end of the transition. + await tester.pump(const Duration(milliseconds: 100)); + expect(find.byKey(containerKey), findsOneWidget); + + // Container have transitioned out after 100ms. + await tester.pump(const Duration(milliseconds: 1)); + expect(find.byKey(containerKey), findsNothing); + }); }); } +double _getOpacity(GlobalKey key, WidgetTester tester) { + final Finder finder = find.ancestor( + of: find.byKey(key), + matching: find.byType(FadeTransition), + ); + return tester.widgetList(finder).fold(1.0, (double a, Widget widget) { + final FadeTransition transition = widget as FadeTransition; + return a * transition.opacity.value; + }); +} + +class ModifiedReverseTransitionDurationRoute extends MaterialPageRoute { + ModifiedReverseTransitionDurationRoute({ + @required WidgetBuilder builder, + RouteSettings settings, + this.reverseTransitionDuration, + bool fullscreenDialog = false, + }) : super( + builder: builder, + settings: settings, + fullscreenDialog: fullscreenDialog, + ); + + @override + final Duration reverseTransitionDuration; +} + class MockPageRoute extends Mock implements PageRoute { } class MockRoute extends Mock implements Route { }