Added Navigator.removeRoute() (#10298)
This commit is contained in:
parent
69c2542458
commit
0cadbce490
@ -328,6 +328,10 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
|
||||
child: menu,
|
||||
);
|
||||
}
|
||||
|
||||
void _dismiss() {
|
||||
navigator?.removeRoute(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// An item in a menu created by a [DropdownButton].
|
||||
@ -486,7 +490,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
//TODO(hansmuller) if _dropDownRoute != null Navigator.remove(context, _dropdownRoute)
|
||||
_removeDropdownRoute();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -494,7 +498,11 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
||||
// Defined by WidgetsBindingObserver
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
//TODO(hansmuller) if _dropDownRoute != null Navigator.remove(context, _dropdownRoute)
|
||||
_removeDropdownRoute();
|
||||
}
|
||||
|
||||
void _removeDropdownRoute() {
|
||||
_dropdownRoute?._dismiss();
|
||||
_dropdownRoute = null;
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'binding.dart';
|
||||
@ -231,12 +232,15 @@ class NavigatorObserver {
|
||||
NavigatorState get navigator => _navigator;
|
||||
NavigatorState _navigator;
|
||||
|
||||
/// The [Navigator] pushed the given route.
|
||||
/// The [Navigator] pushed `route`.
|
||||
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { }
|
||||
|
||||
/// The [Navigator] popped the given route.
|
||||
/// The [Navigator] popped `route`.
|
||||
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { }
|
||||
|
||||
/// The [Navigator] removed `route`.
|
||||
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) { }
|
||||
|
||||
/// The [Navigator] is being controlled by a user gesture.
|
||||
///
|
||||
/// Used for the iOS back gesture.
|
||||
@ -673,6 +677,19 @@ class Navigator extends StatefulWidget {
|
||||
return Navigator.of(context).pushReplacement(route, result: result);
|
||||
}
|
||||
|
||||
/// Immediately remove `route` and [Route.dispose] it.
|
||||
///
|
||||
/// The route's animation does not run and the future returned from pushing
|
||||
/// the route will not complete. Ongoing input gestures are cancelled. If
|
||||
/// the [Navigator] has any [Navigator.observers], they will be notified with
|
||||
/// [NavigatorObserver.didRemove].
|
||||
///
|
||||
/// This method is used to dismiss dropdown menus that are up when the screen's
|
||||
/// orientation changes.
|
||||
static void removeRoute(BuildContext context, Route<dynamic> route) {
|
||||
return Navigator.of(context).removeRoute(route);
|
||||
}
|
||||
|
||||
/// The state from the closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
@ -1100,6 +1117,36 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Immediately remove `route` and [Route.dispose] it.
|
||||
///
|
||||
/// The route's animation does not run and the future returned from pushing
|
||||
/// the route will not complete. Ongoing input gestures are cancelled. If
|
||||
/// the [Navigator] has any [Navigator.observers], they will be notified with
|
||||
/// [NavigatorObserver.didRemove].
|
||||
///
|
||||
/// This method is used to dismiss dropdown menus that are up when the screen's
|
||||
/// orientation changes.
|
||||
void removeRoute(Route<dynamic> route) {
|
||||
assert(route != null);
|
||||
assert(!_debugLocked);
|
||||
assert(() { _debugLocked = true; return true; });
|
||||
assert(route._navigator == this);
|
||||
final int index = _history.indexOf(route);
|
||||
assert(index != -1);
|
||||
final Route<dynamic> previousRoute = index > 0 ? _history[index - 1] : null;
|
||||
final Route<dynamic> nextRoute = (index + 1 < _history.length) ? _history[index + 1] : null;
|
||||
setState(() {
|
||||
_history.removeAt(index);
|
||||
previousRoute?.didChangeNext(nextRoute);
|
||||
nextRoute?.didChangePrevious(previousRoute);
|
||||
for (NavigatorObserver observer in widget.observers)
|
||||
observer.didRemove(route, previousRoute);
|
||||
route.dispose();
|
||||
});
|
||||
assert(() { _debugLocked = false; return true; });
|
||||
_cancelActivePointers();
|
||||
}
|
||||
|
||||
/// Complete the lifecycle for a route that has been popped off the navigator.
|
||||
///
|
||||
/// When the navigator pops a route, the navigator retains a reference to the
|
||||
@ -1178,10 +1225,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
|
||||
|
||||
void _cancelActivePointers() {
|
||||
// TODO(abarth): This mechanism is far from perfect. See https://github.com/flutter/flutter/issues/4770
|
||||
final RenderAbsorbPointer absorber = _overlayKey.currentContext?.ancestorRenderObjectOfType(const TypeMatcher<RenderAbsorbPointer>());
|
||||
setState(() {
|
||||
absorber?.absorbing = true;
|
||||
});
|
||||
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
|
||||
// If we're between frames (SchedulerPhase.idle) then absorb any
|
||||
// subsequent pointers from this frame. The absorbing flag will be
|
||||
// reset in the next frame, see build().
|
||||
final RenderAbsorbPointer absorber = _overlayKey.currentContext?.ancestorRenderObjectOfType(const TypeMatcher<RenderAbsorbPointer>());
|
||||
setState(() {
|
||||
absorber?.absorbing = true;
|
||||
});
|
||||
}
|
||||
for (int pointer in _activePointers.toList())
|
||||
WidgetsBinding.instance.cancelPointer(pointer);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show window;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -463,4 +464,16 @@ void main() {
|
||||
expect(menuRect.bottomLeft, new Offset(800.0 - menuRect.width, 600.0));
|
||||
expect(menuRect.bottomRight, const Offset(800.0, 600.0));
|
||||
});
|
||||
|
||||
testWidgets('Dropdown menus are dismissed on screen orientation changes', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(buildFrame());
|
||||
await tester.tap(find.byType(dropdownButtonType));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ListView), findsOneWidget);
|
||||
|
||||
window.onMetricsChanged();
|
||||
await tester.pump();
|
||||
expect(find.byType(ListView, skipOffstage: false), findsNothing);
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
|
||||
class FirstWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new GestureDetector(
|
||||
return new GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/second');
|
||||
},
|
||||
@ -85,12 +85,12 @@ class OnTapPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
typedef void OnPushed(Route<dynamic> route, Route<dynamic> previousRoute);
|
||||
typedef void OnPopped(Route<dynamic> route, Route<dynamic> previousRoute);
|
||||
typedef void OnObservation(Route<dynamic> route, Route<dynamic> previousRoute);
|
||||
|
||||
class TestObserver extends NavigatorObserver {
|
||||
OnPushed onPushed;
|
||||
OnPopped onPopped;
|
||||
OnObservation onPushed;
|
||||
OnObservation onPopped;
|
||||
OnObservation onRemoved;
|
||||
|
||||
@override
|
||||
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
|
||||
@ -105,6 +105,12 @@ class TestObserver extends NavigatorObserver {
|
||||
onPopped(route, previousRoute);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
|
||||
if (onRemoved != null)
|
||||
onRemoved(route, previousRoute);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
@ -445,4 +451,120 @@ void main() {
|
||||
final String replaceNamedValue = await value; // replaceNamed result was 'B'
|
||||
expect(replaceNamedValue, 'B');
|
||||
});
|
||||
|
||||
testWidgets('removeRoute', (WidgetTester tester) async {
|
||||
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
|
||||
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }),
|
||||
'/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pushNamed(context, '/B'); }),
|
||||
'/B': (BuildContext context) => const OnTapPage(id: 'B'),
|
||||
};
|
||||
final Map<String, Route<String>> routes = <String, Route<String>>{};
|
||||
|
||||
Route<String> removedRoute;
|
||||
Route<String> previousRoute;
|
||||
|
||||
final TestObserver observer = new TestObserver()
|
||||
..onRemoved = (Route<dynamic> route, Route<dynamic> previous) {
|
||||
removedRoute = route;
|
||||
previousRoute = previous;
|
||||
};
|
||||
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
navigatorObservers: <NavigatorObserver>[observer],
|
||||
onGenerateRoute: (RouteSettings settings) {
|
||||
routes[settings.name] = new PageRouteBuilder<String>(
|
||||
settings: settings,
|
||||
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
|
||||
return pageBuilders[settings.name](context);
|
||||
},
|
||||
);
|
||||
return routes[settings.name];
|
||||
}
|
||||
));
|
||||
|
||||
expect(find.text('/'), findsOneWidget);
|
||||
expect(find.text('A'), findsNothing);
|
||||
expect(find.text('B'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('/'), findsNothing);
|
||||
expect(find.text('A'), findsOneWidget);
|
||||
expect(find.text('B'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('A')); // pushNamed('/B'), stack becomes /, /A, /B
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('/'), findsNothing);
|
||||
expect(find.text('A'), findsNothing);
|
||||
expect(find.text('B'), findsOneWidget);
|
||||
|
||||
// Verify that the navigator's stack is ordered as expected.
|
||||
expect(routes['/'].isActive, true);
|
||||
expect(routes['/A'].isActive, true);
|
||||
expect(routes['/B'].isActive, true);
|
||||
expect(routes['/'].isFirst, true);
|
||||
expect(routes['/B'].isCurrent, true);
|
||||
|
||||
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
|
||||
navigator.removeRoute(routes['/B']); // stack becomes /, /A
|
||||
await tester.pump();
|
||||
expect(find.text('/'), findsNothing);
|
||||
expect(find.text('A'), findsOneWidget);
|
||||
expect(find.text('B'), findsNothing);
|
||||
|
||||
// Verify that the navigator's stack no longer includes /B
|
||||
expect(routes['/'].isActive, true);
|
||||
expect(routes['/A'].isActive, true);
|
||||
expect(routes['/B'].isActive, false);
|
||||
expect(routes['/'].isFirst, true);
|
||||
expect(routes['/A'].isCurrent, true);
|
||||
|
||||
expect(removedRoute, routes['/B']);
|
||||
expect(previousRoute, routes['/A']);
|
||||
|
||||
navigator.removeRoute(routes['/A']); // stack becomes just /
|
||||
await tester.pump();
|
||||
expect(find.text('/'), findsOneWidget);
|
||||
expect(find.text('A'), findsNothing);
|
||||
expect(find.text('B'), findsNothing);
|
||||
|
||||
// Verify that the navigator's stack no longer includes /A
|
||||
expect(routes['/'].isActive, true);
|
||||
expect(routes['/A'].isActive, false);
|
||||
expect(routes['/B'].isActive, false);
|
||||
expect(routes['/'].isFirst, true);
|
||||
expect(routes['/'].isCurrent, true);
|
||||
expect(removedRoute, routes['/A']);
|
||||
expect(previousRoute, routes['/']);
|
||||
});
|
||||
|
||||
testWidgets('remove a route whose value is awaited', (WidgetTester tester) async {
|
||||
Future<String> pageValue;
|
||||
final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{
|
||||
'/': (BuildContext context) => new OnTapPage(id: '/', onTap: () { pageValue = Navigator.pushNamed(context, '/A'); }),
|
||||
'/A': (BuildContext context) => new OnTapPage(id: 'A', onTap: () { Navigator.pop(context, 'A'); }),
|
||||
};
|
||||
final Map<String, Route<String>> routes = <String, Route<String>>{};
|
||||
|
||||
await tester.pumpWidget(new MaterialApp(
|
||||
onGenerateRoute: (RouteSettings settings) {
|
||||
routes[settings.name] = new PageRouteBuilder<String>(
|
||||
settings: settings,
|
||||
pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
|
||||
return pageBuilders[settings.name](context);
|
||||
},
|
||||
);
|
||||
return routes[settings.name];
|
||||
}
|
||||
));
|
||||
|
||||
await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A
|
||||
await tester.pumpAndSettle();
|
||||
pageValue.then((String value) { assert(false); });
|
||||
|
||||
final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator));
|
||||
navigator.removeRoute(routes['/A']); // stack becomes /, pageValue will not complete
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user