Commit c951f66f authored by Ian Hickson's avatar Ian Hickson

Merge pull request #631 from Hixie/yak2-replace

Add a "replace()" API for the navigator.
parents d293bb8c d1253979
......@@ -13,12 +13,21 @@ abstract class Route<T> {
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
/// Called when the route is inserted into the navigator.
/// Use this to install any overlays.
///
/// Use this to populate overlayEntries and add them to the overlay.
/// (The reason the Route is responsible for doing this, rather than the
/// Navigator, is that the Route will be responsible for _removing_ the
/// entries and this way it's symmetric.
///
/// The overlay argument will be null if this is the first route inserted.
void install(OverlayState overlay, OverlayEntry insertionPoint) { }
/// Called after install() when the route is pushed onto the navigator.
void didPush() { }
/// Called after install() when the route replaced another in the navigator.
void didReplace(Route oldRoute) { }
/// A request was made to pop this route. If the route can handle it
/// internally (e.g. because it has its own stack of internal state) then
/// return false, otherwise return true. Returning false will prevent the
......@@ -35,6 +44,10 @@ abstract class Route<T> {
/// navigator.
void didPopNext(Route nextRoute) { }
/// The given old route, which was the route that came after this one, has
/// been replaced with the given new route.
void didReplaceNext(Route oldNextRoute, Route newNextRoute) { }
/// The route should remove its overlays and free any other resources.
///
/// A call to didPop() implies that the Route should call dispose() itself,
......@@ -173,6 +186,44 @@ class NavigatorState extends State<Navigator> {
assert(() { _debugLocked = false; return true; });
}
/// Replaces one given route with another, but does not call didPush/didPop.
/// Instead, this calls install() on the new route, then didReplace() on the
/// new route passing the old route, then dispose() on the old route.
///
/// The old route must have overlays, otherwise we won't know where to insert
/// the overlays of the new route. The old route must not be currently visible
/// (i.e. a later route have overlays that are currently opaque), otherwise
/// the replacement would have a jarring effect.
///
/// It is safe to call this redundantly (replacing a route with itself). Such
/// calls are ignored.
void replace({ Route oldRoute, Route newRoute }) {
assert(!_debugLocked);
assert(oldRoute != null);
assert(newRoute != null);
if (oldRoute == newRoute)
return;
assert(() { _debugLocked = true; return true; });
assert(oldRoute._navigator == this);
assert(newRoute._navigator == null);
assert(oldRoute.overlayEntries.isNotEmpty);
assert(newRoute.overlayEntries.isEmpty);
assert(!overlay.debugIsVisible(oldRoute.overlayEntries.last));
setState(() {
int index = _history.indexOf(oldRoute);
assert(index >= 0);
newRoute._navigator = this;
newRoute.install(overlay, oldRoute.overlayEntries.last);
_history[index] = newRoute;
newRoute.didReplace(oldRoute);
if (index > 0)
_history[index - 1].didReplaceNext(oldRoute, newRoute);
oldRoute.dispose();
oldRoute._navigator = null;
});
assert(() { _debugLocked = false; return true; });
}
/// Removes the current route, notifying the observer (if any), and the
/// previous routes (using [Route.didPopNext]).
///
......
......@@ -83,6 +83,26 @@ class OverlayState extends State<Overlay> {
});
}
bool debugIsVisible(OverlayEntry entry) {
bool result = false;
assert(_entries.contains(entry));
assert(() {
// This is an O(N) algorithm, and should not be necessary except for debug asserts.
// To avoid people depending on it, we only implement it in checked mode.
for (int i = _entries.length - 1; i > 0; i -= 1) {
OverlayEntry candidate = _entries[i];
if (candidate == entry) {
result = true;
break;
}
if (entry.opaque)
break;
}
return true;
});
return result;
}
Widget build(BuildContext context) {
List<Widget> backwardsChildren = <Widget>[];
......
......@@ -119,6 +119,13 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
super.didPush();
}
void didReplace(Route oldRoute) {
if (oldRoute is TransitionRoute)
_performance.progress = oldRoute._performance.progress;
_performance.addStatusListener(handleStatusChanged);
super.didReplace(oldRoute);
}
bool didPop(T result) {
_result = result;
_performance.reverse();
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
final List<String> results = <String>[];
class TestRoute extends Route<String> {
TestRoute(this.name);
final String name;
List<OverlayEntry> get overlayEntries => _entries;
List<OverlayEntry> _entries = <OverlayEntry>[];
void log(String s) {
results.add('$name: $s');
}
void install(OverlayState overlay, OverlayEntry insertionPoint) {
log('install');
OverlayEntry entry = new OverlayEntry(
builder: (BuildContext context) => new Container(),
opaque: true
);
_entries.add(entry);
overlay?.insert(entry, above: insertionPoint);
}
void didPush() {
log('didPush');
}
void didReplace(TestRoute oldRoute) {
log('didReplace ${oldRoute.name}');
}
bool didPop(String result) {
log('didPop $result');
return super.didPop(result);
}
void didPushNext(TestRoute nextRoute) {
log('didPushNext ${nextRoute.name}');
}
void didPopNext(TestRoute nextRoute) {
log('didPopNext ${nextRoute.name}');
}
void didReplaceNext(TestRoute oldNextRoute, TestRoute newNextRoute) {
log('didReplaceNext ${oldNextRoute.name} ${newNextRoute.name}');
}
void dispose() {
log('dispose');
_entries.forEach((OverlayEntry entry) { entry.remove(); });
_entries.clear();
}
}
void runNavigatorTest(
WidgetTester tester,
NavigatorState host,
void test(NavigatorState transaction),
List<String> expectations
) {
expect(host, isNotNull);
test(host);
expect(results, equals(expectations));
results.clear();
tester.pump();
}
void main() {
test('Route management', () {
testWidgets((WidgetTester tester) {
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
tester.pumpWidget(new Navigator(
key: navigatorKey,
onGenerateRoute: (_) => new TestRoute('initial')
));
NavigatorState host = navigatorKey.currentState;
runNavigatorTest(
tester,
host,
(NavigatorState transaction) {
},
[
'initial: install',
'initial: didPush',
]
);
TestRoute second;
runNavigatorTest(
tester,
host,
(NavigatorState transaction) {
transaction.push(second = new TestRoute('second'));
},
[
'second: install',
'second: didPush',
'initial: didPushNext second',
]
);
runNavigatorTest(
tester,
host,
(NavigatorState transaction) {
transaction.push(new TestRoute('third'));
},
[
'third: install',
'third: didPush',
'second: didPushNext third',
]
);
runNavigatorTest(
tester,
host,
(NavigatorState transaction) {
transaction.replace(oldRoute: second, newRoute: new TestRoute('two'));
},
[
'two: install',
'two: didReplace second',
'initial: didReplaceNext second two',
'second: dispose',
]
);
runNavigatorTest(
tester,
host,
(NavigatorState transaction) {
transaction.pop('hello');
},
[
'third: didPop hello',
'two: didPopNext third',
]
);
runNavigatorTest(
tester,
host,
(NavigatorState transaction) {
transaction.pop('good bye');
},
[
'two: didPop good bye',
'initial: didPopNext two',
]
);
});
});
}
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