您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
786 行
30 KiB
786 行
30 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using RSG;
|
|
using RSG.Promises;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.gestures;
|
|
using Unity.UIWidgets.rendering;
|
|
using Unity.UIWidgets.scheduler;
|
|
|
|
namespace Unity.UIWidgets.widgets {
|
|
public delegate Route RouteFactory(RouteSettings settings);
|
|
|
|
public delegate bool RoutePredicate(Route route);
|
|
|
|
public delegate IPromise<bool> WillPopCallback();
|
|
|
|
public enum RoutePopDisposition {
|
|
pop,
|
|
doNotPop,
|
|
bubble
|
|
}
|
|
|
|
public abstract class Route {
|
|
public readonly RouteSettings settings;
|
|
|
|
internal NavigatorState _navigator;
|
|
|
|
public Route(RouteSettings settings = null) {
|
|
this.settings = settings ?? new RouteSettings();
|
|
}
|
|
|
|
public NavigatorState navigator => this._navigator;
|
|
|
|
public virtual List<OverlayEntry> overlayEntries => new List<OverlayEntry>();
|
|
|
|
public virtual bool willHandlePopInternally => false;
|
|
|
|
public object currentResult => default;
|
|
|
|
public Promise<object> popped { get; } = new Promise<object>();
|
|
|
|
public bool isCurrent => this._navigator != null && this._navigator._history.last() == this;
|
|
|
|
public bool isFirst => this._navigator != null && this._navigator._history.first() == this;
|
|
|
|
public bool isActive => this._navigator != null && this._navigator._history.Contains(this);
|
|
|
|
protected internal virtual void install(OverlayEntry insertionPoint) {
|
|
}
|
|
|
|
protected internal virtual TickerFuture didPush() {
|
|
return TickerFutureImpl.complete();
|
|
}
|
|
|
|
protected internal virtual void didReplace(Route oldRoute) {
|
|
}
|
|
|
|
public virtual IPromise<RoutePopDisposition> willPop() {
|
|
return Promise<RoutePopDisposition>.Resolved(this.isFirst
|
|
? RoutePopDisposition.bubble
|
|
: RoutePopDisposition.pop);
|
|
}
|
|
|
|
protected internal virtual bool didPop(object result) {
|
|
this.didComplete(result);
|
|
return true;
|
|
}
|
|
|
|
protected internal virtual void didComplete(object result) {
|
|
this.popped.Resolve(result);
|
|
}
|
|
|
|
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() {
|
|
}
|
|
|
|
protected internal virtual void dispose() {
|
|
this._navigator = null;
|
|
}
|
|
}
|
|
|
|
public class RouteSettings {
|
|
public readonly bool isInitialRoute;
|
|
|
|
public readonly string name;
|
|
|
|
public RouteSettings(string name = null, bool isInitialRoute = false) {
|
|
this.name = name;
|
|
this.isInitialRoute = isInitialRoute;
|
|
}
|
|
|
|
RouteSettings copyWith(string name = null, bool? isInitialRoute = null) {
|
|
return new RouteSettings(
|
|
name ?? this.name,
|
|
isInitialRoute ?? this.isInitialRoute
|
|
);
|
|
}
|
|
|
|
|
|
public override string ToString() {
|
|
return $"\"{this.name}\"";
|
|
}
|
|
}
|
|
|
|
public class NavigatorObserver {
|
|
internal NavigatorState _navigator;
|
|
|
|
public NavigatorState navigator => this._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 class Navigator : StatefulWidget {
|
|
/// The default name for the [initialRoute].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [dart:ui.Window.defaultRouteName], which reflects the route that the
|
|
/// application was started with.
|
|
public static string defaultRouteName = "/";
|
|
|
|
public readonly string initialRoute;
|
|
|
|
public readonly List<NavigatorObserver> observers;
|
|
|
|
public readonly RouteFactory onGenerateRoute;
|
|
|
|
public readonly RouteFactory onUnknownRoute;
|
|
|
|
public Navigator(Key key = null, string initialRoute = null,
|
|
RouteFactory onGenerateRoute = null, RouteFactory onUnknownRoute = null,
|
|
List<NavigatorObserver> observers = null) : base(key) {
|
|
D.assert(onGenerateRoute != null);
|
|
this.initialRoute = initialRoute;
|
|
this.onUnknownRoute = onUnknownRoute;
|
|
this.onGenerateRoute = onGenerateRoute;
|
|
this.observers = observers ?? new List<NavigatorObserver>();
|
|
}
|
|
|
|
public static IPromise<object> pushName(BuildContext context, string routeName) {
|
|
return of(context).pushNamed(routeName);
|
|
}
|
|
|
|
public static IPromise<object> pushReplacementNamed(BuildContext context, string routeName,
|
|
object result = null) {
|
|
return of(context).pushReplacementNamed(routeName, result);
|
|
}
|
|
|
|
public static IPromise<object> popAndPushNamed(BuildContext context, string routeName, object result = null) {
|
|
return of(context).popAndPushNamed(routeName, result);
|
|
}
|
|
|
|
public static IPromise<object> pushNamedAndRemoveUntil(BuildContext context, string newRouteName,
|
|
RoutePredicate predicate) {
|
|
return of(context).pushNamedAndRemoveUntil(newRouteName, predicate);
|
|
}
|
|
|
|
public static IPromise<object> push(BuildContext context, Route route) {
|
|
return of(context).push(route);
|
|
}
|
|
|
|
public static IPromise<object> pushReplacement(BuildContext context, Route newRoute, object result = null) {
|
|
return of(context).pushReplacement(newRoute, result);
|
|
}
|
|
|
|
public static IPromise<object> pushAndRemoveUntil(BuildContext context, Route newRoute,
|
|
RoutePredicate predicate) {
|
|
return of(context).pushAndRemoveUntil(newRoute, predicate);
|
|
}
|
|
|
|
public static void replace(BuildContext context, Route oldRoute, Route newRoute) {
|
|
of(context).replace(oldRoute, newRoute);
|
|
}
|
|
|
|
public static void replaceRouteBelow(BuildContext context, Route anchorRoute = null, Route newRoute = null) {
|
|
of(context).replaceRouteBelow(anchorRoute, newRoute);
|
|
}
|
|
|
|
|
|
public static IPromise<bool> maybePop(BuildContext context, object result = null) {
|
|
return of(context).maybePop(result);
|
|
}
|
|
|
|
public static bool pop(BuildContext context, object result = null) {
|
|
return 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);
|
|
}
|
|
|
|
|
|
static void removeRouteBelow(BuildContext context, Route anchorRoute) {
|
|
of(context).removeRouteBelow(anchorRoute);
|
|
}
|
|
|
|
public static NavigatorState of(
|
|
BuildContext context,
|
|
bool rootNavigator = false,
|
|
bool nullOk = false
|
|
) {
|
|
var navigator = rootNavigator
|
|
? (NavigatorState) context.rootAncestorStateOfType(new TypeMatcher<NavigatorState>())
|
|
: (NavigatorState) context.ancestorStateOfType(new TypeMatcher<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 override State createState() {
|
|
return new NavigatorState();
|
|
}
|
|
}
|
|
|
|
public class NavigatorState : TickerProviderStateMixin<Navigator> {
|
|
internal readonly List<Route> _history = new List<Route>();
|
|
readonly GlobalKey<OverlayState> _overlayKey = new LabeledGlobalKey<OverlayState>();
|
|
readonly HashSet<Route> _poppedRoutes = new HashSet<Route>();
|
|
public readonly FocusScopeNode focusScopeNode = new FocusScopeNode();
|
|
|
|
readonly HashSet<int> _activePointers = new HashSet<int>();
|
|
|
|
bool _debugLocked;
|
|
readonly List<OverlayEntry> _initialOverlayEntries = new List<OverlayEntry>();
|
|
|
|
int _userGesturesInProgress;
|
|
|
|
public OverlayState overlay => this._overlayKey.currentState;
|
|
|
|
OverlayEntry _currentOverlayEntry {
|
|
get {
|
|
var route = this._history.FindLast(r => r.overlayEntries.isNotEmpty());
|
|
return route?.overlayEntries.last();
|
|
}
|
|
}
|
|
|
|
public bool userGestureInProgress => this._userGesturesInProgress > 0;
|
|
|
|
public override void initState() {
|
|
base.initState();
|
|
foreach (var observer in this.widget.observers) {
|
|
D.assert(observer.navigator == null);
|
|
observer._navigator = this;
|
|
}
|
|
|
|
var initialRouteName = this.widget.initialRoute ?? Navigator.defaultRouteName;
|
|
if (initialRouteName.StartsWith("/") && initialRouteName.Length > 1) {
|
|
initialRouteName = initialRouteName.Substring(1);
|
|
D.assert(Navigator.defaultRouteName == "/");
|
|
var plannedInitialRouteNames = new List<string> {
|
|
Navigator.defaultRouteName
|
|
};
|
|
var plannedInitialRoutes = new List<Route> {
|
|
this._routeNamed(Navigator.defaultRouteName, true)
|
|
};
|
|
|
|
var routeParts = initialRouteName.Split('/');
|
|
if (initialRouteName.isNotEmpty()) {
|
|
var routeName = "";
|
|
foreach (var part in routeParts) {
|
|
routeName += $"/{part}";
|
|
plannedInitialRouteNames.Add(routeName);
|
|
plannedInitialRoutes.Add(this._routeNamed(routeName, true));
|
|
}
|
|
}
|
|
|
|
if (plannedInitialRoutes.Contains(null)) {
|
|
D.assert(() => {
|
|
UIWidgetsError.reportError(new UIWidgetsErrorDetails(
|
|
new Exception(
|
|
"Could not navigate to initial route.\n" +
|
|
$"The requested route name was: \"{initialRouteName}\n" +
|
|
"The following routes were therefore attempted:\n" +
|
|
$" * {string.Join("\n * ", plannedInitialRouteNames)}\n" +
|
|
"This resulted in the following objects:\n" +
|
|
$" * {string.Join("\n * ", plannedInitialRoutes)}\n" +
|
|
"One or more of those objects was null, and therefore the initial route specified will be " +
|
|
$"ignored and \"{Navigator.defaultRouteName}\" will be used instead.")));
|
|
return true;
|
|
});
|
|
this.push(this._routeNamed(Navigator.defaultRouteName));
|
|
}
|
|
else {
|
|
plannedInitialRoutes.Each(route => { this.push(route); });
|
|
}
|
|
}
|
|
else {
|
|
Route route = null;
|
|
if (initialRouteName != Navigator.defaultRouteName) route = this._routeNamed(initialRouteName, true);
|
|
|
|
route = route ?? this._routeNamed(Navigator.defaultRouteName);
|
|
this.push(route);
|
|
}
|
|
|
|
foreach (var route in this._history) this._initialOverlayEntries.AddRange(route.overlayEntries);
|
|
}
|
|
|
|
public override void didUpdateWidget(StatefulWidget oldWidget) {
|
|
base.didUpdateWidget(oldWidget);
|
|
if (((Navigator) oldWidget).observers != this.widget.observers) {
|
|
foreach (var observer in ((Navigator) oldWidget).observers) observer._navigator = null;
|
|
|
|
foreach (var observer in this.widget.observers) {
|
|
D.assert(observer.navigator == null);
|
|
observer._navigator = this;
|
|
}
|
|
}
|
|
|
|
foreach (var route in this._history) route.changedExternalState();
|
|
}
|
|
|
|
public override void dispose() {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(() => {
|
|
this._debugLocked = true;
|
|
return true;
|
|
});
|
|
foreach (var observer in this.widget.observers) observer._navigator = null;
|
|
|
|
var doomed = this._poppedRoutes.ToList();
|
|
doomed.AddRange(this._history);
|
|
foreach (var route in doomed) route.dispose();
|
|
|
|
this._poppedRoutes.Clear();
|
|
this._history.Clear();
|
|
this.focusScopeNode.detach();
|
|
base.dispose();
|
|
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
Route _routeNamed(string name, bool allowNull = false) {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(name != null);
|
|
var settings = new RouteSettings(name, this._history.isEmpty());
|
|
var route = this.widget.onGenerateRoute(settings);
|
|
if (route == null && !allowNull) {
|
|
D.assert(() => {
|
|
if (this.widget.onUnknownRoute == null)
|
|
throw new UIWidgetsError(
|
|
"If a Navigator has no onUnknownRoute, then its onGenerateRoute must never return null.\n" +
|
|
$"When trying to build the route \"{name}\", onGenerateRoute returned null, but there was no " +
|
|
"onUnknownRoute callback specified.\n" +
|
|
"The Navigator was:\n" +
|
|
$" {this}");
|
|
|
|
return true;
|
|
});
|
|
|
|
route = this.widget.onUnknownRoute(settings);
|
|
D.assert(() => {
|
|
if (route == null)
|
|
throw new UIWidgetsError(
|
|
"A Navigator\'s onUnknownRoute returned null.\n" +
|
|
$"When trying to build the route \"{name}\", both onGenerateRoute and onUnknownRoute returned " +
|
|
"null. The onUnknownRoute callback should never return null.\n" +
|
|
"The Navigator was:\n" +
|
|
$" {this}"
|
|
);
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return route;
|
|
}
|
|
|
|
public Promise<object> pushNamed(string routeName) {
|
|
return this.push(this._routeNamed(routeName));
|
|
}
|
|
|
|
public Promise<object> pushReplacementNamed(string routeName, object result = null) {
|
|
return this.pushReplacement(this._routeNamed(routeName), result);
|
|
}
|
|
|
|
public Promise<object> popAndPushNamed(string routeName, object result = null) {
|
|
this.pop(result);
|
|
return this.pushNamed(routeName);
|
|
}
|
|
|
|
public Promise<object> pushNamedAndRemoveUntil(string newRouteName, RoutePredicate predicate) {
|
|
return this.pushAndRemoveUntil(this._routeNamed(newRouteName), predicate);
|
|
}
|
|
|
|
public Promise<object> push(Route route) {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(() => {
|
|
this._debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(route != null);
|
|
D.assert(route._navigator == null);
|
|
var oldRoute = this._history.isNotEmpty() ? this._history.last() : null;
|
|
route._navigator = this;
|
|
route.install(this._currentOverlayEntry);
|
|
this._history.Add(route);
|
|
route.didPush();
|
|
route.didChangeNext(null);
|
|
if (oldRoute != null) {
|
|
oldRoute.didChangeNext(route);
|
|
route.didChangePrevious(oldRoute);
|
|
}
|
|
|
|
foreach (var observer in this.widget.observers) observer.didPush(route, oldRoute);
|
|
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
this._afterNavigation();
|
|
return route.popped;
|
|
}
|
|
|
|
void _afterNavigation() {
|
|
}
|
|
|
|
public Promise<object> pushReplacement(Route newRoute, object result = null) {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(() => {
|
|
this._debugLocked = true;
|
|
return true;
|
|
});
|
|
var oldRoute = this._history.last();
|
|
D.assert(oldRoute != null && oldRoute._navigator == this);
|
|
D.assert(oldRoute.overlayEntries.isNotEmpty());
|
|
D.assert(newRoute._navigator == null);
|
|
D.assert(newRoute.overlayEntries.isEmpty());
|
|
var index = this._history.Count - 1;
|
|
D.assert(index >= 0);
|
|
D.assert(this._history.IndexOf(oldRoute) == index);
|
|
newRoute._navigator = this;
|
|
newRoute.install(this._currentOverlayEntry);
|
|
this._history[index] = newRoute;
|
|
newRoute.didPush().whenCompleteOrCancel(() => {
|
|
// The old route's exit is not animated. We're assuming that the
|
|
// new route completely obscures the old one.
|
|
if (this.mounted) {
|
|
oldRoute.didComplete(result ?? oldRoute.currentResult);
|
|
oldRoute.dispose();
|
|
}
|
|
});
|
|
newRoute.didChangeNext(null);
|
|
if (index > 0) {
|
|
this._history[index - 1].didChangeNext(newRoute);
|
|
newRoute.didChangePrevious(this._history[index - 1]);
|
|
}
|
|
|
|
foreach (var observer in this.widget.observers) observer.didReplace(newRoute, oldRoute);
|
|
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
this._afterNavigation();
|
|
return newRoute.popped;
|
|
}
|
|
|
|
public Promise<object> pushAndRemoveUntil(Route newRoute, RoutePredicate predicate) {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(() => {
|
|
this._debugLocked = true;
|
|
return true;
|
|
});
|
|
var removedRoutes = new List<Route>();
|
|
while (this._history.isNotEmpty() && !predicate(this._history.last())) {
|
|
var removedRoute = this._history.last();
|
|
this._history.RemoveAt(this._history.Count - 1);
|
|
D.assert(removedRoute != null && removedRoute._navigator == this);
|
|
D.assert(removedRoute.overlayEntries.isNotEmpty());
|
|
removedRoutes.Add(removedRoute);
|
|
}
|
|
|
|
D.assert(newRoute._navigator == null);
|
|
D.assert(newRoute.overlayEntries.isEmpty());
|
|
var oldRoute = this._history.isNotEmpty() ? this._history.last() : null;
|
|
newRoute._navigator = this;
|
|
newRoute.install(this._currentOverlayEntry);
|
|
this._history.Add(newRoute);
|
|
newRoute.didPush().whenCompleteOrCancel(() => {
|
|
if (this.mounted)
|
|
foreach (var route in removedRoutes)
|
|
route.dispose();
|
|
});
|
|
newRoute.didChangeNext(null);
|
|
if (oldRoute != null) oldRoute.didChangeNext(newRoute);
|
|
|
|
foreach (var observer in this.widget.observers) {
|
|
observer.didPush(newRoute, oldRoute);
|
|
foreach (var removedRoute in removedRoutes) observer.didRemove(removedRoute, oldRoute);
|
|
}
|
|
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
this._afterNavigation();
|
|
return newRoute.popped;
|
|
}
|
|
|
|
public void replace(Route oldRoute = null, Route newRoute = null) {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(oldRoute != null);
|
|
D.assert(newRoute != null);
|
|
if (oldRoute == newRoute
|
|
) // ignore: unrelated_type_equality_checks, https://github.com/dart-lang/sdk/issues/32522
|
|
return;
|
|
D.assert(() => {
|
|
this._debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(oldRoute._navigator == this);
|
|
D.assert(newRoute._navigator == null);
|
|
D.assert(oldRoute.overlayEntries.isNotEmpty());
|
|
D.assert(newRoute.overlayEntries.isEmpty());
|
|
D.assert(!this.overlay.debugIsVisible(oldRoute.overlayEntries.last()));
|
|
var index = this._history.IndexOf(oldRoute);
|
|
D.assert(index >= 0);
|
|
newRoute._navigator = this;
|
|
newRoute.install(oldRoute.overlayEntries.last());
|
|
this._history[index] = newRoute;
|
|
newRoute.didReplace(oldRoute);
|
|
if (index + 1 < this._history.Capacity) {
|
|
newRoute.didChangeNext(this._history[index + 1]);
|
|
this._history[index + 1].didChangePrevious(newRoute);
|
|
}
|
|
else {
|
|
newRoute.didChangeNext(null);
|
|
}
|
|
|
|
if (index > 0) {
|
|
this._history[index - 1].didChangeNext(newRoute);
|
|
newRoute.didChangePrevious(this._history[index - 1]);
|
|
}
|
|
|
|
foreach (var observer in this.widget.observers) observer.didReplace(newRoute, oldRoute);
|
|
|
|
oldRoute.dispose();
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public void replaceRouteBelow(Route anchorRoute = null, Route newRoute = null) {
|
|
D.assert(anchorRoute != null);
|
|
D.assert(anchorRoute._navigator == this);
|
|
D.assert(this._history.IndexOf(anchorRoute) > 0);
|
|
this.replace(this._history[this._history.IndexOf(anchorRoute) - 1], newRoute);
|
|
}
|
|
|
|
public bool canPop() {
|
|
D.assert(this._history.isNotEmpty);
|
|
return this._history.Count > 1 || this._history[0].willHandlePopInternally;
|
|
}
|
|
|
|
public IPromise<bool> maybePop(object result = null) {
|
|
var route = this._history.last();
|
|
D.assert(route._navigator == this);
|
|
return route.willPop().Then(disposition => {
|
|
if (disposition != RoutePopDisposition.bubble && this.mounted) {
|
|
if (disposition == RoutePopDisposition.pop) this.pop(result);
|
|
return Promise<bool>.Resolved(true);
|
|
}
|
|
|
|
return Promise<bool>.Resolved(false);
|
|
});
|
|
}
|
|
|
|
public bool pop(object result = null) {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(() => {
|
|
this._debugLocked = true;
|
|
return true;
|
|
});
|
|
var route = this._history.last();
|
|
D.assert(route._navigator == this);
|
|
var debugPredictedWouldPop = false;
|
|
D.assert(() => {
|
|
debugPredictedWouldPop = !route.willHandlePopInternally;
|
|
return true;
|
|
});
|
|
if (route.didPop(result ?? route.currentResult)) {
|
|
D.assert(debugPredictedWouldPop);
|
|
if (this._history.Count > 1) {
|
|
this._history.removeLast();
|
|
// If route._navigator is null, the route called finalizeRoute from
|
|
// didPop, which means the route has already been disposed and doesn't
|
|
// need to be added to _poppedRoutes for later disposal.
|
|
if (route._navigator != null) this._poppedRoutes.Add(route);
|
|
this._history.last().didPopNext(route);
|
|
foreach (var observer in this.widget.observers) observer.didPop(route, this._history.last());
|
|
}
|
|
else {
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
D.assert(!debugPredictedWouldPop);
|
|
}
|
|
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
this._afterNavigation();
|
|
return true;
|
|
}
|
|
|
|
public void popUntil(RoutePredicate predicate) {
|
|
while (!predicate(this._history.last())) this.pop();
|
|
}
|
|
|
|
public void removeRoute(Route route) {
|
|
D.assert(route != null);
|
|
D.assert(!this._debugLocked);
|
|
D.assert(() => {
|
|
this._debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(route._navigator == this);
|
|
var index = this._history.IndexOf(route);
|
|
D.assert(index != -1);
|
|
var previousRoute = index > 0 ? this._history[index - 1] : null;
|
|
var nextRoute = index + 1 < this._history.Count ? this._history[index + 1] : null;
|
|
this._history.RemoveAt(index);
|
|
previousRoute?.didChangeNext(nextRoute);
|
|
nextRoute?.didChangePrevious(previousRoute);
|
|
foreach (var observer in this.widget.observers) observer.didRemove(route, previousRoute);
|
|
|
|
route.dispose();
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
this._afterNavigation();
|
|
}
|
|
|
|
public void removeRouteBelow(Route anchorRoute) {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(() => {
|
|
this._debugLocked = true;
|
|
return true;
|
|
});
|
|
D.assert(anchorRoute._navigator == this);
|
|
var index = this._history.IndexOf(anchorRoute) - 1;
|
|
D.assert(index >= 0);
|
|
var targetRoute = this._history[index];
|
|
D.assert(targetRoute._navigator == this);
|
|
D.assert(targetRoute.overlayEntries.isEmpty() ||
|
|
!this.overlay.debugIsVisible(targetRoute.overlayEntries.last()));
|
|
this._history.RemoveAt(index);
|
|
var nextRoute = index < this._history.Count ? this._history[index] : null;
|
|
var previousRoute = index > 0 ? this._history[index - 1] : null;
|
|
if (previousRoute != null)
|
|
previousRoute.didChangeNext(nextRoute);
|
|
if (nextRoute != null)
|
|
nextRoute.didChangePrevious(previousRoute);
|
|
targetRoute.dispose();
|
|
D.assert(() => {
|
|
this._debugLocked = false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public void finalizeRoute(Route route) {
|
|
this._poppedRoutes.Remove(route);
|
|
route.dispose();
|
|
}
|
|
|
|
public void didStartUserGesture() {
|
|
this._userGesturesInProgress += 1;
|
|
if (this._userGesturesInProgress == 1) {
|
|
var route = this._history.last();
|
|
var previousRoute = !route.willHandlePopInternally && this._history.Count > 1
|
|
? this._history[this._history.Count - 2]
|
|
: null;
|
|
// Don't operate the _history list since the gesture may be cancelled.
|
|
// In case of a back swipe, the gesture controller will call .pop() itself.
|
|
foreach (var observer in this.widget.observers) observer.didStartUserGesture(route, previousRoute);
|
|
}
|
|
}
|
|
|
|
public void didStopUserGesture() {
|
|
D.assert(this._userGesturesInProgress > 0);
|
|
this._userGesturesInProgress -= 1;
|
|
if (this._userGesturesInProgress == 0)
|
|
foreach (var observer in this.widget.observers)
|
|
observer.didStopUserGesture();
|
|
}
|
|
|
|
|
|
void _handlePointerDown(PointerDownEvent evt) {
|
|
this._activePointers.Add(evt.pointer);
|
|
}
|
|
|
|
void _handlePointerUpOrCancel(PointerEvent evt) {
|
|
this._activePointers.Remove(evt.pointer);
|
|
}
|
|
|
|
void _cancelActivePointers() {
|
|
// TODO flutter issue https://github.com/flutter/flutter/issues/4770
|
|
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().
|
|
var absorber = (RenderAbsorbPointer) this._overlayKey.currentContext?
|
|
.ancestorRenderObjectOfType(new TypeMatcher<RenderAbsorbPointer>());
|
|
this.setState(() => {
|
|
if (absorber != null) absorber.absorbing = true;
|
|
});
|
|
}
|
|
|
|
foreach (var activePointer in this._activePointers) WidgetsBinding.instance.cancelPointer(activePointer);
|
|
}
|
|
|
|
|
|
public override Widget build(BuildContext context) {
|
|
D.assert(!this._debugLocked);
|
|
D.assert(this._history.isNotEmpty());
|
|
return new Listener(
|
|
onPointerDown: this._handlePointerDown,
|
|
onPointerUp: this._handlePointerUpOrCancel,
|
|
onPointerCancel: this._handlePointerUpOrCancel,
|
|
child: new AbsorbPointer(
|
|
absorbing: false, // it's mutated directly by _cancelActivePointers above
|
|
child: new FocusScope(
|
|
this.focusScopeNode,
|
|
autofocus: true,
|
|
child: new Overlay(
|
|
this._overlayKey,
|
|
this._initialOverlayEntries
|
|
)
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|