diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart index f7f6c3cdf69..e577a0a688d 100644 --- a/packages/flutter/lib/src/cupertino/route.dart +++ b/packages/flutter/lib/src/cupertino/route.dart @@ -272,8 +272,8 @@ class CupertinoPageRoute extends PageRoute { _CupertinoBackGestureController backController; backController = _CupertinoBackGestureController( - navigator: route.navigator, - controller: route.controller, + route: route, + controller: route.controller, // protected access onEnded: () { backController?.dispose(); backController = null; @@ -576,22 +576,15 @@ class _CupertinoBackGestureController { /// /// The [navigator] and [controller] arguments must not be null. _CupertinoBackGestureController({ - @required this.navigator, + @required this.route, @required this.controller, @required this.onEnded, - }) : assert(navigator != null), - assert(controller != null), - assert(onEnded != null) { - navigator.didStartUserGesture(); + }) : assert(route != null), assert(controller != null), assert(onEnded != null) { + route.navigator.didStartUserGesture(); } - /// The navigator that this object is controlling. - final NavigatorState navigator; - - /// The animation controller that the route uses to drive its transition - /// animation. + final PageRoute route; final AnimationController controller; - final VoidCallback onEnded; bool _animating = false; @@ -626,8 +619,10 @@ class _CupertinoBackGestureController { // The closer the panel is to dismissing, the shorter the animation is. // We want to cap the animation time, but we want to use a linear curve // to determine it. - final int droppedPageForwardAnimationTime = min(lerpDouble(_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value).floor(), - _kMaxPageBackAnimationTime); + final int droppedPageForwardAnimationTime = min( + lerpDouble(_kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value).floor(), + _kMaxPageBackAnimationTime, + ); controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve); } else { final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value).floor(); @@ -652,14 +647,14 @@ class _CupertinoBackGestureController { } _animating = false; if (status == AnimationStatus.dismissed) - navigator.pop(); // this will cause the route to get disposed, which will dispose us + route.navigator.removeRoute(route); // this will cause the route to get disposed, which will dispose us onEnded(); // this will call dispose if popping the route failed to do so } void dispose() { if (_animating) controller.removeStatusListener(_handleStatusChanged); - navigator.didStopUserGesture(); + route.navigator?.didStopUserGesture(); } } diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index b57170279c3..84ab501e473 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -151,11 +151,11 @@ abstract class TransitionRoute extends OverlayRoute { overlayEntries.first.opaque = false; break; case AnimationStatus.dismissed: - // We might still be the current route if a subclass is controlling the + // We might still be an active route if a subclass is controlling the // the transition and hits the dismissed status. For example, the iOS // back gesture drives this animation to the dismissed status before - // popping the navigator. - if (!isCurrent) { + // removing the route and disposing it. + if (!isActive) { navigator.finalizeRoute(this); assert(overlayEntries.isEmpty); } diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart index 1c2dd128f40..16396e2d3a2 100644 --- a/packages/flutter/test/cupertino/route_test.dart +++ b/packages/flutter/test/cupertino/route_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -253,4 +254,79 @@ void main() { expect(find.widgetWithText(CupertinoButton, 'Back'), findsOneWidget); expect(tester.getTopLeft(find.text('Back')).dx, 8.0 + 34.0 + 6.0); }); + + testWidgets('Back swipe dismiss interrupted by route push', (WidgetTester tester) async { + final GlobalKey scaffoldKey = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(platform: TargetPlatform.iOS), + home: Scaffold( + key: scaffoldKey, + body: Center( + child: RaisedButton( + onPressed: () { + Navigator.push(scaffoldKey.currentContext, MaterialPageRoute( + builder: (BuildContext context) { + return const Scaffold( + body: Center(child: Text('route')), + ); + }, + )); + }, + child: const Text('push'), + ), + ), + ), + ), + ); + + // Check the basic iOS back-swipe dismiss transition. Dragging the pushed + // route halfway across the screen will trigger the iOS dismiss animation + + await tester.tap(find.text('push')); + await tester.pumpAndSettle(); + expect(find.text('route'), findsOneWidget); + expect(find.text('push'), findsNothing); + + TestGesture gesture = await tester.startGesture(const Offset(5, 300)); + await gesture.moveBy(const Offset(400, 0)); + await gesture.up(); + await tester.pump(); + expect( // The 'route' route has been dragged to the right, halfway across the screen + tester.getTopLeft(find.ancestor(of: find.text('route'), matching: find.byType(Scaffold))), + const Offset(400, 0), + ); + expect( // The 'push' route is sliding in from the left. + tester.getTopLeft(find.ancestor(of: find.text('push'), matching: find.byType(Scaffold))).dx, + lessThan(0), + ); + await tester.pumpAndSettle(); + expect(find.text('push'), findsOneWidget); + expect( + tester.getTopLeft(find.ancestor(of: find.text('push'), matching: find.byType(Scaffold))), + Offset.zero, + ); + expect(find.text('route'), findsNothing); + + + // Run the dismiss animation 75%, which exposes the route "push" button, + // and then press the button. MaterialPageTransition duration is 300ms, + // 275 = 300 * 0.75. + + await tester.tap(find.text('push')); + await tester.pumpAndSettle(); + expect(find.text('route'), findsOneWidget); + expect(find.text('push'), findsNothing); + + gesture = await tester.startGesture(const Offset(5, 300)); + await gesture.moveBy(const Offset(400, 0)); // drag halfway + await gesture.up(); + await tester.pump(const Duration(milliseconds: 275)); // partially dismiss "route" + expect(find.text('route'), findsOneWidget); + await tester.tap(find.text('push')); + await tester.pumpAndSettle(); + expect(find.text('route'), findsOneWidget); + expect(find.text('push'), findsNothing); + }); }