Unverified Commit 1c332cae authored by xster's avatar xster Committed by GitHub

Commit a navigator.pop as soon as the back swipe is lifted (#30422)

parent fdf9a453
...@@ -270,16 +270,10 @@ class CupertinoPageRoute<T> extends PageRoute<T> { ...@@ -270,16 +270,10 @@ class CupertinoPageRoute<T> extends PageRoute<T> {
static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) { static _CupertinoBackGestureController<T> _startPopGesture<T>(PageRoute<T> route) {
assert(_isPopGestureEnabled(route)); assert(_isPopGestureEnabled(route));
_CupertinoBackGestureController<T> backController; return _CupertinoBackGestureController<T>(
backController = _CupertinoBackGestureController<T>( navigator: route.navigator,
route: route,
controller: route.controller, // protected access controller: route.controller, // protected access
onEnded: () {
backController?.dispose();
backController = null;
},
); );
return backController;
} }
/// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full /// Returns a [CupertinoFullscreenDialogTransition] if [route] is a full
...@@ -592,18 +586,15 @@ class _CupertinoBackGestureController<T> { ...@@ -592,18 +586,15 @@ class _CupertinoBackGestureController<T> {
/// ///
/// The [navigator] and [controller] arguments must not be null. /// The [navigator] and [controller] arguments must not be null.
_CupertinoBackGestureController({ _CupertinoBackGestureController({
@required this.route, @required this.navigator,
@required this.controller, @required this.controller,
@required this.onEnded, }) : assert(navigator != null),
}) : assert(route != null), assert(controller != null), assert(onEnded != null) { assert(controller != null) {
route.navigator.didStartUserGesture(); navigator.didStartUserGesture();
} }
final PageRoute<T> route;
final AnimationController controller; final AnimationController controller;
final VoidCallback onEnded; final NavigatorState navigator;
bool _animating = false;
/// The drag gesture has changed by [fractionalDelta]. The total range of the /// The drag gesture has changed by [fractionalDelta]. The total range of the
/// drag should be 0.0 to 1.0. /// drag should be 0.0 to 1.0.
...@@ -640,36 +631,28 @@ class _CupertinoBackGestureController<T> { ...@@ -640,36 +631,28 @@ class _CupertinoBackGestureController<T> {
); );
controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve); controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve);
} else { } else {
final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value).floor(); // This route is destined to pop at this point. Reuse navigator's pop.
controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve); navigator.pop();
}
// The popping may have finished inline if already at the target destination.
if (controller.isAnimating) { if (controller.isAnimating) {
// Don't end the gesture until the transition completes. // Otherwise, use a custom popping animation duration and curve.
_animating = true; final int droppedPageBackAnimationTime = lerpDouble(0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value).floor();
controller.addStatusListener(_handleStatusChanged); controller.animateBack(0.0, duration: Duration(milliseconds: droppedPageBackAnimationTime), curve: animationCurve);
} else {
// Animate calls could return inline if already at the target destination
// value.
return _handleStatusChanged(controller.status);
} }
} }
void _handleStatusChanged(AnimationStatus status) { if (controller.isAnimating) {
if (_animating) { // Keep the userGestureInProgress in true state so we don't change the
controller.removeStatusListener(_handleStatusChanged); // curve of the page transition mid-flight since CupertinoPageTransition
} // depends on userGestureInProgress.
_animating = false; AnimationStatusListener animationStatusCallback;
onEnded(); animationStatusCallback = (AnimationStatus status) {
if (status == AnimationStatus.dismissed) navigator.didStopUserGesture();
route.navigator.removeRoute(route); // This also disposes the route. controller.removeStatusListener(animationStatusCallback);
};
controller.addStatusListener(animationStatusCallback);
} }
void dispose() {
if (_animating)
controller.removeStatusListener(_handleStatusChanged);
route.navigator?.didStopUserGesture();
} }
} }
......
...@@ -620,6 +620,9 @@ class HeroController extends NavigatorObserver { ...@@ -620,6 +620,9 @@ class HeroController extends NavigatorObserver {
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
assert(navigator != null); assert(navigator != null);
assert(route != null); assert(route != null);
// Don't trigger another flight when a pop is committed as a user gesture
// back swipe is snapped.
if (!navigator.userGestureInProgress)
_maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop, false); _maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop, false);
} }
......
...@@ -462,39 +462,6 @@ void main() { ...@@ -462,39 +462,6 @@ void main() {
expect(find.text('Page 1'), findsNothing); expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage); expect(find.text('Page 2'), isOnstage);
}); });
testWidgets('test edge swipe then drop back at ending point works', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2';
return Center(child: Text('Page $pageNumber'));
},
);
},
),
);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
final TestGesture gesture = await tester.startGesture(const Offset(5, 200));
// The width of the page.
await gesture.moveBy(const Offset(800, 0));
await gesture.up();
await tester.pump();
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
});
} }
class RtlOverrideWidgetsDelegate extends LocalizationsDelegate<WidgetsLocalizations> { class RtlOverrideWidgetsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
......
...@@ -6,7 +6,15 @@ import 'package:flutter/cupertino.dart'; ...@@ -6,7 +6,15 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
void main() { void main() {
MockNavigatorObserver navigatorObserver;
setUp(() {
navigatorObserver = MockNavigatorObserver();
});
testWidgets('Middle auto-populates with title', (WidgetTester tester) async { testWidgets('Middle auto-populates with title', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const CupertinoApp( const CupertinoApp(
...@@ -579,10 +587,11 @@ void main() { ...@@ -579,10 +587,11 @@ void main() {
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(787, epsilon: 1)); expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(787, epsilon: 1));
}); });
testWidgets('Snapped drags forwards and backwards should signal didStopUserGesture', (WidgetTester tester) async { testWidgets('Snapped drags forwards and backwards should signal didStart/StopUserGesture', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
navigatorObservers: <NavigatorObserver>[navigatorObserver],
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
home: const Text('1'), home: const Text('1'),
), ),
...@@ -598,8 +607,10 @@ void main() { ...@@ -598,8 +607,10 @@ void main() {
navigatorKey.currentState.push(route2); navigatorKey.currentState.push(route2);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
verify(navigatorObserver.didPush(any, any)).called(greaterThanOrEqualTo(1));
await tester.dragFrom(const Offset(5, 100), const Offset(100, 0)); await tester.dragFrom(const Offset(5, 100), const Offset(100, 0));
verify(navigatorObserver.didStartUserGesture(any, any)).called(1);
await tester.pump(); await tester.pump();
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(100)); expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(100));
expect(navigatorKey.currentState.userGestureInProgress, true); expect(navigatorKey.currentState.userGestureInProgress, true);
...@@ -610,11 +621,14 @@ void main() { ...@@ -610,11 +621,14 @@ void main() {
// Back to the page covering the whole screen. // Back to the page covering the whole screen.
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(0)); expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(0));
expect(navigatorKey.currentState.userGestureInProgress, false); expect(navigatorKey.currentState.userGestureInProgress, false);
verify(navigatorObserver.didStopUserGesture()).called(1);
verifyNever(navigatorObserver.didPop(any, any));
await tester.dragFrom(const Offset(5, 100), const Offset(500, 0)); await tester.dragFrom(const Offset(5, 100), const Offset(500, 0));
await tester.pump(); await tester.pump();
expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(500)); expect(tester.getTopLeft(find.text('2')).dx, moreOrLessEquals(500));
expect(navigatorKey.currentState.userGestureInProgress, true); expect(navigatorKey.currentState.userGestureInProgress, true);
verify(navigatorObserver.didPop(any, any)).called(1);
// Did go far enough to snap out of this route. // Did go far enough to snap out of this route.
await tester.pump(const Duration(milliseconds: 301)); await tester.pump(const Duration(milliseconds: 301));
...@@ -624,4 +638,42 @@ void main() { ...@@ -624,4 +638,42 @@ void main() {
expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(0)); expect(tester.getTopLeft(find.text('1')).dx, moreOrLessEquals(0));
expect(navigatorKey.currentState.userGestureInProgress, false); expect(navigatorKey.currentState.userGestureInProgress, false);
}); });
/// Regression test for https://github.com/flutter/flutter/issues/29596.
testWidgets('test edge swipe then drop back at ending point works', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
navigatorObservers: <NavigatorObserver>[navigatorObserver],
onGenerateRoute: (RouteSettings settings) {
return CupertinoPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
final String pageNumber = settings.name == '/' ? '1' : '2';
return Center(child: Text('Page $pageNumber'));
},
);
},
),
);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), isOnstage);
final TestGesture gesture = await tester.startGesture(const Offset(5, 200));
// The width of the page.
await gesture.moveBy(const Offset(800, 0));
await gesture.up();
await tester.pump();
expect(find.text('Page 1'), isOnstage);
expect(find.text('Page 2'), findsNothing);
verify(navigatorObserver.didPop(any, any)).called(1);
});
} }
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -1779,4 +1780,67 @@ void main() { ...@@ -1779,4 +1780,67 @@ void main() {
expect(find.byKey(smallContainer), isInCard); expect(find.byKey(smallContainer), isInCard);
expect(tester.getSize(find.byKey(smallContainer)), const Size(100,100)); expect(tester.getSize(find.byKey(smallContainer)), const Size(100,100));
}); });
testWidgets('On an iOS back swipe and snap, only a single flight should take place', (WidgetTester tester) async {
int shuttlesBuilt = 0;
final HeroFlightShuttleBuilder shuttleBuilder = (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
shuttlesBuilt += 1;
return const Text("I'm flying in a jetplane");
};
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
await tester.pumpWidget(
CupertinoApp(
navigatorKey: navigatorKey,
home: Hero(
tag: navigatorKey,
// Since we're popping, only the destination route's builder is used.
flightShuttleBuilder: shuttleBuilder,
transitionOnUserGestures: true,
child: const Text('1')
),
),
);
final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
child: Hero(
tag: navigatorKey,
transitionOnUserGestures: true,
child: const Text('2')
),
);
}
);
navigatorKey.currentState.push(route2);
await tester.pumpAndSettle();
expect(shuttlesBuilt, 1);
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0));
await gesture.moveBy(const Offset(500.0, 0.0));
await tester.pump();
// Starting the back swipe creates a new hero shuttle.
expect(shuttlesBuilt, 2);
await gesture.up();
await tester.pump();
// After the lift, no additional shuttles should be created since it's the
// same hero flight.
expect(shuttlesBuilt, 2);
// Did go far enough to snap out of this route.
await tester.pump(const Duration(milliseconds: 301));
expect(find.text('2'), findsNothing);
// Still one shuttle.
expect(shuttlesBuilt, 2);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment