您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
2264 行
88 KiB
2264 行
88 KiB
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Unity.UIWidgets.async2;
|
|
using Unity.UIWidgets.external;
|
|
using Unity.UIWidgets.external.simplejson;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.gestures;
|
|
using Unity.UIWidgets.rendering;
|
|
using Unity.UIWidgets.scheduler2;
|
|
using Unity.UIWidgets.services;
|
|
using SchedulerBinding = Unity.UIWidgets.scheduler2.SchedulerBinding;
|
|
using SchedulerPhase = Unity.UIWidgets.scheduler2.SchedulerPhase;
|
|
|
|
namespace Unity.UIWidgets.widgets {
|
|
public delegate Route RouteFactory(RouteSettings settings);
|
|
|
|
public delegate Route<T> RouteBuilder<T>(BuildContext context, RouteSettings settings);
|
|
|
|
public delegate List<Route> RouteListFactory(NavigatorState navigator, string initialRoute);
|
|
|
|
public delegate bool RoutePredicate(Route route);
|
|
|
|
public delegate bool PopPageCallback(Route route, object result);
|
|
|
|
public delegate Future<bool> WillPopCallback();
|
|
|
|
|
|
|
|
public enum RoutePopDisposition {
|
|
pop,
|
|
doNotPop,
|
|
bubble
|
|
}
|
|
|
|
public enum _RouteLifecycle {
|
|
staging, // we will wait for transition delegate to decide what to do with this route.
|
|
add, // we"ll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages
|
|
adding, // we"ll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages
|
|
|
|
// routes that are ready for transition.
|
|
push, // we"ll want to run install, didPush, etc; a route added via push() and friends
|
|
pushReplace, // we"ll want to run install, didPush, etc; a route added via pushReplace() and friends
|
|
pushing, // we"re waiting for the future from didPush to complete
|
|
replace, // we"ll want to run install, didReplace, etc; a route added via replace() and friends
|
|
idle, // route is being harmless
|
|
|
|
//
|
|
// routes that are not present:
|
|
//
|
|
// routes that should be included in route announcement and should still listen to transition changes.
|
|
pop, // we"ll want to call didPop
|
|
remove, // we"ll want to run didReplace/didRemove etc
|
|
|
|
// routes should not be included in route announcement but should still listen to transition changes.
|
|
popping, // we"re waiting for the route to call finalizeRoute to switch to dispose
|
|
removing, // we are waiting for subsequent routes to be done animating, then will switch to dispose
|
|
|
|
// routes that are completely removed from the navigator and overlay.
|
|
dispose, // we will dispose the route momentarily
|
|
disposed, // we have disposed the route
|
|
}
|
|
|
|
public delegate bool _RouteEntryPredicate(_RouteEntry entry);
|
|
|
|
public abstract class Route {
|
|
public Route(RouteSettings settings = null) {
|
|
_settings = settings ?? new RouteSettings();
|
|
}
|
|
|
|
internal NavigatorState _navigator;
|
|
|
|
public NavigatorState navigator {
|
|
get { return _navigator; }
|
|
}
|
|
|
|
public RouteSettings settings {
|
|
get { return _settings; }
|
|
}
|
|
|
|
internal RouteSettings _settings;
|
|
|
|
public void _updateSettings(RouteSettings newSettings) {
|
|
D.assert(newSettings != null);
|
|
if (_settings != newSettings) {
|
|
_settings = newSettings;
|
|
changedInternalState();
|
|
}
|
|
}
|
|
|
|
public virtual List<OverlayEntry> overlayEntries {
|
|
get { return new List<OverlayEntry>(); }
|
|
}
|
|
|
|
protected internal virtual void install() {
|
|
}
|
|
|
|
protected internal virtual TickerFuture didPush() {
|
|
var future = TickerFuture.complete();
|
|
future.then(_ => { navigator?.focusScopeNode?.requestFocus(); });
|
|
return future;
|
|
}
|
|
|
|
protected internal virtual void didAdd() {
|
|
|
|
TickerFuture.complete().then(_ => { navigator.focusScopeNode.requestFocus(); });
|
|
}
|
|
|
|
|
|
|
|
public virtual Future<RoutePopDisposition> willPop() {
|
|
return Future.value(isFirst
|
|
? RoutePopDisposition.bubble
|
|
: RoutePopDisposition.pop).to<RoutePopDisposition>();
|
|
}
|
|
|
|
public virtual bool willHandlePopInternally {
|
|
get { return false; }
|
|
}
|
|
|
|
public virtual object currentResult {
|
|
get { return null; }
|
|
}
|
|
public Future popped {
|
|
get { return _popCompleter.future; }
|
|
}
|
|
protected internal virtual bool didPop(object result) {
|
|
didComplete(result);
|
|
return true;
|
|
}
|
|
protected internal virtual void didComplete(object result ) {
|
|
_popCompleter.complete(FutureOr.value(result ?? currentResult));
|
|
}
|
|
|
|
|
|
internal readonly Completer _popCompleter = Completer.create();
|
|
|
|
protected internal virtual void didReplace(Route oldRoute) {
|
|
}
|
|
protected internal virtual void didPopNext(Route nextRoute) {
|
|
}
|
|
|
|
protected internal virtual void didChangeNext(Route nextRoute) {
|
|
}
|
|
|
|
protected internal virtual void didChangePrevious(Route previousRoute) {
|
|
}
|
|
|
|
protected internal virtual void changedInternalState() {
|
|
}
|
|
|
|
protected internal virtual void changedExternalState() {
|
|
}
|
|
|
|
public virtual void dispose() {
|
|
_navigator = null;
|
|
}
|
|
|
|
public bool isCurrent {
|
|
get {
|
|
if (_navigator == null)
|
|
return false;
|
|
_RouteEntry currentRouteEntry = null;
|
|
foreach (var historyEntry in _navigator._history) {
|
|
if ( _RouteEntry.isPresentPredicate(historyEntry)) {
|
|
currentRouteEntry = historyEntry;
|
|
}
|
|
}
|
|
if (currentRouteEntry == null)
|
|
return false;
|
|
return currentRouteEntry.route == this;
|
|
|
|
}
|
|
}
|
|
|
|
public bool isFirst {
|
|
get {
|
|
if (_navigator == null)
|
|
return false;
|
|
_RouteEntry currentRouteEntry = null;
|
|
foreach (var historyEntry in _navigator._history) {
|
|
if ( _RouteEntry.isPresentPredicate(historyEntry)) {
|
|
currentRouteEntry = historyEntry;
|
|
break;
|
|
}
|
|
}
|
|
if (currentRouteEntry == null)
|
|
return false;
|
|
return currentRouteEntry.route == this;
|
|
|
|
}
|
|
}
|
|
|
|
public bool isActive {
|
|
get {
|
|
if (_navigator == null)
|
|
return false;
|
|
_RouteEntry routeEntry = null;
|
|
foreach (var historyEntry in _navigator._history) {
|
|
if (_RouteEntry.isRoutePredicate(this)(historyEntry)) {
|
|
routeEntry = historyEntry;
|
|
break;
|
|
}
|
|
}
|
|
return routeEntry?.isPresent == true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class Route<T> : Route {
|
|
public Route(RouteSettings settings = null) {
|
|
_settings = settings ?? new RouteSettings();
|
|
}
|
|
public T currentResult {
|
|
get { return default(T); }
|
|
}
|
|
public Future<T> popped {
|
|
get { return _popCompleter.future.to<T>(); }
|
|
}
|
|
protected internal virtual bool didPop(T result) {
|
|
didComplete(result);
|
|
return true;
|
|
}
|
|
protected internal void didComplete(T result ) {
|
|
if (default(T) == null) {
|
|
_popCompleter.complete(FutureOr.value(currentResult));
|
|
}
|
|
else {
|
|
_popCompleter.complete(FutureOr.value(result));
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
public class RouteSettings {
|
|
public RouteSettings(
|
|
string name = null,
|
|
object arguments = null) {
|
|
this.name = name;
|
|
this.arguments = arguments;
|
|
}
|
|
|
|
RouteSettings copyWith(
|
|
string name = null,
|
|
|
|
object arguments = null) {
|
|
return new RouteSettings(
|
|
name ?? this.name,
|
|
arguments ?? this.arguments
|
|
);
|
|
}
|
|
|
|
|
|
public readonly string name;
|
|
|
|
public readonly object arguments;
|
|
|
|
public override string ToString() {
|
|
return $"{GetType()}(\"{name}\", {arguments})";
|
|
}
|
|
}
|
|
|
|
public abstract class Page : RouteSettings {
|
|
public Page(
|
|
Key key = null,
|
|
string name = null,
|
|
object arguments = null
|
|
) : base(name: name, arguments: arguments) {
|
|
this.key = (LocalKey) key;
|
|
}
|
|
|
|
public LocalKey key;
|
|
|
|
public bool canUpdate(Page other) {
|
|
return other.GetType() == GetType() &&
|
|
other.key == key;
|
|
}
|
|
public override string ToString() {
|
|
return $"{GetType()}(\"{name}\",{key},{arguments})";
|
|
}
|
|
}
|
|
|
|
public abstract class Page<T> : Page {
|
|
public Page(
|
|
Key key = null,
|
|
string name = null,
|
|
object arguments = null
|
|
) : base(name: name, arguments: arguments) {
|
|
this.key = (LocalKey) key;
|
|
}
|
|
public abstract Route<T> createRoute(BuildContext context);
|
|
}
|
|
|
|
public class CustomBuilderPage<T> : Page<T> {
|
|
public CustomBuilderPage(
|
|
LocalKey key,
|
|
RouteBuilder<T> routeBuilder,
|
|
string name = null,
|
|
object arguments = null
|
|
) : base(key: key, name: name, arguments: arguments) {
|
|
this.routeBuilder = routeBuilder;
|
|
}
|
|
|
|
|
|
public readonly RouteBuilder<T> routeBuilder;
|
|
|
|
|
|
public override Route<T> createRoute(BuildContext context) {
|
|
Route<T> route = routeBuilder(context, this);
|
|
D.assert(route.settings == this);
|
|
return route;
|
|
}
|
|
}
|
|
|
|
public class NavigatorObserver {
|
|
internal NavigatorState _navigator;
|
|
|
|
public NavigatorState navigator {
|
|
get { return _navigator; }
|
|
}
|
|
|
|
public virtual void didPush(Route route, Route previousRoute) {
|
|
}
|
|
|
|
public virtual void didPop(Route route, Route previousRoute) {
|
|
}
|
|
|
|
public virtual void didRemove(Route route, Route previousRoute) {
|
|
}
|
|
|
|
public virtual void didReplace(Route newRoute = null, Route oldRoute = null) {
|
|
}
|
|
|
|
public virtual void didStartUserGesture(Route route, Route previousRoute) {
|
|
}
|
|
|
|
public virtual void didStopUserGesture() {
|
|
}
|
|
}
|
|
|
|
|
|
public abstract class RouteTransitionRecord {
|
|
|
|
public Route route;
|
|
|
|
public bool isEntering;
|
|
|
|
public bool _debugWaitingForExitDecision = false;
|
|
public abstract void markForPush();
|
|
public abstract void markForAdd();
|
|
public abstract void markForPop(object result = null);
|
|
public abstract void markForComplete(object result = null);
|
|
public abstract void markForRemove();
|
|
}
|
|
|
|
public abstract class TransitionDelegate<T> {
|
|
public TransitionDelegate() {
|
|
}
|
|
|
|
public IEnumerable<RouteTransitionRecord> _transition(
|
|
List<RouteTransitionRecord> newPageRouteHistory = null,
|
|
Dictionary<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute = null,
|
|
Dictionary<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes = null
|
|
) {
|
|
IEnumerable<RouteTransitionRecord> results = resolve(
|
|
newPageRouteHistory: newPageRouteHistory,
|
|
locationToExitingPageRoute: locationToExitingPageRoute,
|
|
pageRouteToPagelessRoutes: pageRouteToPagelessRoutes
|
|
);
|
|
|
|
D.assert(() => {
|
|
List<RouteTransitionRecord> resultsToVerify = results.ToList();
|
|
HashSet<RouteTransitionRecord> exitingPageRoutes = new HashSet<RouteTransitionRecord>();
|
|
foreach(RouteTransitionRecord item in locationToExitingPageRoute.Values) {
|
|
exitingPageRoutes.Add(item);
|
|
}
|
|
foreach (RouteTransitionRecord exitingPageRoute in exitingPageRoutes) {
|
|
D.assert(!exitingPageRoute._debugWaitingForExitDecision);
|
|
if (pageRouteToPagelessRoutes.ContainsKey(exitingPageRoute)) {
|
|
foreach (RouteTransitionRecord pagelessRoute in pageRouteToPagelessRoutes[exitingPageRoute]) {
|
|
D.assert(!pagelessRoute._debugWaitingForExitDecision);
|
|
}
|
|
}
|
|
}
|
|
|
|
int indexOfNextRouteInNewHistory = 0;
|
|
|
|
foreach (_RouteEntry routeEntry in resultsToVerify.Cast<_RouteEntry>()) {
|
|
D.assert(routeEntry != null);
|
|
D.assert(!routeEntry.isEntering && !routeEntry._debugWaitingForExitDecision);
|
|
if (
|
|
indexOfNextRouteInNewHistory >= newPageRouteHistory.Count ||
|
|
routeEntry != newPageRouteHistory[indexOfNextRouteInNewHistory]
|
|
) {
|
|
D.assert(exitingPageRoutes.Contains(routeEntry));
|
|
exitingPageRoutes.Remove(routeEntry);
|
|
}
|
|
else {
|
|
indexOfNextRouteInNewHistory += 1;
|
|
}
|
|
}
|
|
|
|
D.assert(
|
|
indexOfNextRouteInNewHistory == newPageRouteHistory.Count &&
|
|
exitingPageRoutes.isEmpty()
|
|
);
|
|
return true;
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
public abstract IEnumerable<RouteTransitionRecord> resolve(
|
|
List<RouteTransitionRecord> newPageRouteHistory = null,
|
|
Dictionary<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute = null,
|
|
Dictionary<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes = null
|
|
);
|
|
}
|
|
|
|
|
|
public class DefaultTransitionDelegate<T> : TransitionDelegate<T> {
|
|
public DefaultTransitionDelegate() : base() {
|
|
}
|
|
|
|
public override IEnumerable<RouteTransitionRecord> resolve(
|
|
List<RouteTransitionRecord> newPageRouteHistory = null,
|
|
Dictionary<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute = null,
|
|
Dictionary<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes = null) {
|
|
|
|
List<RouteTransitionRecord> results = new List<RouteTransitionRecord>();
|
|
|
|
void handleExitingRoute(RouteTransitionRecord location, bool isLast) {
|
|
RouteTransitionRecord exitingPageRoute = locationToExitingPageRoute[location];
|
|
if (exitingPageRoute == null)
|
|
return;
|
|
D.assert(exitingPageRoute._debugWaitingForExitDecision);
|
|
bool hasPagelessRoute = pageRouteToPagelessRoutes.ContainsKey(exitingPageRoute);
|
|
bool isLastExitingPageRoute = isLast && !locationToExitingPageRoute.ContainsKey(exitingPageRoute);
|
|
if (isLastExitingPageRoute && !hasPagelessRoute) {
|
|
exitingPageRoute.markForPop(exitingPageRoute.route.currentResult);
|
|
}
|
|
else {
|
|
exitingPageRoute.markForComplete(exitingPageRoute.route.currentResult);
|
|
}
|
|
|
|
results.Add(exitingPageRoute);
|
|
|
|
if (hasPagelessRoute) {
|
|
List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
|
|
foreach (RouteTransitionRecord pagelessRoute in pagelessRoutes) {
|
|
D.assert(pagelessRoute._debugWaitingForExitDecision);
|
|
if (isLastExitingPageRoute && pagelessRoute == pagelessRoutes.Last()) {
|
|
pagelessRoute.markForPop(pagelessRoute.route.currentResult);
|
|
}
|
|
else {
|
|
pagelessRoute.markForComplete(pagelessRoute.route.currentResult);
|
|
}
|
|
}
|
|
}
|
|
|
|
// It is possible there is another exiting route above this exitingPageRoute.
|
|
handleExitingRoute(exitingPageRoute, isLast);
|
|
}
|
|
|
|
handleExitingRoute(null, newPageRouteHistory.isEmpty());
|
|
|
|
foreach (RouteTransitionRecord pageRoute in newPageRouteHistory) {
|
|
bool isLastIteration = newPageRouteHistory.Last() == pageRoute;
|
|
if (pageRoute.isEntering) {
|
|
if (!locationToExitingPageRoute.ContainsKey(pageRoute) && isLastIteration) {
|
|
pageRoute.markForPush();
|
|
}
|
|
else {
|
|
pageRoute.markForAdd();
|
|
}
|
|
}
|
|
|
|
results.Add(pageRoute);
|
|
handleExitingRoute(pageRoute, isLastIteration);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
}
|
|
|
|
|
|
public class Navigator : StatefulWidget {
|
|
public Navigator(
|
|
Key key = null,
|
|
List<Page<object>> pages = null,
|
|
PopPageCallback onPopPage = null,
|
|
string initialRoute = null,
|
|
RouteListFactory onGenerateInitialRoutes = null,
|
|
RouteFactory onGenerateRoute = null,
|
|
RouteFactory onUnknownRoute = null,
|
|
TransitionDelegate<object> transitionDelegate = null,
|
|
List<NavigatorObserver> observers = null) : base(key) {
|
|
|
|
this.pages = pages ?? new List<Page<object>>();
|
|
this.onPopPage = onPopPage;
|
|
this.initialRoute = initialRoute;
|
|
this.onGenerateInitialRoutes = onGenerateInitialRoutes ?? defaultGenerateInitialRoutes;
|
|
this.onGenerateRoute = onGenerateRoute;
|
|
this.onUnknownRoute = onUnknownRoute;
|
|
this.transitionDelegate = transitionDelegate ?? new DefaultTransitionDelegate<object>();
|
|
this.observers = observers ?? new List<NavigatorObserver>();
|
|
|
|
}
|
|
|
|
public readonly List<Page<object>> pages;
|
|
|
|
public readonly PopPageCallback onPopPage;
|
|
|
|
public readonly TransitionDelegate<object> transitionDelegate;
|
|
|
|
public readonly string initialRoute;
|
|
|
|
public readonly RouteListFactory onGenerateInitialRoutes;
|
|
|
|
public readonly RouteFactory onGenerateRoute;
|
|
|
|
public readonly RouteFactory onUnknownRoute;
|
|
|
|
public readonly List<NavigatorObserver> observers;
|
|
|
|
public static readonly string defaultRouteName = "/";
|
|
|
|
|
|
public static Future<T> pushNamed<T>(BuildContext context, string routeName, object arguments = null) {
|
|
return of(context).pushNamed<T>(routeName, arguments: arguments);
|
|
}
|
|
|
|
public static Future pushNamed(BuildContext context, string routeName, object arguments = null) {
|
|
return of(context).pushNamed(routeName, arguments: arguments);
|
|
}
|
|
|
|
public static Future<T> pushReplacementNamed<T,TO>(BuildContext context, string routeName,
|
|
TO result = default , object arguments = null) {
|
|
return of(context).pushReplacementNamed<T,TO>(routeName, arguments: arguments,result: result);
|
|
}
|
|
|
|
public static Future<T> popAndPushNamed<T,TO>(BuildContext context, string routeName,
|
|
TO result = default,
|
|
object arguments = null) {
|
|
return of(context).popAndPushNamed<T,TO>(routeName, result: result, arguments: arguments);
|
|
}
|
|
|
|
public static Future<T> pushNamedAndRemoveUntil<T>(BuildContext context, string newRouteName,
|
|
RoutePredicate predicate, object arguments = null) {
|
|
return of(context).pushNamedAndRemoveUntil<T>(newRouteName, predicate, arguments: arguments);
|
|
}
|
|
|
|
public static Future<T> push<T>(BuildContext context, Route route) {
|
|
return of(context).push<T>(route);
|
|
}
|
|
|
|
public static Future<T> pushReplacement<T,TO>(BuildContext context, Route<T> newRoute, TO result = default ) {
|
|
return of(context).pushReplacement<T,TO>(newRoute, result);
|
|
}
|
|
|
|
|
|
public static Future<T> pushAndRemoveUntil<T>(BuildContext context, Route<T> newRoute, RoutePredicate predicate) {
|
|
return of(context).pushAndRemoveUntil<T>(newRoute, predicate);
|
|
}
|
|
|
|
public static void replace<T>(BuildContext context, Route oldRoute = null, Route<T> newRoute = null) {
|
|
of(context).replace<T>(oldRoute: oldRoute, newRoute: newRoute);
|
|
}
|
|
|
|
public static void replaceRouteBelow<T>(BuildContext context, Route anchorRoute = null, Route<T> newRoute = null ) {
|
|
of(context).replaceRouteBelow<T>(anchorRoute: anchorRoute, newRoute: newRoute);
|
|
}
|
|
|
|
public static bool canPop(BuildContext context) {
|
|
NavigatorState navigator = of(context, nullOk: true);
|
|
return navigator != null && navigator.canPop();
|
|
}
|
|
public static Future<bool> maybePop<T>(BuildContext context, T result = default(T)) {
|
|
return of(context).maybePop<T>(result);
|
|
}
|
|
public static Future<bool> maybePop(BuildContext context, object result = null) {
|
|
return of(context).maybePop(result);
|
|
}
|
|
|
|
public static void pop<T>(BuildContext context, T result = default(T) ) {
|
|
of(context).pop<T>(result);
|
|
}
|
|
|
|
public static void pop(BuildContext context, object result = null ) {
|
|
of(context).pop(result);
|
|
}
|
|
|
|
|
|
public static void popUntil(BuildContext context, RoutePredicate predicate) {
|
|
of(context).popUntil(predicate);
|
|
}
|
|
|
|
public static void removeRoute(BuildContext context, Route route) {
|
|
of(context).removeRoute(route);
|
|
}
|
|
|
|
|
|
public static void removeRouteBelow(BuildContext context, Route anchorRoute) {
|
|
of(context).removeRouteBelow(anchorRoute);
|
|
}
|
|
|
|
public static NavigatorState of(
|
|
BuildContext context,
|
|
bool rootNavigator = false,
|
|
bool nullOk = false
|
|
) {
|
|
NavigatorState navigator = rootNavigator
|
|
? context.findRootAncestorStateOfType<NavigatorState>()
|
|
: context.findAncestorStateOfType<NavigatorState>();
|
|
D.assert(() => {
|
|
if (navigator == null && !nullOk) {
|
|
throw new UIWidgetsError(
|
|
"Navigator operation requested with a context that does not include a Navigator.\n" +
|
|
"The context used to push or pop routes from the Navigator must be that of a " +
|
|
"widget that is a descendant of a Navigator widget."
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
return navigator;
|
|
}
|
|
|
|
public static List<Route> defaultGenerateInitialRoutes(NavigatorState navigator, string initialRouteName) {
|
|
List<Route> result = new List<Route>();
|
|
if (initialRouteName.StartsWith("/") && initialRouteName.Length > 1) {
|
|
initialRouteName = initialRouteName.Substring(1); // strip leading "/"
|
|
D.assert(defaultRouteName == "/");
|
|
List<string> debugRouteNames = new List<string>();
|
|
D.assert(() => {
|
|
debugRouteNames = new List<string> {defaultRouteName};
|
|
return true;
|
|
});
|
|
result.Add(navigator._routeNamed(defaultRouteName, arguments: null, allowNull: true));
|
|
string[] routeParts = initialRouteName.Split('/');
|
|
if (initialRouteName.isNotEmpty()) {
|
|
string routeName = "";
|
|
foreach (string part in routeParts) {
|
|
routeName += $"/{part}";
|
|
D.assert(() => {
|
|
debugRouteNames.Add(routeName);
|
|
return true;
|
|
});
|
|
result.Add(navigator._routeNamed(routeName, arguments: null, allowNull: true));
|
|
}
|
|
}
|
|
|
|
if (result.Last() == null) {
|
|
D.assert(() => {
|
|
throw new UIWidgetsError(
|
|
"Could not navigate to initial route.\n" +
|
|
$"The requested route name was: {initialRouteName} \n " +
|
|
"There was no corresponding route in the app, and therefore the initial route specified will be " +
|
|
$"ignored and {defaultRouteName} will be used instead."
|
|
);
|
|
return true;
|
|
});
|
|
result.Clear();
|
|
}
|
|
}
|
|
else if (initialRouteName != defaultRouteName) {
|
|
result.Add(navigator._routeNamed(initialRouteName, arguments: null, allowNull: true));
|
|
}
|
|
|
|
//result.RemoveWhere((Route<object> route) => route == null);
|
|
foreach (var resultRoute in result) {
|
|
if (resultRoute == null) {
|
|
result.Remove(resultRoute);
|
|
}
|
|
}
|
|
if (result.isEmpty())
|
|
result.Add(
|
|
navigator._routeNamed(
|
|
name:defaultRouteName,
|
|
arguments: null));
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
public override State createState() {
|
|
return new NavigatorState();
|
|
}
|
|
}
|
|
|
|
public class _RouteEntry : RouteTransitionRecord {
|
|
public _RouteEntry(
|
|
Route route,
|
|
_RouteLifecycle initialState
|
|
) {
|
|
D.assert(
|
|
initialState == _RouteLifecycle.staging ||
|
|
initialState == _RouteLifecycle.add ||
|
|
initialState == _RouteLifecycle.push ||
|
|
initialState == _RouteLifecycle.pushReplace ||
|
|
initialState == _RouteLifecycle.replace
|
|
);
|
|
this.route = route;
|
|
currentState = initialState;
|
|
}
|
|
|
|
public Route route;
|
|
public _RouteLifecycle currentState;
|
|
public Route lastAnnouncedPreviousRoute; // last argument to Route.didChangePrevious
|
|
public Route lastAnnouncedPoppedNextRoute; // last argument to Route.didPopNext
|
|
public Route lastAnnouncedNextRoute; // last argument to Route.didChangeNext
|
|
|
|
public bool hasPage {
|
|
get { return route.settings is Page; }
|
|
}
|
|
|
|
public bool canUpdateFrom(Page page) {
|
|
if (currentState.GetHashCode() > _RouteLifecycle.idle.GetHashCode())
|
|
return false;
|
|
if (!hasPage)
|
|
return false;
|
|
Page routePage = route.settings as Page;
|
|
return page.canUpdate(routePage);
|
|
}
|
|
|
|
public void handleAdd(NavigatorState navigator) {
|
|
D.assert(currentState == _RouteLifecycle.add);
|
|
D.assert(navigator != null);
|
|
D.assert(navigator._debugLocked);
|
|
D.assert(route._navigator == null);
|
|
route._navigator = navigator;
|
|
route.install();
|
|
D.assert(route.overlayEntries.isNotEmpty());
|
|
currentState = _RouteLifecycle.adding;
|
|
}
|
|
|
|
public void handlePush(NavigatorState navigator, bool isNewFirst, Route previous = null,
|
|
Route previousPresent = null) {
|
|
D.assert(currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace ||
|
|
currentState == _RouteLifecycle.replace);
|
|
D.assert(navigator != null);
|
|
D.assert(navigator._debugLocked);
|
|
D.assert(route._navigator == null);
|
|
_RouteLifecycle previousState = currentState;
|
|
route._navigator = navigator;
|
|
route.install();
|
|
D.assert(route.overlayEntries.isNotEmpty());
|
|
if (currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace) {
|
|
TickerFuture routeFuture = route.didPush();
|
|
currentState = _RouteLifecycle.pushing;
|
|
routeFuture.whenCompleteOrCancel(() => {
|
|
if (currentState == _RouteLifecycle.pushing) {
|
|
currentState = _RouteLifecycle.idle;
|
|
D.assert(!navigator._debugLocked);
|
|
D.assert(() => {
|
|
navigator._debugLocked = true;
|
|
return true;
|
|
});
|
|
navigator._flushHistoryUpdates();
|
|
D.assert(() => {
|
|
navigator._debugLocked = false;
|
|
return true;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
D.assert(currentState == _RouteLifecycle.replace);
|
|
route.didReplace(previous);
|
|
currentState = _RouteLifecycle.idle;
|
|
}
|
|
|
|
if (isNewFirst) {
|
|
route.didChangeNext(null);
|
|
}
|
|
|
|
if (previousState == _RouteLifecycle.replace || previousState == _RouteLifecycle.pushReplace) {
|
|
foreach (NavigatorObserver observer in navigator.widget.observers)
|
|
observer.didReplace(newRoute: route, oldRoute: previous);
|
|
}
|
|
else {
|
|
D.assert(previousState == _RouteLifecycle.push);
|
|
foreach (NavigatorObserver observer in navigator.widget.observers)
|
|
observer.didPush(route, previousPresent);
|
|
}
|
|
}
|
|
|
|
public void handleDidPopNext(Route poppedRoute) {
|
|
route.didPopNext(poppedRoute);
|
|
lastAnnouncedPoppedNextRoute = poppedRoute;
|
|
}
|
|
|
|
public void handlePop(NavigatorState navigator , Route previousPresent = null) {
|
|
D.assert(navigator != null);
|
|
D.assert(navigator._debugLocked);
|
|
D.assert(route._navigator == navigator);
|
|
currentState = _RouteLifecycle.popping;
|
|
foreach (NavigatorObserver observer in navigator.widget.observers)
|
|
observer.didPop(route, previousPresent);
|
|
}
|
|
|
|
public void handleRemoval(NavigatorState navigator, Route previousPresent = null) {
|
|
D.assert(navigator != null);
|
|
D.assert(navigator._debugLocked);
|
|
D.assert(route._navigator == navigator);
|
|
currentState = _RouteLifecycle.removing;
|
|
if (_reportRemovalToObserver) {
|
|
foreach (NavigatorObserver observer in navigator.widget.observers)
|
|
observer.didRemove(route, previousPresent);
|
|
}
|
|
}
|
|
|
|
public bool doingPop = false;
|
|
|
|
public void didAdd(
|
|
NavigatorState navigator = null,
|
|
bool isNewFirst = false,
|
|
Route previous = null,
|
|
Route previousPresent = null) {
|
|
|
|
route.didAdd();
|
|
currentState = _RouteLifecycle.idle;
|
|
if (isNewFirst) {
|
|
route.didChangeNext(null);
|
|
}
|
|
|
|
foreach (NavigatorObserver observer in navigator.widget.observers)
|
|
observer.didPush(route, previousPresent);
|
|
}
|
|
|
|
public void pop<T>(T result) {
|
|
D.assert(isPresent);
|
|
doingPop = true;
|
|
if (route.didPop(result) && doingPop) {
|
|
currentState = _RouteLifecycle.pop;
|
|
}
|
|
|
|
doingPop = false;
|
|
}
|
|
|
|
bool _reportRemovalToObserver = true;
|
|
|
|
|
|
public void remove(bool isReplaced = false) {
|
|
D.assert(
|
|
!hasPage || _debugWaitingForExitDecision, () =>
|
|
"A page-based route cannot be completed using imperative api, provide a " +
|
|
"new list without the corresponding Page to Navigator.pages instead. "
|
|
);
|
|
if (currentState.GetHashCode() >= _RouteLifecycle.remove.GetHashCode())
|
|
return;
|
|
D.assert(isPresent);
|
|
_reportRemovalToObserver = !isReplaced;
|
|
currentState = _RouteLifecycle.remove;
|
|
}
|
|
|
|
|
|
public void complete<T>(T result, bool isReplaced = false) {
|
|
D.assert(
|
|
!hasPage || _debugWaitingForExitDecision, () =>
|
|
"A page-based route cannot be completed using imperative api, provide a " +
|
|
"new list without the corresponding Page to Navigator.pages instead. "
|
|
);
|
|
if (currentState.GetHashCode() >= _RouteLifecycle.remove.GetHashCode())
|
|
return;
|
|
D.assert(isPresent);
|
|
_reportRemovalToObserver = !isReplaced;
|
|
route.didComplete(result);
|
|
D.assert(route._popCompleter.isCompleted); // implies didComplete was called
|
|
currentState = _RouteLifecycle.remove;
|
|
}
|
|
|
|
public void finalize() {
|
|
D.assert(currentState.GetHashCode() < _RouteLifecycle.dispose.GetHashCode());
|
|
currentState = _RouteLifecycle.dispose;
|
|
}
|
|
|
|
public void dispose() {
|
|
D.assert(currentState.GetHashCode() < _RouteLifecycle.disposed.GetHashCode());
|
|
((Route)route).dispose();
|
|
currentState = _RouteLifecycle.disposed;
|
|
}
|
|
|
|
bool willBePresent {
|
|
get {
|
|
return currentState.GetHashCode() <= _RouteLifecycle.idle.GetHashCode() &&
|
|
currentState.GetHashCode() >= _RouteLifecycle.add.GetHashCode();
|
|
}
|
|
}
|
|
|
|
public bool isPresent {
|
|
|
|
get {
|
|
return currentState.GetHashCode() <= _RouteLifecycle.remove.GetHashCode() &&
|
|
currentState.GetHashCode() >= _RouteLifecycle.add.GetHashCode();
|
|
}
|
|
}
|
|
|
|
public bool suitableForAnnouncement {
|
|
get {
|
|
return currentState.GetHashCode() <= _RouteLifecycle.removing.GetHashCode() &&
|
|
currentState.GetHashCode() >= _RouteLifecycle.push.GetHashCode();
|
|
}
|
|
}
|
|
|
|
bool suitableForTransitionAnimation {
|
|
get {
|
|
return currentState.GetHashCode() <= _RouteLifecycle.remove.GetHashCode() &&
|
|
currentState.GetHashCode() >= _RouteLifecycle.push.GetHashCode();
|
|
}
|
|
|
|
}
|
|
|
|
public bool shouldAnnounceChangeToNext(Route nextRoute) {
|
|
D.assert(nextRoute != lastAnnouncedNextRoute);
|
|
return !(
|
|
nextRoute == null &&
|
|
lastAnnouncedPoppedNextRoute != null &&
|
|
lastAnnouncedPoppedNextRoute == lastAnnouncedNextRoute
|
|
);
|
|
}
|
|
|
|
public readonly static _RouteEntryPredicate isPresentPredicate = (_RouteEntry entry) => entry.isPresent;
|
|
|
|
public readonly static _RouteEntryPredicate suitableForTransitionAnimationPredicate =
|
|
(_RouteEntry entry) => entry.suitableForTransitionAnimation;
|
|
|
|
public readonly static _RouteEntryPredicate willBePresentPredicate = (_RouteEntry entry) => entry.willBePresent;
|
|
|
|
public static _RouteEntryPredicate isRoutePredicate(Route route) {
|
|
return (_RouteEntry entry) => entry.route == route;
|
|
}
|
|
|
|
|
|
public bool isEntering {
|
|
get { return currentState == _RouteLifecycle.staging; }
|
|
}
|
|
|
|
public override void markForPush() {
|
|
D.assert(
|
|
isEntering && !_debugWaitingForExitDecision, () =>
|
|
"This route cannot be marked for push. Either a decision has already been " +
|
|
"made or it does not require an explicit decision on how to transition in."
|
|
);
|
|
currentState = _RouteLifecycle.push;
|
|
}
|
|
|
|
|
|
public override void markForAdd() {
|
|
D.assert(
|
|
isEntering && !_debugWaitingForExitDecision, () =>
|
|
"This route cannot be marked for add. Either a decision has already been " +
|
|
"made or it does not require an explicit decision on how to transition in."
|
|
);
|
|
currentState = _RouteLifecycle.add;
|
|
}
|
|
|
|
|
|
public override void markForPop(object result = null) {
|
|
D.assert(
|
|
!isEntering && _debugWaitingForExitDecision, () =>
|
|
"This route cannot be marked for pop. Either a decision has already been " +
|
|
"made or it does not require an explicit decision on how to transition out."
|
|
);
|
|
pop(result);
|
|
_debugWaitingForExitDecision = false;
|
|
}
|
|
|
|
|
|
public override void markForComplete(object result = null) {
|
|
D.assert(
|
|
!isEntering && _debugWaitingForExitDecision, () =>
|
|
"This route cannot be marked for complete. Either a decision has already " +
|
|
"been made or it does not require an explicit decision on how to transition " +
|
|
"out."
|
|
);
|
|
complete(result);
|
|
_debugWaitingForExitDecision = false;
|
|
}
|
|
|
|
|
|
public override void markForRemove() {
|
|
D.assert(
|
|
!isEntering && _debugWaitingForExitDecision, () =>
|
|
"This route cannot be marked for remove. Either a decision has already " +
|
|
"been made or it does not require an explicit decision on how to transition " +
|
|
"out."
|
|
);
|
|
remove();
|
|
_debugWaitingForExitDecision = false;
|
|
}
|
|
}
|
|
|
|
public class NavigatorState : TickerProviderStateMixin<Navigator> {
|
|
public readonly GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>.key();
|
|
public List<_RouteEntry> _history = new List<_RouteEntry>();
|
|
public readonly FocusScopeNode focusScopeNode = new FocusScopeNode(debugLabel: "Navigator Scope");
|
|
public bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
D.assert(
|
|
widget.pages.isEmpty() || widget.onPopPage != null, () =>
|
|
"The Navigator.onPopPage must be provided to use the Navigator.pages API"
|
|
);
|
|
foreach (NavigatorObserver observer in widget.observers) {
|
|
D.assert(observer.navigator == null);
|
|
observer._navigator = this;
|
|
}
|
|
|
|
string initialRoute = widget.initialRoute;
|
|
if (widget.pages.isNotEmpty()) {
|
|
foreach (Page<object> page in widget.pages) {
|
|
_history.Add(
|
|
new _RouteEntry(
|
|
page.createRoute(context),
|
|
initialState: _RouteLifecycle.add
|
|
)
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
initialRoute = initialRoute ?? Navigator.defaultRouteName;
|
|
}
|
|
|
|
if (initialRoute != null) {
|
|
_history.AddRange(
|
|
LinqUtils<_RouteEntry,Route>.SelectList(
|
|
widget.onGenerateInitialRoutes(
|
|
this,
|
|
widget.initialRoute ?? Navigator.defaultRouteName
|
|
), (Route route) =>
|
|
new _RouteEntry(
|
|
route,
|
|
initialState: _RouteLifecycle.add
|
|
)
|
|
)
|
|
);
|
|
}
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
_flushHistoryUpdates();
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
|
|
public override void didUpdateWidget(StatefulWidget oldWidget) {
|
|
oldWidget = (Navigator) oldWidget;
|
|
base.didUpdateWidget(oldWidget);
|
|
D.assert(
|
|
widget.pages.isEmpty() || widget.onPopPage != null, () =>
|
|
"The Navigator.onPopPage must be provided to use the Navigator.pages API"
|
|
);
|
|
if (!((Navigator)oldWidget).observers.equalsList(widget.observers)) {
|
|
foreach (NavigatorObserver observer in ((Navigator)oldWidget).observers)
|
|
observer._navigator = null;
|
|
foreach (NavigatorObserver observer in widget.observers) {
|
|
D.assert(observer.navigator == null);
|
|
observer._navigator = this;
|
|
}
|
|
}
|
|
|
|
if (!((Navigator)oldWidget).pages.equalsList(widget.pages)) {
|
|
D.assert(
|
|
widget.pages.isNotEmpty(), () =>
|
|
"To use the Navigator.pages, there must be at least one page in the list."
|
|
);
|
|
_updatePages();
|
|
}
|
|
|
|
foreach (_RouteEntry entry in _history)
|
|
entry.route.changedExternalState();
|
|
}
|
|
|
|
void _debugCheckDuplicatedPageKeys() {
|
|
D.assert(() => {
|
|
List<Key> keyReservation = new List<Key>();
|
|
foreach (Page page in widget.pages) {
|
|
if (page.key != null) {
|
|
D.assert(!keyReservation.Contains(page.key));
|
|
keyReservation.Add(page.key);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
|
|
public override void dispose() {
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
foreach (NavigatorObserver observer in widget.observers)
|
|
observer._navigator = null;
|
|
focusScopeNode.dispose();
|
|
foreach (_RouteEntry entry in _history)
|
|
entry.dispose();
|
|
base.dispose();
|
|
D.assert(_debugLocked);
|
|
}
|
|
|
|
public OverlayState overlay {
|
|
get { return _overlayKey.currentState; }
|
|
}
|
|
|
|
public List<OverlayEntry> _allRouteOverlayEntries {
|
|
get {
|
|
List<OverlayEntry> overlayEntries = new List<OverlayEntry>();
|
|
foreach (var historyEntry in _history) {
|
|
overlayEntries.AddRange(historyEntry.route.overlayEntries);
|
|
}
|
|
return overlayEntries;
|
|
}
|
|
}
|
|
|
|
string _lastAnnouncedRouteName;
|
|
|
|
bool _debugUpdatingPage = false;
|
|
|
|
void _updatePages() {
|
|
D.assert(() => {
|
|
D.assert(!_debugUpdatingPage);
|
|
_debugCheckDuplicatedPageKeys();
|
|
_debugUpdatingPage = true;
|
|
return true;
|
|
});
|
|
|
|
bool needsExplicitDecision = false;
|
|
int newPagesBottom = 0;
|
|
int oldEntriesBottom = 0;
|
|
int newPagesTop = widget.pages.Count - 1;
|
|
int oldEntriesTop = _history.Count - 1;
|
|
List<_RouteEntry> newHistory = new List<_RouteEntry>();
|
|
Dictionary<_RouteEntry, List<_RouteEntry>> pageRouteToPagelessRoutes =
|
|
new Dictionary<_RouteEntry, List<_RouteEntry>>();
|
|
_RouteEntry previousOldPageRouteEntry = null;
|
|
|
|
while (oldEntriesBottom <= oldEntriesTop) {
|
|
_RouteEntry oldEntry = _history[oldEntriesBottom];
|
|
D.assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
|
|
if (!oldEntry.hasPage) {
|
|
List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes.putIfAbsent(
|
|
previousOldPageRouteEntry,
|
|
() => new List<_RouteEntry>()
|
|
);
|
|
pagelessRoutes.Add(oldEntry);
|
|
oldEntriesBottom += 1;
|
|
continue;
|
|
}
|
|
|
|
if (newPagesBottom > newPagesTop)
|
|
break;
|
|
Page newPage = widget.pages[newPagesBottom];
|
|
if (!oldEntry.canUpdateFrom(newPage))
|
|
break;
|
|
previousOldPageRouteEntry = oldEntry;
|
|
oldEntry.route._updateSettings(newPage);
|
|
newHistory.Add(oldEntry);
|
|
newPagesBottom += 1;
|
|
oldEntriesBottom += 1;
|
|
}
|
|
|
|
int pagelessRoutesToSkip = 0;
|
|
while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) {
|
|
_RouteEntry oldEntry = _history[oldEntriesTop];
|
|
D.assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
|
|
if (!oldEntry.hasPage) {
|
|
// This route might need to be skipped if we can not find a page above.
|
|
pagelessRoutesToSkip += 1;
|
|
oldEntriesTop -= 1;
|
|
continue;
|
|
}
|
|
|
|
Page newPage = widget.pages[newPagesTop];
|
|
if (!oldEntry.canUpdateFrom(newPage))
|
|
break;
|
|
pagelessRoutesToSkip = 0;
|
|
oldEntriesTop -= 1;
|
|
newPagesTop -= 1;
|
|
}
|
|
|
|
oldEntriesTop += pagelessRoutesToSkip;
|
|
int oldEntriesBottomToScan = oldEntriesBottom;
|
|
Dictionary<LocalKey, _RouteEntry> pageKeyToOldEntry = new Dictionary<LocalKey, _RouteEntry> { };
|
|
while (oldEntriesBottomToScan <= oldEntriesTop) {
|
|
_RouteEntry oldEntry = _history[oldEntriesBottomToScan];
|
|
oldEntriesBottomToScan += 1;
|
|
D.assert(
|
|
oldEntry != null &&
|
|
oldEntry.currentState != _RouteLifecycle.disposed
|
|
);
|
|
|
|
if (!oldEntry.hasPage)
|
|
continue;
|
|
|
|
D.assert(oldEntry.hasPage);
|
|
Page page = oldEntry.route.settings as Page;
|
|
if (page.key == null)
|
|
continue;
|
|
|
|
D.assert(!pageKeyToOldEntry.ContainsKey(page.key));
|
|
pageKeyToOldEntry[page.key] = oldEntry;
|
|
}
|
|
|
|
while (newPagesBottom <= newPagesTop) {
|
|
Page<object> nextPage = widget.pages[newPagesBottom];
|
|
newPagesBottom += 1;
|
|
if (
|
|
nextPage.key == null ||
|
|
!pageKeyToOldEntry.ContainsKey(nextPage.key) ||
|
|
!pageKeyToOldEntry[nextPage.key].canUpdateFrom(nextPage)
|
|
) {
|
|
|
|
_RouteEntry newEntry = new _RouteEntry(
|
|
nextPage.createRoute(context),
|
|
initialState: _RouteLifecycle.staging
|
|
);
|
|
needsExplicitDecision = true;
|
|
D.assert(
|
|
newEntry.route.settings == nextPage, () =>
|
|
"If a route is created from a page, its must have that page as its " +
|
|
"settings."
|
|
);
|
|
newHistory.Add(newEntry);
|
|
}
|
|
else {
|
|
|
|
_RouteEntry matchingEntry = pageKeyToOldEntry[nextPage.key];
|
|
pageKeyToOldEntry.Remove(nextPage.key);
|
|
D.assert(matchingEntry.canUpdateFrom(nextPage));
|
|
matchingEntry.route._updateSettings(nextPage);
|
|
newHistory.Add(matchingEntry);
|
|
}
|
|
}
|
|
|
|
// Any remaining old routes that do not have a match will need to be removed.
|
|
Dictionary<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute =
|
|
new Dictionary<RouteTransitionRecord, RouteTransitionRecord>();
|
|
while (oldEntriesBottom <= oldEntriesTop) {
|
|
_RouteEntry potentialEntryToRemove = _history[oldEntriesBottom];
|
|
oldEntriesBottom += 1;
|
|
|
|
if (!potentialEntryToRemove.hasPage) {
|
|
D.assert(previousOldPageRouteEntry != null);
|
|
List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes
|
|
.putIfAbsent(
|
|
previousOldPageRouteEntry,
|
|
() => new List<_RouteEntry>()
|
|
);
|
|
pagelessRoutes.Add(potentialEntryToRemove);
|
|
D.assert(() => {
|
|
potentialEntryToRemove._debugWaitingForExitDecision =
|
|
previousOldPageRouteEntry._debugWaitingForExitDecision;
|
|
return true;
|
|
});
|
|
continue;
|
|
}
|
|
|
|
Page potentialPageToRemove = potentialEntryToRemove.route.settings as Page;
|
|
|
|
if (
|
|
potentialPageToRemove.key == null ||
|
|
pageKeyToOldEntry.ContainsKey(potentialPageToRemove.key)
|
|
) {
|
|
locationToExitingPageRoute[previousOldPageRouteEntry] = potentialEntryToRemove;
|
|
D.assert(() => {
|
|
potentialEntryToRemove._debugWaitingForExitDecision = true;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
previousOldPageRouteEntry = potentialEntryToRemove;
|
|
}
|
|
|
|
// We"ve scanned the whole list.
|
|
D.assert(oldEntriesBottom == oldEntriesTop + 1);
|
|
D.assert(newPagesBottom == newPagesTop + 1);
|
|
newPagesTop = widget.pages.Count - 1;
|
|
oldEntriesTop = _history.Count - 1;
|
|
|
|
D.assert(() => {
|
|
if (oldEntriesBottom <= oldEntriesTop)
|
|
return newPagesBottom <= newPagesTop &&
|
|
_history[oldEntriesBottom].hasPage &&
|
|
_history[oldEntriesBottom].canUpdateFrom(widget.pages[newPagesBottom]);
|
|
else
|
|
return newPagesBottom > newPagesTop;
|
|
});
|
|
|
|
|
|
while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) {
|
|
_RouteEntry oldEntry = _history[oldEntriesBottom];
|
|
D.assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
|
|
if (!oldEntry.hasPage) {
|
|
D.assert(previousOldPageRouteEntry != null);
|
|
List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes
|
|
.putIfAbsent(
|
|
previousOldPageRouteEntry,
|
|
() => new List<_RouteEntry>()
|
|
);
|
|
pagelessRoutes.Add(oldEntry);
|
|
continue;
|
|
}
|
|
|
|
previousOldPageRouteEntry = oldEntry;
|
|
Page newPage = widget.pages[newPagesBottom];
|
|
D.assert(oldEntry.canUpdateFrom(newPage));
|
|
oldEntry.route._updateSettings(newPage);
|
|
newHistory.Add(oldEntry);
|
|
oldEntriesBottom += 1;
|
|
newPagesBottom += 1;
|
|
}
|
|
|
|
// Finally, uses transition delegate to make explicit decision if needed.
|
|
needsExplicitDecision = needsExplicitDecision || locationToExitingPageRoute.isNotEmpty();
|
|
IEnumerable<_RouteEntry> results = newHistory;
|
|
if (needsExplicitDecision) {
|
|
List<RouteTransitionRecord> newHistoryRecord = new List<RouteTransitionRecord>();
|
|
for (int i = 0; i < newHistoryRecord.Count; i++) {
|
|
newHistoryRecord[i] = newHistory[i];
|
|
}
|
|
Dictionary<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutesRecord
|
|
= new Dictionary<RouteTransitionRecord, List<RouteTransitionRecord>>();
|
|
foreach (var pageRoute in pageRouteToPagelessRoutes) {
|
|
List<RouteTransitionRecord> newRecord = new List<RouteTransitionRecord>();
|
|
for(int i = 0; i < pageRoute.Value.Count;i++){
|
|
newRecord.Add(pageRoute.Value[i]);
|
|
}
|
|
pageRouteToPagelessRoutesRecord.Add(pageRoute.Key,newRecord);
|
|
}
|
|
results = widget.transitionDelegate._transition(
|
|
newPageRouteHistory:newHistoryRecord,
|
|
locationToExitingPageRoute: locationToExitingPageRoute,
|
|
pageRouteToPagelessRoutes: pageRouteToPagelessRoutesRecord
|
|
).Cast<_RouteEntry>();
|
|
}
|
|
|
|
_history = new List<_RouteEntry>();
|
|
// Adds the leading pageless routes if there is any.
|
|
if (pageRouteToPagelessRoutes.ContainsKey(null)) {
|
|
_history.AddRange(pageRouteToPagelessRoutes[null]);
|
|
}
|
|
|
|
foreach (_RouteEntry result in results) {
|
|
_history.Add(result);
|
|
if (pageRouteToPagelessRoutes.ContainsKey(result)) {
|
|
_history.AddRange(pageRouteToPagelessRoutes[result]);
|
|
}
|
|
}
|
|
|
|
D.assert(() => {
|
|
_debugUpdatingPage = false;
|
|
return true;
|
|
});
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
_flushHistoryUpdates();
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public void _flushHistoryUpdates(bool rearrangeOverlay = true) {
|
|
D.assert(_debugLocked && !_debugUpdatingPage);
|
|
|
|
int index = _history.Count - 1;
|
|
_RouteEntry next = null;
|
|
_RouteEntry entry = _history[index];
|
|
_RouteEntry previous = index > 0 ? _history[index - 1] : null;
|
|
bool canRemoveOrAdd = false; // Whether there is a fully opaque route on top to silently remove or add route underneath.
|
|
Route poppedRoute = null; // The route that should trigger didPopNext on the top active route.
|
|
bool seenTopActiveRoute = false; // Whether we"ve seen the route that would get didPopNext.
|
|
List<_RouteEntry> toBeDisposed = new List<_RouteEntry>();
|
|
while (index >= 0) {
|
|
switch (entry.currentState) {
|
|
case _RouteLifecycle.add:
|
|
D.assert(rearrangeOverlay);
|
|
entry.handleAdd(
|
|
navigator: this
|
|
);
|
|
D.assert(entry.currentState == _RouteLifecycle.adding);
|
|
continue;
|
|
case _RouteLifecycle.adding:
|
|
if (canRemoveOrAdd || next == null) {
|
|
entry.didAdd(
|
|
navigator: this,
|
|
previous: previous?.route,
|
|
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
|
|
isNewFirst: next == null
|
|
);
|
|
D.assert(entry.currentState == _RouteLifecycle.idle);
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
case _RouteLifecycle.push:
|
|
case _RouteLifecycle.pushReplace:
|
|
case _RouteLifecycle.replace:
|
|
D.assert(rearrangeOverlay);
|
|
entry.handlePush(
|
|
navigator: this,
|
|
previous: previous?.route,
|
|
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
|
|
isNewFirst: next == null
|
|
);
|
|
D.assert(entry.currentState != _RouteLifecycle.push);
|
|
D.assert(entry.currentState != _RouteLifecycle.pushReplace);
|
|
D.assert(entry.currentState != _RouteLifecycle.replace);
|
|
if (entry.currentState == _RouteLifecycle.idle) {
|
|
continue;
|
|
}
|
|
break;
|
|
case _RouteLifecycle.pushing: // Will exit this state when animation completes.
|
|
if (!seenTopActiveRoute && poppedRoute != null)
|
|
entry.handleDidPopNext(poppedRoute);
|
|
seenTopActiveRoute = true;
|
|
break;
|
|
case _RouteLifecycle.idle:
|
|
if (!seenTopActiveRoute && poppedRoute != null)
|
|
entry.handleDidPopNext(poppedRoute);
|
|
seenTopActiveRoute = true;
|
|
canRemoveOrAdd = true;
|
|
break;
|
|
case _RouteLifecycle.pop:
|
|
if (!seenTopActiveRoute) {
|
|
if (poppedRoute != null)
|
|
entry.handleDidPopNext(poppedRoute);
|
|
poppedRoute = entry.route;
|
|
}
|
|
entry.handlePop(
|
|
navigator: this,
|
|
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route
|
|
);
|
|
D.assert(entry.currentState == _RouteLifecycle.popping);
|
|
canRemoveOrAdd = true;
|
|
break;
|
|
case _RouteLifecycle.popping:
|
|
// Will exit this state when animation completes.
|
|
break;
|
|
case _RouteLifecycle.remove:
|
|
if (!seenTopActiveRoute) {
|
|
if (poppedRoute != null)
|
|
entry.route.didPopNext(poppedRoute);
|
|
poppedRoute = null;
|
|
}
|
|
|
|
entry.handleRemoval(
|
|
navigator: this,
|
|
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route
|
|
);
|
|
D.assert(entry.currentState == _RouteLifecycle.removing);
|
|
continue;
|
|
case _RouteLifecycle.removing:
|
|
if (!canRemoveOrAdd && next != null) {
|
|
// We aren"t allowed to remove this route yet.
|
|
break;
|
|
}
|
|
|
|
entry.currentState = _RouteLifecycle.dispose;
|
|
continue;
|
|
case _RouteLifecycle.dispose:
|
|
// Delay disposal until didChangeNext/didChangePrevious have been sent.
|
|
toBeDisposed.Add(_history[index]);
|
|
_history.RemoveAt(index);
|
|
entry = next;
|
|
break;
|
|
case _RouteLifecycle.disposed:
|
|
case _RouteLifecycle.staging:
|
|
D.assert(false);
|
|
break;
|
|
}
|
|
|
|
index -= 1;
|
|
next = entry;
|
|
entry = previous;
|
|
previous = index > 0 ? _history[index - 1] : null;
|
|
}
|
|
_flushRouteAnnouncement();
|
|
|
|
|
|
_RouteEntry lastEntry = null;//_history.lastWhere(_RouteEntry.isPresentPredicate, orElse: () => null);
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isPresentPredicate(historyEntry)) {
|
|
lastEntry = historyEntry;
|
|
}
|
|
}
|
|
string routeName = lastEntry?.route?.settings?.name;
|
|
if (routeName != _lastAnnouncedRouteName) {
|
|
//RouteNotificationMessages.maybeNotifyRouteChange(routeName, _lastAnnouncedRouteName);
|
|
_lastAnnouncedRouteName = routeName;
|
|
}
|
|
|
|
// Lastly, removes the overlay entries of all marked entries and disposes
|
|
// them.
|
|
foreach (_RouteEntry routeEntry in toBeDisposed) {
|
|
foreach (OverlayEntry overlayEntry in routeEntry.route.overlayEntries) {
|
|
overlayEntry.remove();
|
|
}
|
|
routeEntry.dispose();
|
|
}
|
|
if (rearrangeOverlay)
|
|
overlay?.rearrange(_allRouteOverlayEntries);
|
|
}
|
|
|
|
void _flushRouteAnnouncement() {
|
|
int index = _history.Count - 1;
|
|
while (index >= 0) {
|
|
_RouteEntry entry = _history[index];
|
|
if (!entry.suitableForAnnouncement) {
|
|
index -= 1;
|
|
continue;
|
|
}
|
|
|
|
_RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.suitableForTransitionAnimationPredicate);
|
|
|
|
if (next?.route != entry.lastAnnouncedNextRoute) {
|
|
if (entry.shouldAnnounceChangeToNext(next?.route)) {
|
|
entry.route.didChangeNext(next?.route);
|
|
}
|
|
|
|
entry.lastAnnouncedNextRoute = next?.route;
|
|
}
|
|
|
|
_RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.suitableForTransitionAnimationPredicate);
|
|
if (previous?.route != entry.lastAnnouncedPreviousRoute) {
|
|
entry.route.didChangePrevious(previous?.route);
|
|
entry.lastAnnouncedPreviousRoute = previous?.route;
|
|
}
|
|
|
|
index -= 1;
|
|
}
|
|
}
|
|
|
|
_RouteEntry _getRouteBefore(int index, _RouteEntryPredicate predicate) {
|
|
index = _getIndexBefore(index, predicate);
|
|
return index >= 0 ? _history[index] : null;
|
|
}
|
|
|
|
int _getIndexBefore(int index, _RouteEntryPredicate predicate) {
|
|
while (index >= 0 && !predicate(_history[index])) {
|
|
index -= 1;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
_RouteEntry _getRouteAfter(int index, _RouteEntryPredicate predicate) {
|
|
while (index < _history.Count && !predicate(_history[index])) {
|
|
index += 1;
|
|
}
|
|
|
|
return index < _history.Count ? _history[index] : null;
|
|
}
|
|
|
|
public Route _routeNamed(string name, object arguments, bool allowNull = false) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(name != null);
|
|
if (allowNull && widget.onGenerateRoute == null)
|
|
return null;
|
|
D.assert(() => {
|
|
if (widget.onGenerateRoute == null) {
|
|
throw new UIWidgetsError(
|
|
"Navigator.onGenerateRoute was null, but the route named " + $"{name} was referenced.\n" +
|
|
"To use the Navigator API with named routes (pushNamed, pushReplacementNamed, or " +
|
|
"pushNamedAndRemoveUntil), the Navigator must be provided with an " +
|
|
"onGenerateRoute handler.\n" +
|
|
"The Navigator was:\n" +
|
|
$" {this}"
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
RouteSettings settings = new RouteSettings(
|
|
name: name,
|
|
arguments: arguments
|
|
);
|
|
Route route = widget.onGenerateRoute(settings) as Route;
|
|
if (route == null && !allowNull) {
|
|
D.assert(() => {
|
|
if (widget.onUnknownRoute == null) {
|
|
throw new UIWidgetsError(
|
|
new List<DiagnosticsNode>() {
|
|
new ErrorSummary($"Navigator.onGenerateRoute returned null when requested to build route \"{name}\"."),
|
|
new ErrorDescription(
|
|
"The onGenerateRoute callback must never return null, unless an onUnknownRoute "+
|
|
"callback is provided as well."
|
|
),
|
|
new DiagnosticsProperty<NavigatorState>("The Navigator was", this, style: DiagnosticsTreeStyle.errorProperty),
|
|
}
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
route = widget.onUnknownRoute(settings) as Route;
|
|
D.assert(() => {
|
|
if (route == null) {
|
|
throw new UIWidgetsError(
|
|
new List<DiagnosticsNode>() {
|
|
new ErrorSummary($"Navigator.onUnknownRoute returned null when requested to build route \"{name}\"."),
|
|
new ErrorDescription("The onUnknownRoute callback must never return null."),
|
|
new DiagnosticsProperty<NavigatorState>("The Navigator was", this, style: DiagnosticsTreeStyle.errorProperty),
|
|
|
|
}
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
D.assert(route != null || allowNull);
|
|
return route;
|
|
}
|
|
|
|
public Route<T> _routeNamed<T>(string name, object arguments, bool allowNull = false) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(name != null);
|
|
if (allowNull && widget.onGenerateRoute == null)
|
|
return null;
|
|
D.assert(() => {
|
|
if (widget.onGenerateRoute == null) {
|
|
throw new UIWidgetsError(
|
|
"Navigator.onGenerateRoute was null, but the route named " + $"{name} was referenced.\n" +
|
|
"To use the Navigator API with named routes (pushNamed, pushReplacementNamed, or " +
|
|
"pushNamedAndRemoveUntil), the Navigator must be provided with an " +
|
|
"onGenerateRoute handler.\n" +
|
|
"The Navigator was:\n" +
|
|
$" {this}"
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
RouteSettings settings = new RouteSettings(
|
|
name: name,
|
|
arguments: arguments
|
|
);
|
|
|
|
var routeee = widget.onGenerateRoute(settings);
|
|
Route<T> route = routeee as Route<T>;
|
|
if (route == null && !allowNull) {
|
|
D.assert(() => {
|
|
if (widget.onUnknownRoute == null) {
|
|
throw new UIWidgetsError(
|
|
"Navigator.onGenerateRoute returned null when requested to build route " + $"{name}." +
|
|
"The onGenerateRoute callback must never return null, unless an onUnknownRoute " +
|
|
"callback is provided as well." +
|
|
new DiagnosticsProperty<NavigatorState>("The Navigator was", this,
|
|
style: DiagnosticsTreeStyle.errorProperty)
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
route = widget.onUnknownRoute(settings) as Route<T>;
|
|
D.assert(() => {
|
|
if (route == null) {
|
|
throw new UIWidgetsError(
|
|
"Navigator.onUnknownRoute returned null when requested to build route " + $"{name}." +
|
|
"The onUnknownRoute callback must never return null." +
|
|
new DiagnosticsProperty<NavigatorState>("The Navigator was", this,
|
|
style: DiagnosticsTreeStyle.errorProperty)
|
|
);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
D.assert(route != null || allowNull);
|
|
return route;
|
|
}
|
|
|
|
public Future<T> pushNamed<T>(
|
|
string routeName,
|
|
object arguments = null
|
|
) {
|
|
return push<T>(_routeNamed<T>(routeName, arguments: arguments));
|
|
}
|
|
|
|
public Future pushNamed(
|
|
string routeName,
|
|
object arguments = null
|
|
) {
|
|
return push(_routeNamed(routeName, arguments: arguments));
|
|
}
|
|
|
|
public Future<T> pushReplacementNamed<T, TO>(
|
|
string routeName,
|
|
TO result = default,
|
|
object arguments = null
|
|
) {
|
|
return pushReplacement<T, TO>(_routeNamed<T>(routeName, arguments: arguments), result: result);
|
|
}
|
|
|
|
public Future<T> popAndPushNamed<T, TO>(
|
|
string routeName,
|
|
TO result = default,
|
|
object arguments = null
|
|
) {
|
|
pop<TO>(result);
|
|
return pushNamed<T>(routeName, arguments: arguments);
|
|
}
|
|
|
|
public Future<T> pushNamedAndRemoveUntil<T>(
|
|
string newRouteName,
|
|
RoutePredicate predicate,
|
|
object arguments = null
|
|
) {
|
|
return pushAndRemoveUntil<T>(_routeNamed<T>(newRouteName, arguments: arguments), predicate);
|
|
}
|
|
public Future push(Route route) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(route != null);
|
|
D.assert(route._navigator == null);
|
|
_history.Add(new _RouteEntry(route, initialState: _RouteLifecycle.push));
|
|
_flushHistoryUpdates();
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
_afterNavigation(route);
|
|
return route.popped;
|
|
}
|
|
|
|
public Future<T> push<T>(Route route) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(route != null);
|
|
D.assert(route._navigator == null);
|
|
_history.Add(new _RouteEntry(route, initialState: _RouteLifecycle.push));
|
|
_flushHistoryUpdates();
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
_afterNavigation(route);
|
|
return route.popped.to<T>();
|
|
}
|
|
|
|
void _afterNavigation(Route route) {
|
|
if (!foundation_.kReleaseMode) {
|
|
Dictionary<string, object> routeJsonable = new Dictionary<string, object>();
|
|
if (route != null) {
|
|
routeJsonable = new Dictionary<string, object>();
|
|
|
|
string description;
|
|
if (route is TransitionRoute ){
|
|
description = ((TransitionRoute)route).debugLabel;
|
|
}
|
|
else {
|
|
description = $"{route}";
|
|
}
|
|
|
|
routeJsonable["description"] = description;
|
|
|
|
RouteSettings settings = route.settings;
|
|
Dictionary<string, object> settingsJsonable = new Dictionary<string, object> {
|
|
{"name", settings.name}
|
|
};
|
|
if (settings.arguments != null) {
|
|
settingsJsonable["arguments"] = JSONMessageCodec.instance.toJson(
|
|
settings.arguments);
|
|
}
|
|
|
|
routeJsonable["settings"] = settingsJsonable;
|
|
}
|
|
developer.developer_.postEvent("Flutter.Navigation", new Hashtable{
|
|
{"route", routeJsonable}
|
|
});
|
|
}
|
|
|
|
_cancelActivePointers();
|
|
}
|
|
|
|
|
|
|
|
public Future<T> pushReplacement<T, TO>(Route<T> newRoute, TO result) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(newRoute != null);
|
|
D.assert(newRoute._navigator == null);
|
|
D.assert(_history.isNotEmpty());
|
|
|
|
bool anyEntry = false;
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isPresentPredicate(historyEntry)) {
|
|
anyEntry = true;
|
|
}
|
|
}
|
|
D.assert(anyEntry,()=> "Navigator has no active routes to replace.");
|
|
_RouteEntry lastEntry = null;
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isPresentPredicate(historyEntry)) {
|
|
lastEntry = historyEntry;
|
|
}
|
|
}
|
|
lastEntry.complete(result, isReplaced: true);
|
|
|
|
_history.Add(new _RouteEntry(newRoute, initialState: _RouteLifecycle.pushReplace));
|
|
_flushHistoryUpdates();
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
_afterNavigation(newRoute);
|
|
return newRoute.popped.to<T>();
|
|
}
|
|
|
|
public Future<T> pushAndRemoveUntil<T>(Route<T> newRoute, RoutePredicate predicate) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(newRoute != null);
|
|
D.assert(newRoute._navigator == null);
|
|
D.assert(newRoute.overlayEntries.isEmpty());
|
|
D.assert(predicate != null);
|
|
int index = _history.Count - 1;
|
|
_history.Add(new _RouteEntry(newRoute, initialState: _RouteLifecycle.push));
|
|
while (index >= 0) {
|
|
_RouteEntry entry = _history[index];
|
|
if (entry.isPresent && !predicate(entry.route))
|
|
_history[index].remove();
|
|
index -= 1;
|
|
}
|
|
|
|
_flushHistoryUpdates();
|
|
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
_afterNavigation(newRoute);
|
|
return newRoute.popped.to<T>();
|
|
}
|
|
|
|
public void replace<T>(Route oldRoute, Route<T> newRoute) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(oldRoute != null);
|
|
D.assert(newRoute != null);
|
|
if (oldRoute == newRoute)
|
|
return;
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(oldRoute._navigator == this);
|
|
D.assert(newRoute._navigator == null);
|
|
|
|
int index = -1;
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isRoutePredicate(oldRoute)(historyEntry)) {
|
|
index = _history.IndexOf(historyEntry);
|
|
break;
|
|
}
|
|
}
|
|
|
|
D.assert(index >= 0, () => "This Navigator does not contain the specified oldRoute.");
|
|
D.assert(_history[index].isPresent,
|
|
() => "The specified oldRoute has already been removed from the Navigator.");
|
|
bool wasCurrent = oldRoute.isCurrent;
|
|
_history.Insert(index + 1, new _RouteEntry(newRoute, initialState: _RouteLifecycle.replace));
|
|
_history.RemoveAt(index);
|
|
_flushHistoryUpdates();
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
if (wasCurrent)
|
|
_afterNavigation(newRoute);
|
|
}
|
|
|
|
public void replaceRouteBelow<T>(Route anchorRoute, Route<T> newRoute) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(anchorRoute != null);
|
|
D.assert(anchorRoute._navigator == this);
|
|
D.assert(newRoute != null);
|
|
D.assert(newRoute._navigator == null);
|
|
int anchorIndex = -1;
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isRoutePredicate(anchorRoute)(historyEntry)) {
|
|
anchorIndex = _history.IndexOf(historyEntry);
|
|
break;
|
|
}
|
|
}
|
|
|
|
D.assert(anchorIndex >= 0, () => "This Navigator does not contain the specified anchorRoute.");
|
|
D.assert(_history[anchorIndex].isPresent,
|
|
() => "The specified anchorRoute has already been removed from the Navigator.");
|
|
int index = anchorIndex - 1;
|
|
while (index >= 0) {
|
|
if (_history[index].isPresent)
|
|
break;
|
|
index -= 1;
|
|
}
|
|
|
|
D.assert(index >= 0, () => "There are no routes below the specified anchorRoute.");
|
|
_history.Insert(index + 1, new _RouteEntry(newRoute, initialState: _RouteLifecycle.replace));
|
|
|
|
_history.RemoveAt(index);
|
|
_flushHistoryUpdates();
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public bool canPop() {
|
|
IEnumerable<_RouteEntry> iterator = new List<_RouteEntry>();
|
|
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isPresentPredicate(historyEntry)) {
|
|
iterator.Append(historyEntry);
|
|
}
|
|
}
|
|
if (!iterator.GetEnumerator().MoveNext())
|
|
return false; // we have no active routes, so we can"t pop
|
|
if(iterator.GetEnumerator().Current.route.willHandlePopInternally)
|
|
return true; // the first route can handle pops itself, so we can pop
|
|
if (!iterator.GetEnumerator().MoveNext())
|
|
return false; // there"s only one route, so we can"t pop
|
|
return true; // there"s at least two routes, so we can pop
|
|
}
|
|
|
|
public Future<bool> maybePop<T>(T result = default(T)) {
|
|
///asyn
|
|
_RouteEntry lastEntry = null;
|
|
foreach (_RouteEntry routeEntry in _history) {
|
|
if (_RouteEntry.isPresentPredicate(routeEntry)) {
|
|
lastEntry = routeEntry;
|
|
}
|
|
}
|
|
|
|
D.assert(lastEntry.route._navigator == this);
|
|
// this is asynchronous // await
|
|
return lastEntry.route.willPop().then_(disposition => {
|
|
if (lastEntry == null) {
|
|
return false;
|
|
}
|
|
D.assert(disposition != null);
|
|
if (!mounted)
|
|
return true; // forget about this pop, we were disposed in the meantime
|
|
_RouteEntry newLastEntry = null;
|
|
|
|
foreach (_RouteEntry history in _history) {
|
|
if (_RouteEntry.isPresentPredicate(history)) {
|
|
newLastEntry = history;
|
|
}
|
|
}
|
|
|
|
if (lastEntry != newLastEntry)
|
|
return true; // forget about this pop, something happened to our history in the meantime
|
|
switch (disposition) {
|
|
case RoutePopDisposition.bubble:
|
|
return false;
|
|
case RoutePopDisposition.pop:
|
|
pop(result);
|
|
return true;
|
|
case RoutePopDisposition.doNotPop:
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
|
|
}).to<bool>();
|
|
|
|
}
|
|
|
|
public void pop<T>(T result = default) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
|
|
_RouteEntry entry = null;
|
|
foreach (_RouteEntry route in _history) {
|
|
if (_RouteEntry.isPresentPredicate(route)) {
|
|
entry = route;
|
|
}
|
|
}
|
|
|
|
if (entry.hasPage) {
|
|
if (widget.onPopPage(entry.route, result))
|
|
entry.currentState = _RouteLifecycle.pop;
|
|
}
|
|
else {
|
|
entry.pop<T>(result);
|
|
}
|
|
|
|
if (entry.currentState == _RouteLifecycle.pop) {
|
|
|
|
_flushHistoryUpdates(rearrangeOverlay: false);
|
|
D.assert(entry.route._popCompleter.isCompleted);
|
|
}
|
|
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
_afterNavigation(entry.route);
|
|
}
|
|
|
|
|
|
public void popUntil(RoutePredicate predicate) {
|
|
_RouteEntry routeEntry = null;
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isPresentPredicate(historyEntry)) {
|
|
routeEntry = historyEntry;
|
|
}
|
|
}
|
|
|
|
while (!predicate(routeEntry.route)) {
|
|
pop<object>();
|
|
}
|
|
}
|
|
|
|
public void removeRoute(Route route) {
|
|
D.assert(route != null);
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(route._navigator == this);
|
|
bool wasCurrent = route.isCurrent;
|
|
_RouteEntry entry = null;
|
|
foreach (_RouteEntry routeEntry in _history) {
|
|
if (_RouteEntry.isRoutePredicate(route)(routeEntry)) {
|
|
entry = routeEntry;
|
|
break;
|
|
}
|
|
}
|
|
|
|
D.assert(entry != null);
|
|
entry.remove();
|
|
_flushHistoryUpdates(rearrangeOverlay: false);
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
if (wasCurrent) {
|
|
_RouteEntry lastEntry = null;
|
|
foreach (_RouteEntry routeEntry in _history) {
|
|
if (_RouteEntry.isPresentPredicate(routeEntry)) {
|
|
lastEntry = routeEntry;
|
|
}
|
|
}
|
|
|
|
_afterNavigation(
|
|
lastEntry?.route
|
|
);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
public void removeRouteBelow(Route anchorRoute) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(() => {
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(anchorRoute != null);
|
|
D.assert(anchorRoute._navigator == this);
|
|
int anchorIndex = -1;
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isRoutePredicate(anchorRoute)(historyEntry)) {
|
|
anchorIndex = _history.IndexOf(historyEntry);
|
|
break;
|
|
}
|
|
}
|
|
|
|
D.assert(anchorIndex >= 0, () => "This Navigator does not contain the specified anchorRoute.");
|
|
D.assert(_history[anchorIndex].isPresent,
|
|
() => "The specified anchorRoute has already been removed from the Navigator.");
|
|
int index = anchorIndex - 1;
|
|
while (index >= 0) {
|
|
if (_history[index].isPresent)
|
|
break;
|
|
index -= 1;
|
|
}
|
|
|
|
D.assert(index >= 0, () => "There are no routes below the specified anchorRoute.");
|
|
_history.RemoveAt(index);
|
|
_flushHistoryUpdates(rearrangeOverlay: false);
|
|
D.assert(() => {
|
|
_debugLocked = false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public void finalizeRoute(Route route) {
|
|
bool wasDebugLocked = false;
|
|
D.assert(() => {
|
|
wasDebugLocked = _debugLocked;
|
|
_debugLocked = true;
|
|
return true;
|
|
});
|
|
|
|
List<_RouteEntry> routeEntries = new List<_RouteEntry>();
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isRoutePredicate(route)(historyEntry)) {
|
|
routeEntries.Add(historyEntry);
|
|
}
|
|
}
|
|
D.assert(routeEntries.Count == 1);
|
|
|
|
_RouteEntry entry = null;
|
|
foreach (var historyEntry in _history) {
|
|
if (_RouteEntry.isRoutePredicate(route)(historyEntry)) {
|
|
entry = historyEntry;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (entry.doingPop) {
|
|
entry.currentState = _RouteLifecycle.pop;
|
|
_flushHistoryUpdates(rearrangeOverlay: false);
|
|
}
|
|
|
|
D.assert(entry.currentState != _RouteLifecycle.pop);
|
|
entry.finalize();
|
|
_flushHistoryUpdates(rearrangeOverlay: false);
|
|
D.assert(() => {
|
|
_debugLocked = wasDebugLocked;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public int _userGesturesInProgress {
|
|
get { return _userGesturesInProgressCount; }
|
|
set {
|
|
_userGesturesInProgressCount = value;
|
|
userGestureInProgressNotifier.value = _userGesturesInProgress > 0;
|
|
}
|
|
}
|
|
|
|
int _userGesturesInProgressCount = 0;
|
|
|
|
|
|
public bool userGestureInProgress {
|
|
get { return userGestureInProgressNotifier.value; }
|
|
|
|
}
|
|
|
|
|
|
public ValueNotifier<bool> userGestureInProgressNotifier = new ValueNotifier<bool>(false);
|
|
|
|
|
|
public void didStartUserGesture() {
|
|
_userGesturesInProgress += 1;
|
|
if (_userGesturesInProgress == 1) {
|
|
int routeIndex = _getIndexBefore(
|
|
_history.Count - 1,
|
|
_RouteEntry.willBePresentPredicate
|
|
);
|
|
D.assert(routeIndex != null);
|
|
Route route = _history[routeIndex].route;
|
|
Route previousRoute = null;
|
|
if (!route.willHandlePopInternally && routeIndex > 0) {
|
|
previousRoute = (Route) _getRouteBefore(
|
|
routeIndex - 1,
|
|
_RouteEntry.willBePresentPredicate
|
|
).route;
|
|
}
|
|
|
|
foreach (NavigatorObserver observer in widget.observers)
|
|
observer.didStartUserGesture(route, previousRoute);
|
|
}
|
|
}
|
|
|
|
public void didStopUserGesture() {
|
|
D.assert(_userGesturesInProgress > 0);
|
|
_userGesturesInProgress -= 1;
|
|
if (_userGesturesInProgress == 0) {
|
|
foreach (NavigatorObserver observer in widget.observers)
|
|
observer.didStopUserGesture();
|
|
}
|
|
}
|
|
|
|
public readonly HashSet<int> _activePointers = new HashSet<int>();
|
|
|
|
void _handlePointerDown(PointerDownEvent evt) {
|
|
_activePointers.Add(evt.pointer);
|
|
}
|
|
|
|
void _handlePointerUpOrCancel(PointerEvent evt) {
|
|
_activePointers.Remove(evt.pointer);
|
|
}
|
|
|
|
void _cancelActivePointers() {
|
|
// TODO(abarth): This mechanism is far from perfect. See https://github.com/flutter/flutter/issues/4770
|
|
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) {
|
|
|
|
RenderAbsorbPointer absorber =
|
|
_overlayKey.currentContext?.findAncestorRenderObjectOfType<RenderAbsorbPointer>();
|
|
setState(() => {
|
|
if(absorber != null)
|
|
absorber.absorbing = true;
|
|
});
|
|
}
|
|
|
|
_activePointers.ToList().ForEach(WidgetsBinding.instance.cancelPointer);
|
|
}
|
|
|
|
|
|
public override Widget build(BuildContext context) {
|
|
D.assert(!_debugLocked);
|
|
D.assert(_history.isNotEmpty());
|
|
return new Listener(
|
|
onPointerDown: _handlePointerDown,
|
|
onPointerUp: _handlePointerUpOrCancel,
|
|
onPointerCancel: _handlePointerUpOrCancel,
|
|
child: new AbsorbPointer(
|
|
absorbing: false, // it"s mutated directly by _cancelActivePointers above
|
|
child: new FocusScope(
|
|
node: focusScopeNode,
|
|
autofocus: true,
|
|
child: new Overlay(
|
|
key: _overlayKey,
|
|
initialEntries: overlay == null
|
|
? _allRouteOverlayEntries.ToList()
|
|
: new List<OverlayEntry>())
|
|
)
|
|
)
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
}
|