mirror of
https://github.com/flutter/flutter
synced 2024-08-24 18:36:03 +00:00
Support navigation during a Cupertino back gesture (#142248)
Fixes a bug where programmatically navigating during an iOS back gesture caused the app to enter an unstable state.
This commit is contained in:
parent
ac7879e2aa
commit
3280be9371
|
@ -158,7 +158,7 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
|
|||
|
||||
/// True if an iOS-style back swipe pop gesture is currently underway for [route].
|
||||
///
|
||||
/// This just check the route's [NavigatorState.userGestureInProgress].
|
||||
/// This just checks the route's [NavigatorState.userGestureInProgress].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
@ -247,6 +247,8 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
|
|||
|
||||
return _CupertinoBackGestureController<T>(
|
||||
navigator: route.navigator!,
|
||||
getIsCurrent: () => route.isCurrent,
|
||||
getIsActive: () => route.isActive,
|
||||
controller: route.controller!, // protected access
|
||||
);
|
||||
}
|
||||
|
@ -293,6 +295,8 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
|
|||
child: _CupertinoBackGestureDetector<T>(
|
||||
enabledCallback: () => _isPopGestureEnabled<T>(route),
|
||||
onStartPopGesture: () => _startPopGesture<T>(route),
|
||||
getIsCurrent: () => route.isCurrent,
|
||||
getIsActive: () => route.isActive,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
@ -596,6 +600,8 @@ class _CupertinoBackGestureDetector<T> extends StatefulWidget {
|
|||
required this.enabledCallback,
|
||||
required this.onStartPopGesture,
|
||||
required this.child,
|
||||
required this.getIsActive,
|
||||
required this.getIsCurrent,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
@ -604,6 +610,9 @@ class _CupertinoBackGestureDetector<T> extends StatefulWidget {
|
|||
|
||||
final ValueGetter<_CupertinoBackGestureController<T>> onStartPopGesture;
|
||||
|
||||
final ValueGetter<bool> getIsActive;
|
||||
final ValueGetter<bool> getIsCurrent;
|
||||
|
||||
@override
|
||||
_CupertinoBackGestureDetectorState<T> createState() => _CupertinoBackGestureDetectorState<T>();
|
||||
}
|
||||
|
@ -724,12 +733,16 @@ class _CupertinoBackGestureController<T> {
|
|||
_CupertinoBackGestureController({
|
||||
required this.navigator,
|
||||
required this.controller,
|
||||
required this.getIsActive,
|
||||
required this.getIsCurrent,
|
||||
}) {
|
||||
navigator.didStartUserGesture();
|
||||
}
|
||||
|
||||
final AnimationController controller;
|
||||
final NavigatorState navigator;
|
||||
final ValueGetter<bool> getIsActive;
|
||||
final ValueGetter<bool> getIsCurrent;
|
||||
|
||||
/// The drag gesture has changed by [fractionalDelta]. The total range of the
|
||||
/// drag should be 0.0 to 1.0.
|
||||
|
@ -745,12 +758,21 @@ class _CupertinoBackGestureController<T> {
|
|||
// This curve has been determined through rigorously eyeballing native iOS
|
||||
// animations.
|
||||
const Curve animationCurve = Curves.fastLinearToSlowEaseIn;
|
||||
final bool isCurrent = getIsCurrent();
|
||||
final bool animateForward;
|
||||
|
||||
// If the user releases the page before mid screen with sufficient velocity,
|
||||
// or after mid screen, we should animate the page out. Otherwise, the page
|
||||
// should be animated back in.
|
||||
if (velocity.abs() >= _kMinFlingVelocity) {
|
||||
if (!isCurrent) {
|
||||
// If the page has already been navigated away from, then the animation
|
||||
// direction depends on whether or not it's still in the navigation stack,
|
||||
// regardless of velocity or drag position. For example, if a route is
|
||||
// being slowly dragged back by just a few pixels, but then a programmatic
|
||||
// pop occurs, the route should still be animated off the screen.
|
||||
// See https://github.com/flutter/flutter/issues/141268.
|
||||
animateForward = getIsActive();
|
||||
} else if (velocity.abs() >= _kMinFlingVelocity) {
|
||||
// If the user releases the page before mid screen with sufficient velocity,
|
||||
// or after mid screen, we should animate the page out. Otherwise, the page
|
||||
// should be animated back in.
|
||||
animateForward = velocity <= 0;
|
||||
} else {
|
||||
animateForward = controller.value > 0.5;
|
||||
|
@ -766,8 +788,10 @@ class _CupertinoBackGestureController<T> {
|
|||
);
|
||||
controller.animateTo(1.0, duration: Duration(milliseconds: droppedPageForwardAnimationTime), curve: animationCurve);
|
||||
} else {
|
||||
// This route is destined to pop at this point. Reuse navigator's pop.
|
||||
navigator.pop();
|
||||
if (isCurrent) {
|
||||
// This route is destined to pop at this point. Reuse navigator's pop.
|
||||
navigator.pop();
|
||||
}
|
||||
|
||||
// The popping may have finished inline if already at the target destination.
|
||||
if (controller.isAnimating) {
|
||||
|
|
|
@ -2919,7 +2919,7 @@ class _RouteEntry extends RouteTransitionRecord {
|
|||
initialState == _RouteLifecycle.pushReplace ||
|
||||
initialState == _RouteLifecycle.replace,
|
||||
),
|
||||
currentState = initialState {
|
||||
currentState = initialState {
|
||||
// TODO(polina-c): stop duplicating code across disposables
|
||||
// https://github.com/flutter/flutter/issues/137435
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
|
|
|
@ -377,6 +377,272 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets('Back swipe less than halfway is interrupted by route pop', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/141268
|
||||
final GlobalKey scaffoldKey = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
key: scaffoldKey,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Text('Page 1'),
|
||||
CupertinoButton(
|
||||
onPressed: () {
|
||||
Navigator.push<void>(scaffoldKey.currentContext!, CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const CupertinoPageScaffold(
|
||||
child: Center(child: Text('Page 2')),
|
||||
);
|
||||
},
|
||||
));
|
||||
},
|
||||
child: const Text('Push Page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Page 1'), findsOneWidget);
|
||||
expect(find.text('Page 2'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Push Page 2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Page 1'), findsNothing);
|
||||
expect(find.text('Page 2'), findsOneWidget);
|
||||
|
||||
// Start a back gesture and move it less than 50% across the screen.
|
||||
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 300.0));
|
||||
await gesture.moveBy(const Offset(100.0, 0.0));
|
||||
await tester.pump();
|
||||
expect( // The second route has been dragged to the right.
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold))),
|
||||
const Offset(100.0, 0.0),
|
||||
);
|
||||
expect( // The first route is sliding in from the left.
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold))).dx,
|
||||
lessThan(0),
|
||||
);
|
||||
|
||||
// Programmatically pop and observe that Page 2 was popped as if there were
|
||||
// no back gesture.
|
||||
Navigator.pop<void>(scaffoldKey.currentContext!);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold))),
|
||||
Offset.zero,
|
||||
);
|
||||
expect(find.text('Page 2'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Back swipe more than halfway is interrupted by route pop', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/141268
|
||||
final GlobalKey scaffoldKey = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
key: scaffoldKey,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Text('Page 1'),
|
||||
CupertinoButton(
|
||||
onPressed: () {
|
||||
Navigator.push<void>(scaffoldKey.currentContext!, CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const CupertinoPageScaffold(
|
||||
child: Center(child: Text('Page 2')),
|
||||
);
|
||||
},
|
||||
));
|
||||
},
|
||||
child: const Text('Push Page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Page 1'), findsOneWidget);
|
||||
expect(find.text('Page 2'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Push Page 2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Page 1'), findsNothing);
|
||||
expect(find.text('Page 2'), findsOneWidget);
|
||||
|
||||
// Start a back gesture and move it more than 50% across the screen.
|
||||
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 300.0));
|
||||
await gesture.moveBy(const Offset(500.0, 0.0));
|
||||
await tester.pump();
|
||||
expect( // The second route has been dragged to the right.
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold))),
|
||||
const Offset(500.0, 0.0),
|
||||
);
|
||||
expect( // The first route is sliding in from the left.
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold))).dx,
|
||||
lessThan(0),
|
||||
);
|
||||
|
||||
// Programmatically pop and observe that Page 2 was popped as if there were
|
||||
// no back gesture.
|
||||
Navigator.pop<void>(scaffoldKey.currentContext!);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold))),
|
||||
Offset.zero,
|
||||
);
|
||||
expect(find.text('Page 2'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Back swipe less than halfway is interrupted by route push', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/141268
|
||||
final GlobalKey scaffoldKey = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
key: scaffoldKey,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Text('Page 1'),
|
||||
CupertinoButton(
|
||||
onPressed: () {
|
||||
Navigator.push<void>(scaffoldKey.currentContext!, CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const CupertinoPageScaffold(
|
||||
child: Center(child: Text('Page 2')),
|
||||
);
|
||||
},
|
||||
));
|
||||
},
|
||||
child: const Text('Push Page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Page 1'), findsOneWidget);
|
||||
expect(find.text('Page 2'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Push Page 2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Page 1'), findsNothing);
|
||||
expect(find.text('Page 2'), findsOneWidget);
|
||||
|
||||
// Start a back gesture and move it less than 50% across the screen.
|
||||
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 300.0));
|
||||
await gesture.moveBy(const Offset(100.0, 0.0));
|
||||
await tester.pump();
|
||||
expect( // The second route has been dragged to the right.
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold))),
|
||||
const Offset(100.0, 0.0),
|
||||
);
|
||||
expect( // The first route is sliding in from the left.
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold))).dx,
|
||||
lessThan(0),
|
||||
);
|
||||
|
||||
// Programmatically push and observe that Page 3 was pushed as if there were
|
||||
// no back gesture.
|
||||
Navigator.push<void>(scaffoldKey.currentContext!, CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const CupertinoPageScaffold(
|
||||
child: Center(child: Text('Page 3')),
|
||||
);
|
||||
},
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Page 1'), findsNothing);
|
||||
expect(find.text('Page 2'), findsNothing);
|
||||
expect(
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 3'), matching: find.byType(CupertinoPageScaffold))),
|
||||
Offset.zero,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Back swipe more than halfway is interrupted by route push', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/141268
|
||||
final GlobalKey scaffoldKey = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
key: scaffoldKey,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const Text('Page 1'),
|
||||
CupertinoButton(
|
||||
onPressed: () {
|
||||
Navigator.push<void>(scaffoldKey.currentContext!, CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const CupertinoPageScaffold(
|
||||
child: Center(child: Text('Page 2')),
|
||||
);
|
||||
},
|
||||
));
|
||||
},
|
||||
child: const Text('Push Page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Page 1'), findsOneWidget);
|
||||
expect(find.text('Page 2'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Push Page 2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Page 1'), findsNothing);
|
||||
expect(find.text('Page 2'), findsOneWidget);
|
||||
|
||||
// Start a back gesture and move it more than 50% across the screen.
|
||||
final TestGesture gesture = await tester.startGesture(const Offset(5.0, 300.0));
|
||||
await gesture.moveBy(const Offset(500.0, 0.0));
|
||||
await tester.pump();
|
||||
expect( // The second route has been dragged to the right.
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 2'), matching: find.byType(CupertinoPageScaffold))),
|
||||
const Offset(500.0, 0.0),
|
||||
);
|
||||
expect( // The first route is sliding in from the left.
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 1'), matching: find.byType(CupertinoPageScaffold))).dx,
|
||||
lessThan(0),
|
||||
);
|
||||
|
||||
// Programmatically push and observe that Page 3 was pushed as if there were
|
||||
// no back gesture.
|
||||
Navigator.push<void>(scaffoldKey.currentContext!, CupertinoPageRoute<void>(
|
||||
builder: (BuildContext context) {
|
||||
return const CupertinoPageScaffold(
|
||||
child: Center(child: Text('Page 3')),
|
||||
);
|
||||
},
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Page 1'), findsNothing);
|
||||
expect(find.text('Page 2'), findsNothing);
|
||||
expect(
|
||||
tester.getTopLeft(find.ancestor(of: find.text('Page 3'), matching: find.byType(CupertinoPageScaffold))),
|
||||
Offset.zero,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Fullscreen route animates correct transform values over time', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
|
|
Loading…
Reference in a new issue