navigator.dart 20.1 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/rendering.dart';
6 7
import 'package:meta/meta.dart';

8 9
import 'basic.dart';
import 'binding.dart';
10
import 'focus.dart';
11
import 'framework.dart';
Adam Barth's avatar
Adam Barth committed
12
import 'overlay.dart';
13

14 15 16 17 18 19
/// An abstraction for an entry managed by a [Navigator].
///
/// This class defines an abstract interface between the navigator and the
/// "routes" that are pushed on and popped off the navigator. Most routes have
/// visual affordances, which they place in the navigators [Overlay] using one
/// or more [OverlayEntry] objects.
Hixie's avatar
Hixie committed
20
abstract class Route<T> {
21 22 23 24
  /// The navigator that the route is in, if any.
  NavigatorState get navigator => _navigator;
  NavigatorState _navigator;

25
  /// The overlay entries for this route.
26 27
  List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];

28 29 30 31 32 33
  /// The key this route will use for its root [Focus] widget, if any.
  ///
  /// If this route is the first route shown by the navigator, the navigator
  /// will initialize its [Focus] to this key.
  GlobalKey get focusKey => null;

34
  /// Called when the route is inserted into the navigator.
35
  ///
Hixie's avatar
Hixie committed
36 37 38 39
  /// Use this to populate overlayEntries and add them to the overlay
  /// (accessible as navigator.overlay). (The reason the Route is responsible
  /// for doing this, rather than the Navigator, is that the Route will be
  /// responsible for _removing_ the entries and this way it's symmetric.)
40 41
  ///
  /// The overlay argument will be null if this is the first route inserted.
Hixie's avatar
Hixie committed
42
  void install(OverlayEntry insertionPoint) { }
43 44 45

  /// Called after install() when the route is pushed onto the navigator.
  void didPush() { }
Hixie's avatar
Hixie committed
46

47 48 49 50
  /// When this route is popped (see [Navigator.pop]) if the result isn't
  /// specified or if it's null, this value will be used instead.
  T get currentResult => null;

51
  /// Called after install() when the route replaced another in the navigator.
52
  void didReplace(Route<dynamic> oldRoute) { }
53

Hixie's avatar
Hixie committed
54 55 56 57
  /// A request was made to pop this route. If the route can handle it
  /// internally (e.g. because it has its own stack of internal state) then
  /// return false, otherwise return true. Returning false will prevent the
  /// default behavior of NavigatorState.pop().
58 59 60
  ///
  /// If this is called, the Navigator will not call dispose(). It is the
  /// responsibility of the Route to later call dispose().
Hixie's avatar
Hixie committed
61
  bool didPop(T result) => true;
62

Hixie's avatar
Hixie committed
63 64 65
  /// Whether calling didPop() would return false.
  bool get willHandlePopInternally => false;

66
  /// The given route, which came after this one, has been popped off the
67
  /// navigator.
68
  void didPopNext(Route<dynamic> nextRoute) { }
69

Hixie's avatar
Hixie committed
70 71 72 73
  /// This route's next route has changed to the given new route. This is called
  /// on a route whenever the next route changes for any reason, except for
  /// cases when didPopNext() would be called, so long as it is in the history.
  /// nextRoute will be null if there's no next route.
74
  void didChangeNext(Route<dynamic> nextRoute) { }
75

76 77 78 79 80 81 82
  /// The route should remove its overlays and free any other resources.
  ///
  /// A call to didPop() implies that the Route should call dispose() itself,
  /// but it is possible for dispose() to be called directly (e.g. if the route
  /// is replaced, or if the navigator itself is disposed).
  void dispose() { }

83 84 85 86 87 88 89
  // If the route's transition can be popped via a user gesture (e.g. the iOS
  // back gesture), this should return a controller object that can be used
  // to control the transition animation's progress.
  NavigationGestureController startPopGesture(NavigatorState navigator) {
    return null;
  }

90
  /// Whether this route is the top-most route on the navigator.
91 92
  ///
  /// If this is true, then [isActive] is also true.
93 94 95 96 97 98
  bool get isCurrent {
    if (_navigator == null)
      return false;
    assert(_navigator._history.contains(this));
    return _navigator._history.last == this;
  }
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113

  /// Whether this route is on the navigator.
  ///
  /// If the route is not only active, but also the current route (the top-most
  /// route), then [isCurrent] will also be true.
  ///
  /// If a later route is entirely opaque, then the route will be active but not
  /// rendered. In particular, it's possible for a route to be active but for
  /// stateful widgets within the route to not be instantiated.
  bool get isActive {
    if (_navigator == null)
      return false;
    assert(_navigator._history.contains(this));
    return true;
  }
Adam Barth's avatar
Adam Barth committed
114 115
}

116
/// Data that might be useful in constructing a [Route].
117
class RouteSettings {
118
  /// Creates data used to construct routes.
119
  const RouteSettings({
120 121 122 123
    this.name,
    this.isInitialRoute: false
  });

124
  /// The name of the route (e.g., "/settings").
125 126
  ///
  /// If null, the route is anonymous.
Adam Barth's avatar
Adam Barth committed
127
  final String name;
128 129 130 131

  /// Whether this route is the very first route being pushed onto this [Navigator].
  ///
  /// The initial route typically skips any entrance transition to speed startup.
132
  final bool isInitialRoute;
133

134
  @override
135
  String toString() => '"$name"';
Adam Barth's avatar
Adam Barth committed
136 137
}

138
/// Creates a route for the given route settings.
139
typedef Route<dynamic> RouteFactory(RouteSettings settings);
140 141

/// An interface for observing the behavior of a [Navigator].
142
class NavigatorObserver {
143
  /// The navigator that the observer is observing, if any.
144
  NavigatorState get navigator => _navigator;
145
  NavigatorState _navigator;
146 147

  /// The [Navigator] pushed the given route.
148
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { }
149

Hans Muller's avatar
Hans Muller committed
150
  /// The [Navigator] popped the given route.
151
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) { }
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184

  /// The [Navigator] is being controlled by a user gesture. Used for the
  /// iOS back gesture.
  void didStartUserGesture() { }

  /// User gesture is no longer controlling the [Navigator].
  void didStopUserGesture() { }
}

// An interface to be implemented by the Route, allowing its transition
// animation to be controlled by a drag.
abstract class NavigationGestureController {
  NavigationGestureController(this._navigator) {
    // Disable Hero transitions until the gesture is complete.
    _navigator.didStartUserGesture();
  }

  // Must be called when the gesture is done.
  void dispose() {
    _navigator.didStopUserGesture();
    _navigator = null;
  }

  // The drag gesture has changed by [fractionalDelta]. The total range of the
  // drag should be 0.0 to 1.0.
  void dragUpdate(double fractionalDelta);

  // The drag gesture has ended.
  void dragEnd();

  @protected
  NavigatorState get navigator => _navigator;
  NavigatorState _navigator;
185 186
}

Hans Muller's avatar
Hans Muller committed
187 188 189
/// Signature for the [Navigator.popUntil] predicate argument.
typedef bool RoutePredicate(Route<dynamic> route);

190
/// A widget that manages a set of child widgets with a stack discipline.
191 192 193 194 195 196 197
///
/// Many apps have a navigator near the top of their widget hierarchy in order
/// to display their logical history using an [Overlay] with the most recently
/// visited pages visually on top of the older pages. Using this pattern lets
/// the navigator visually transition from one page to another by the widgets
/// around in the overlay. Similarly, the navigator can be used to show a dialog
/// by positioning the dialog widget above the current page.
198
class Navigator extends StatefulWidget {
199 200 201
  /// Creates a widget that maintains a stack-based history of child widgets.
  ///
  /// The [onGenerateRoute] argument must not be null.
202 203
  Navigator({
    Key key,
204
    this.initialRoute,
205
    @required this.onGenerateRoute,
206 207
    this.onUnknownRoute,
    this.observer
208
  }) : super(key: key) {
Adam Barth's avatar
Adam Barth committed
209
    assert(onGenerateRoute != null);
210 211
  }

212
  /// The name of the first route to show.
213
  final String initialRoute;
214 215

  /// Called to generate a route for a given [RouteSettings].
Adam Barth's avatar
Adam Barth committed
216
  final RouteFactory onGenerateRoute;
217 218 219 220 221 222 223 224 225

  /// Called when [onGenerateRoute] fails to generate a route.
  ///
  /// This callback is typically used for error handling. For example, this
  /// callback might always generate a "not found" page that describes the route
  /// that wasn't found.
  ///
  /// Unknown routes can arise either from errors in the app or from external
  /// requests to push routes, such as from Android intents.
Adam Barth's avatar
Adam Barth committed
226
  final RouteFactory onUnknownRoute;
227 228

  /// An observer for this navigator.
229
  final NavigatorObserver observer;
Adam Barth's avatar
Adam Barth committed
230

231
  /// The default name for the initial route.
Adam Barth's avatar
Adam Barth committed
232
  static const String defaultRouteName = '/';
233

234 235 236
  /// Push a named route onto the navigator that most tightly encloses the given context.
  ///
  /// The route name will be passed to that navigator's [onGenerateRoute]
237 238 239
  /// callback. The returned route will be pushed into the navigator.
  static void pushNamed(BuildContext context, String routeName) {
    Navigator.of(context).pushNamed(routeName);
Hixie's avatar
Hixie committed
240 241
  }

242 243 244 245 246 247
  /// Push a route onto the navigator that most tightly encloses the given context.
  ///
  /// Adds the given route to the Navigator's history, and transitions to it.
  /// The route will have didPush() and didChangeNext() called on it; the
  /// previous route, if any, will have didChangeNext() called on it; and the
  /// Navigator observer, if any, will have didPush() called on it.
248
  static void push(BuildContext context, Route<dynamic> route) {
249
    Navigator.of(context).push(route);
Hixie's avatar
Hixie committed
250 251
  }

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
  /// Pop a route off the navigator that most tightly encloses the given context.
  ///
  /// Tries to removes the current route, calling its didPop() method. If that
  /// method returns false, then nothing else happens. Otherwise, the observer
  /// (if any) is notified using its didPop() method, and the previous route is
  /// notified using [Route.didChangeNext].
  ///
  /// If non-null, [result] will be used as the result of the route. Routes
  /// such as dialogs or popup menus typically use this mechanism to return the
  /// value selected by the user to the widget that created their route. The
  /// type of [result], if provided, must match the type argument of the class
  /// of the current route. (In practice, this is usually "dynamic".)
  ///
  /// Returns true if a route was popped; returns false if there are no further
  /// previous routes.
Hixie's avatar
Hixie committed
267
  static bool pop(BuildContext context, [ dynamic result ]) {
268
    return Navigator.of(context).pop(result);
Hixie's avatar
Hixie committed
269
  }
270

Hans Muller's avatar
Hans Muller committed
271 272 273 274 275
  /// Calls [pop()] repeatedly until the predicate returns false.
  /// The predicate may be applied to the same route more than once if
  /// [Route.willHandlePopInternally] is true.
  static void popUntil(BuildContext context, RoutePredicate predicate) {
    Navigator.of(context).popUntil(predicate);
Hixie's avatar
Hixie committed
276
  }
Hixie's avatar
Hixie committed
277

278 279 280 281 282
  /// Whether the navigator that most tightly encloses the given context can be popped.
  ///
  /// The initial route cannot be popped off the navigator, which implies that
  /// this function returns true only if popping the navigator would not remove
  /// the initial route.
Hixie's avatar
Hixie committed
283
  static bool canPop(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
284
    NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
285
    return navigator != null && navigator.canPop();
Hixie's avatar
Hixie committed
286 287
  }

288 289
  /// Executes a simple transaction that both pops the current route off and
  /// pushes a named route into the navigator that most tightly encloses the given context.
290
  static void popAndPushNamed(BuildContext context, String routeName) {
291 292
    Navigator.of(context)
      ..pop()
293
      ..pushNamed(routeName);
Hixie's avatar
Hixie committed
294 295
  }

296
  static NavigatorState of(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
297
    NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
298
    assert(() {
299
      if (navigator == null) {
300
        throw new FlutterError(
301 302
          '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.'
303 304
        );
      }
305 306
      return true;
    });
307
    return navigator;
Hixie's avatar
Hixie committed
308
  }
Adam Barth's avatar
Adam Barth committed
309

310
  @override
311
  NavigatorState createState() => new NavigatorState();
312 313
}

314
/// The state for a [Navigator] widget.
315
class NavigatorState extends State<Navigator> {
Adam Barth's avatar
Adam Barth committed
316
  final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>();
317
  final List<Route<dynamic>> _history = new List<Route<dynamic>>();
318

319
  @override
320 321
  void initState() {
    super.initState();
322 323
    assert(config.observer == null || config.observer.navigator == null);
    config.observer?._navigator = this;
324
    push(config.onGenerateRoute(new RouteSettings(
325 326
      name: config.initialRoute ?? Navigator.defaultRouteName,
      isInitialRoute: true
327
    )));
328 329
  }

330
  @override
331 332 333 334 335 336 337 338
  void didUpdateConfig(Navigator oldConfig) {
    if (oldConfig.observer != config.observer) {
      oldConfig.observer?._navigator = null;
      assert(config.observer == null || config.observer.navigator == null);
      config.observer?._navigator = this;
    }
  }

339
  @override
340
  void dispose() {
341 342
    assert(!_debugLocked);
    assert(() { _debugLocked = true; return true; });
343
    config.observer?._navigator = null;
344
    for (Route<dynamic> route in _history) {
345 346 347
      route.dispose();
      route._navigator = null;
    }
348
    super.dispose();
349
    assert(() { _debugLocked = false; return true; });
350 351
  }

352
  /// The overlay this navigator uses for its visual presentation.
Adam Barth's avatar
Adam Barth committed
353
  OverlayState get overlay => _overlayKey.currentState;
354

Hixie's avatar
Hixie committed
355
  OverlayEntry get _currentOverlayEntry {
356
    for (Route<dynamic> route in _history.reversed) {
Adam Barth's avatar
Adam Barth committed
357 358
      if (route.overlayEntries.isNotEmpty)
        return route.overlayEntries.last;
359
    }
Adam Barth's avatar
Adam Barth committed
360
    return null;
361 362
  }

363 364
  bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends

365
  void pushNamed(String name) {
366
    assert(!_debugLocked);
367
    assert(name != null);
368
    RouteSettings settings = new RouteSettings(name: name);
369
    Route<dynamic> route = config.onGenerateRoute(settings);
370 371 372 373 374
    if (route == null) {
      assert(config.onUnknownRoute != null);
      route = config.onUnknownRoute(settings);
      assert(route != null);
    }
375
    push(route);
376 377
  }

378
  void push(Route<dynamic> route) {
379 380 381 382
    assert(!_debugLocked);
    assert(() { _debugLocked = true; return true; });
    assert(route != null);
    assert(route._navigator == null);
Hixie's avatar
Hixie committed
383
    setState(() {
384
      Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
385
      route._navigator = this;
Hixie's avatar
Hixie committed
386
      route.install(_currentOverlayEntry);
387
      _history.add(route);
388
      route.didPush();
Hixie's avatar
Hixie committed
389
      route.didChangeNext(null);
390
      if (oldRoute != null)
Hixie's avatar
Hixie committed
391
        oldRoute.didChangeNext(route);
392
      config.observer?.didPush(route, oldRoute);
Hixie's avatar
Hixie committed
393
    });
394
    assert(() { _debugLocked = false; return true; });
395
    _cancelActivePointers();
Hixie's avatar
Hixie committed
396 397
  }

398
  void replace({ Route<dynamic> oldRoute, Route<dynamic> newRoute }) {
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
    assert(!_debugLocked);
    assert(oldRoute != null);
    assert(newRoute != null);
    if (oldRoute == newRoute)
      return;
    assert(() { _debugLocked = true; return true; });
    assert(oldRoute._navigator == this);
    assert(newRoute._navigator == null);
    assert(oldRoute.overlayEntries.isNotEmpty);
    assert(newRoute.overlayEntries.isEmpty);
    assert(!overlay.debugIsVisible(oldRoute.overlayEntries.last));
    setState(() {
      int index = _history.indexOf(oldRoute);
      assert(index >= 0);
      newRoute._navigator = this;
Hixie's avatar
Hixie committed
414
      newRoute.install(oldRoute.overlayEntries.last);
415 416
      _history[index] = newRoute;
      newRoute.didReplace(oldRoute);
Hixie's avatar
Hixie committed
417 418 419 420
      if (index + 1 < _history.length)
        newRoute.didChangeNext(_history[index + 1]);
      else
        newRoute.didChangeNext(null);
421
      if (index > 0)
Hixie's avatar
Hixie committed
422
        _history[index - 1].didChangeNext(newRoute);
423 424 425 426
      oldRoute.dispose();
      oldRoute._navigator = null;
    });
    assert(() { _debugLocked = false; return true; });
427
    _cancelActivePointers();
428 429
  }

430
  void replaceRouteBefore({ Route<dynamic> anchorRoute, Route<dynamic> newRoute }) {
431 432 433
    assert(anchorRoute != null);
    assert(anchorRoute._navigator == this);
    assert(_history.indexOf(anchorRoute) > 0);
434
    replace(oldRoute: _history[_history.indexOf(anchorRoute)-1], newRoute: newRoute);
435
  }
436

437
  void removeRouteBefore(Route<dynamic> anchorRoute) {
438 439 440 441 442
    assert(!_debugLocked);
    assert(() { _debugLocked = true; return true; });
    assert(anchorRoute._navigator == this);
    int index = _history.indexOf(anchorRoute) - 1;
    assert(index >= 0);
443
    Route<dynamic> targetRoute = _history[index];
444 445 446 447
    assert(targetRoute._navigator == this);
    assert(targetRoute.overlayEntries.isEmpty || !overlay.debugIsVisible(targetRoute.overlayEntries.last));
    setState(() {
      _history.removeAt(index);
448
      Route<dynamic> newRoute = index < _history.length ? _history[index] : null;
Hixie's avatar
Hixie committed
449 450
      if (index > 0)
        _history[index - 1].didChangeNext(newRoute);
451 452 453 454
      targetRoute.dispose();
      targetRoute._navigator = null;
    });
    assert(() { _debugLocked = false; return true; });
455
    _cancelActivePointers();
456 457
  }

458
  bool pop([dynamic result]) {
459 460
    assert(!_debugLocked);
    assert(() { _debugLocked = true; return true; });
461
    Route<dynamic> route = _history.last;
Hixie's avatar
Hixie committed
462
    assert(route._navigator == this);
Hixie's avatar
Hixie committed
463 464
    bool debugPredictedWouldPop;
    assert(() { debugPredictedWouldPop = !route.willHandlePopInternally; return true; });
465
    if (route.didPop(result ?? route.currentResult)) {
Hixie's avatar
Hixie committed
466
      assert(debugPredictedWouldPop);
Hixie's avatar
Hixie committed
467 468
      if (_history.length > 1) {
        setState(() {
469 470 471
          // We use setState to guarantee that we'll rebuild, since the routes
          // can't do that for themselves, even if they have changed their own
          // state (e.g. ModalScope.isCurrent).
Hixie's avatar
Hixie committed
472
          _history.removeLast();
473 474
          _history.last.didPopNext(route);
          config.observer?.didPop(route, _history.last);
Hixie's avatar
Hixie committed
475 476 477 478 479 480
          route._navigator = null;
        });
      } else {
        assert(() { _debugLocked = false; return true; });
        return false;
      }
Hixie's avatar
Hixie committed
481 482
    } else {
      assert(!debugPredictedWouldPop);
Hixie's avatar
Hixie committed
483
    }
484
    assert(() { _debugLocked = false; return true; });
485
    _cancelActivePointers();
Hixie's avatar
Hixie committed
486
    return true;
Hixie's avatar
Hixie committed
487 488
  }

Hans Muller's avatar
Hans Muller committed
489 490
  void popUntil(RoutePredicate predicate) {
    while (!predicate(_history.last))
491
      pop();
Hixie's avatar
Hixie committed
492 493
  }

494 495 496 497
  /// Whether this navigator can be popped.
  ///
  /// The only route that cannot be popped off the navigator is the initial
  /// route.
Hixie's avatar
Hixie committed
498 499 500 501 502
  bool canPop() {
    assert(_history.length > 0);
    return _history.length > 1 || _history[0].willHandlePopInternally;
  }

503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
  NavigationGestureController startPopGesture() {
    if (canPop())
      return _history.last.startPopGesture(this);
    return null;
  }

  // TODO(mpcomplete): remove this bool when we fix
  // https://github.com/flutter/flutter/issues/5577
  bool _userGestureInProgress = false;
  bool get userGestureInProgress => _userGestureInProgress;

  void didStartUserGesture() {
    _userGestureInProgress = true;
    config.observer?.didStartUserGesture();
  }

  void didStopUserGesture() {
    _userGestureInProgress = false;
    config.observer?.didStopUserGesture();
  }

524
  final Set<int> _activePointers = new Set<int>();
Hixie's avatar
Hixie committed
525

526 527 528 529 530 531 532 533 534 535 536 537
  void _handlePointerDown(PointerDownEvent event) {
    _activePointers.add(event.pointer);
  }

  void _handlePointerUpOrCancel(PointerEvent event) {
    _activePointers.remove(event.pointer);
  }

  void _cancelActivePointers() {
    // This mechanism is far from perfect. See the issue below for more details:
    // https://github.com/flutter/flutter/issues/4770
    RenderAbsorbPointer absorber = _overlayKey.currentContext?.ancestorRenderObjectOfType(const TypeMatcher<RenderAbsorbPointer>());
538 539 540
    setState(() {
      absorber?.absorbing = true;
    });
541 542
    for (int pointer in _activePointers.toList())
      WidgetsBinding.instance.cancelPointer(pointer);
543 544
  }

545 546 547 548
  // TODO(abarth): We should be able to take a focusScopeKey as configuration
  // information in case our parent wants to control whether we are focused.
  final GlobalKey _focusScopeKey = new GlobalKey();

549
  @override
Adam Barth's avatar
Adam Barth committed
550
  Widget build(BuildContext context) {
551
    assert(!_debugLocked);
552
    assert(_history.isNotEmpty);
553
    final Route<dynamic> initialRoute = _history.first;
554 555 556 557 558 559 560 561 562 563 564 565 566 567
    return new Listener(
      onPointerDown: _handlePointerDown,
      onPointerUp: _handlePointerUpOrCancel,
      onPointerCancel: _handlePointerUpOrCancel,
      child: new AbsorbPointer(
        absorbing: false,
        child: new Focus(
          key: _focusScopeKey,
          initiallyFocusedScope: initialRoute.focusKey,
          child: new Overlay(
            key: _overlayKey,
            initialEntries: initialRoute.overlayEntries
          )
        )
568
      )
569 570 571
    );
  }
}