routes.dart 58.1 KB
Newer Older
Adam Barth's avatar
Adam Barth committed
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 6
import 'dart:async';

7
import 'package:flutter/foundation.dart';
8

Adam Barth's avatar
Adam Barth committed
9
import 'basic.dart';
10 11
import 'focus_manager.dart';
import 'focus_scope.dart';
Adam Barth's avatar
Adam Barth committed
12
import 'framework.dart';
13
import 'modal_barrier.dart';
Adam Barth's avatar
Adam Barth committed
14 15
import 'navigator.dart';
import 'overlay.dart';
16
import 'page_storage.dart';
17
import 'transitions.dart';
Adam Barth's avatar
Adam Barth committed
18

19 20 21
// Examples can assume:
// dynamic routeObserver;

22
const Color _kTransparent = Color(0x00000000);
Hixie's avatar
Hixie committed
23

24
/// A route that displays widgets in the [Navigator]'s [Overlay].
Hixie's avatar
Hixie committed
25
abstract class OverlayRoute<T> extends Route<T> {
26 27 28 29 30
  /// Creates a route that knows how to interact with an [Overlay].
  OverlayRoute({
    RouteSettings settings,
  }) : super(settings: settings);

31
  /// Subclasses should override this getter to return the builders for the overlay.
32
  Iterable<OverlayEntry> createOverlayEntries();
Adam Barth's avatar
Adam Barth committed
33

34
  /// The entries this route has placed in the overlay.
35
  @override
Adam Barth's avatar
Adam Barth committed
36
  List<OverlayEntry> get overlayEntries => _overlayEntries;
37
  final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
Adam Barth's avatar
Adam Barth committed
38

39
  @override
Hixie's avatar
Hixie committed
40
  void install(OverlayEntry insertionPoint) {
41
    assert(_overlayEntries.isEmpty);
42
    _overlayEntries.addAll(createOverlayEntries());
Hixie's avatar
Hixie committed
43
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
44
    super.install(insertionPoint);
Adam Barth's avatar
Adam Barth committed
45 46
  }

47
  /// Controls whether [didPop] calls [NavigatorState.finalizeRoute].
48
  ///
49
  /// If true, this route removes its overlay entries during [didPop].
50 51
  /// Subclasses can override this getter if they want to delay finalization
  /// (for example to animate the route's exit before removing it from the
52
  /// overlay).
53 54 55
  ///
  /// Subclasses that return false from [finishedWhenPopped] are responsible for
  /// calling [NavigatorState.finalizeRoute] themselves.
56 57 58
  @protected
  bool get finishedWhenPopped => true;

59
  @override
Hixie's avatar
Hixie committed
60
  bool didPop(T result) {
61 62
    final bool returnValue = super.didPop(result);
    assert(returnValue);
63
    if (finishedWhenPopped)
64 65
      navigator.finalizeRoute(this);
    return returnValue;
Hixie's avatar
Hixie committed
66 67
  }

68 69
  @override
  void dispose() {
Adam Barth's avatar
Adam Barth committed
70 71 72
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
73
    super.dispose();
74
  }
Adam Barth's avatar
Adam Barth committed
75 76
}

77
/// A route with entrance and exit transitions.
Hixie's avatar
Hixie committed
78
abstract class TransitionRoute<T> extends OverlayRoute<T> {
79 80 81 82 83
  /// Creates a route that animates itself when it is pushed or popped.
  TransitionRoute({
    RouteSettings settings,
  }) : super(settings: settings);

Hixie's avatar
Hixie committed
84 85
  /// This future completes only once the transition itself has finished, after
  /// the overlay entries have been removed from the navigator's overlay.
86 87
  ///
  /// This future completes once the animation has been dismissed. That will be
88 89
  /// after [popped], because [popped] typically completes before the animation
  /// even starts, as soon as the route is popped.
90
  Future<T> get completed => _transitionCompleter.future;
91
  final Completer<T> _transitionCompleter = Completer<T>();
92

93
  /// The duration the transition lasts.
Adam Barth's avatar
Adam Barth committed
94
  Duration get transitionDuration;
95 96 97 98 99

  /// Whether the route obscures previous routes when the transition is complete.
  ///
  /// When an opaque route's entrance transition is complete, the routes behind
  /// the opaque route will not be built to save resources.
Adam Barth's avatar
Adam Barth committed
100 101
  bool get opaque;

102
  @override
103
  bool get finishedWhenPopped => _controller.status == AnimationStatus.dismissed;
104

105 106
  /// The animation that drives the route's transition and the previous route's
  /// forward transition.
107 108
  Animation<double> get animation => _animation;
  Animation<double> _animation;
109

110 111 112
  /// The animation controller that the route uses to drive the transitions.
  ///
  /// The animation itself is exposed by the [animation] property.
113 114
  @protected
  AnimationController get controller => _controller;
115
  AnimationController _controller;
116

117 118 119 120 121 122
  /// The animation for the route being pushed on top of this route. This
  /// animation lets this route coordinate with the entrance and exit transition
  /// of route pushed on top of this route.
  Animation<double> get secondaryAnimation => _secondaryAnimation;
  final ProxyAnimation _secondaryAnimation = ProxyAnimation(kAlwaysDismissedAnimation);

123
  /// Called to create the animation controller that will drive the transitions to
124 125
  /// this route from the previous one, and back to the previous route from this
  /// one.
126
  AnimationController createAnimationController() {
127
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
128
    final Duration duration = transitionDuration;
129
    assert(duration != null && duration >= Duration.zero);
130
    return AnimationController(
131 132 133 134
      duration: duration,
      debugLabel: debugLabel,
      vsync: navigator,
    );
Adam Barth's avatar
Adam Barth committed
135 136
  }

137 138
  /// Called to create the animation that exposes the current progress of
  /// the transition controlled by the animation controller created by
139
  /// [createAnimationController()].
140
  Animation<double> createAnimation() {
141
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
142 143
    assert(_controller != null);
    return _controller.view;
144 145
  }

Hixie's avatar
Hixie committed
146
  T _result;
Adam Barth's avatar
Adam Barth committed
147

148
  void _handleStatusChanged(AnimationStatus status) {
Adam Barth's avatar
Adam Barth committed
149
    switch (status) {
150
      case AnimationStatus.completed:
Adam Barth's avatar
Adam Barth committed
151 152 153
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
154 155
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Adam Barth's avatar
Adam Barth committed
156 157 158
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
159
      case AnimationStatus.dismissed:
160
        // We might still be an active route if a subclass is controlling the
161 162
        // the transition and hits the dismissed status. For example, the iOS
        // back gesture drives this animation to the dismissed status before
163 164
        // removing the route and disposing it.
        if (!isActive) {
165 166 167
          navigator.finalizeRoute(this);
          assert(overlayEntries.isEmpty);
        }
Adam Barth's avatar
Adam Barth committed
168 169
        break;
    }
170
    changedInternalState();
Adam Barth's avatar
Adam Barth committed
171 172
  }

173
  @override
Hixie's avatar
Hixie committed
174
  void install(OverlayEntry insertionPoint) {
175
    assert(!_transitionCompleter.isCompleted, 'Cannot install a $runtimeType after disposing it.');
176
    _controller = createAnimationController();
177
    assert(_controller != null, '$runtimeType.createAnimationController() returned null.');
178
    _animation = createAnimation();
179
    assert(_animation != null, '$runtimeType.createAnimation() returned null.');
Hixie's avatar
Hixie committed
180
    super.install(insertionPoint);
181 182
  }

183
  @override
184
  TickerFuture didPush() {
185 186
    assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
187
    _animation.addStatusListener(_handleStatusChanged);
188
    return _controller.forward();
Adam Barth's avatar
Adam Barth committed
189 190
  }

191
  @override
192
  void didReplace(Route<dynamic> oldRoute) {
193 194
    assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
195
    if (oldRoute is TransitionRoute)
196
      _controller.value = oldRoute._controller.value;
197
    _animation.addStatusListener(_handleStatusChanged);
198 199 200
    super.didReplace(oldRoute);
  }

201
  @override
Hixie's avatar
Hixie committed
202
  bool didPop(T result) {
203 204
    assert(_controller != null, '$runtimeType.didPop called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
Adam Barth's avatar
Adam Barth committed
205
    _result = result;
206
    _controller.reverse();
207
    return super.didPop(result);
Hixie's avatar
Hixie committed
208 209
  }

210
  @override
211
  void didPopNext(Route<dynamic> nextRoute) {
212 213
    assert(_controller != null, '$runtimeType.didPopNext called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
214
    _updateSecondaryAnimation(nextRoute);
Hixie's avatar
Hixie committed
215
    super.didPopNext(nextRoute);
Adam Barth's avatar
Adam Barth committed
216 217
  }

218
  @override
219
  void didChangeNext(Route<dynamic> nextRoute) {
220 221
    assert(_controller != null, '$runtimeType.didChangeNext called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
222
    _updateSecondaryAnimation(nextRoute);
Hixie's avatar
Hixie committed
223
    super.didChangeNext(nextRoute);
224 225
  }

226
  void _updateSecondaryAnimation(Route<dynamic> nextRoute) {
227
    if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
228
      final Animation<double> current = _secondaryAnimation.parent;
Hixie's avatar
Hixie committed
229
      if (current != null) {
230
        if (current is TrainHoppingAnimation) {
231
          TrainHoppingAnimation newAnimation;
232
          newAnimation = TrainHoppingAnimation(
Hixie's avatar
Hixie committed
233
            current.currentTrain,
234
            nextRoute._animation,
Hixie's avatar
Hixie committed
235
            onSwitchedTrain: () {
236
              assert(_secondaryAnimation.parent == newAnimation);
237
              assert(newAnimation.currentTrain == nextRoute._animation);
238
              _secondaryAnimation.parent = newAnimation.currentTrain;
239
              newAnimation.dispose();
240
            },
Hixie's avatar
Hixie committed
241
          );
242
          _secondaryAnimation.parent = newAnimation;
Hixie's avatar
Hixie committed
243 244
          current.dispose();
        } else {
245
          _secondaryAnimation.parent = TrainHoppingAnimation(current, nextRoute._animation);
Hixie's avatar
Hixie committed
246 247
        }
      } else {
248
        _secondaryAnimation.parent = nextRoute._animation;
Hixie's avatar
Hixie committed
249
      }
Hixie's avatar
Hixie committed
250
    } else {
251
      _secondaryAnimation.parent = kAlwaysDismissedAnimation;
Hixie's avatar
Hixie committed
252 253 254
    }
  }

255 256 257
  /// Returns true if this route supports a transition animation that runs
  /// when [nextRoute] is pushed on top of it or when [nextRoute] is popped
  /// off of it.
258
  ///
259
  /// Subclasses can override this method to restrict the set of routes they
260
  /// need to coordinate transitions with.
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
  ///
  /// If true, and `nextRoute.canTransitionFrom()` is true, then the
  /// [buildTransitions] `secondaryAnimation` will run from 0.0 - 1.0
  /// when [nextRoute] is pushed on top of this one.  Similarly, if
  /// the [nextRoute] is popped off of this route, the
  /// `secondaryAnimation` will run from 1.0 - 0.0.
  ///
  /// If false, this route's [buildTransitions] `secondaryAnimation` parameter
  /// value will be [kAlwaysDismissedAnimation]. In other words, this route
  /// will not animate when when [nextRoute] is pushed on top of it or when
  /// [nextRoute] is popped off of it.
  ///
  /// Returns true by default.
  ///
  /// See also:
  ///
  ///  * [canTransitionFrom], which must be true for [nextRoute] for the
  ///    [buildTransitions] `secondaryAnimation` to run.
279
  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true;
280

281 282
  /// Returns true if [previousRoute] should animate when this route
  /// is pushed on top of it or when then this route is popped off of it.
283
  ///
284
  /// Subclasses can override this method to restrict the set of routes they
285
  /// need to coordinate transitions with.
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
  ///
  /// If true, and `previousRoute.canTransitionTo()` is true, then the
  /// previous route's [buildTransitions] `secondaryAnimation` will
  /// run from 0.0 - 1.0 when this route is pushed on top of
  /// it. Similarly, if this route is popped off of [previousRoute]
  /// the previous route's `secondaryAnimation` will run from 1.0 - 0.0.
  ///
  /// If false, then the previous route's [buildTransitions]
  /// `secondaryAnimation` value will be kAlwaysDismissedAnimation. In
  /// other words [previousRoute] will not animate when this route is
  /// pushed on top of it or when then this route is popped off of it.
  ///
  /// Returns true by default.
  ///
  /// See also:
  ///
  ///  * [canTransitionTo], which must be true for [previousRoute] for its
  ///    [buildTransitions] `secondaryAnimation` to run.
304
  bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => true;
305

306
  @override
Hixie's avatar
Hixie committed
307
  void dispose() {
308 309
    assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.');
    _controller?.dispose();
310
    _transitionCompleter.complete(_result);
Hixie's avatar
Hixie committed
311 312 313
    super.dispose();
  }

314
  /// A short description of this route useful for debugging.
Adam Barth's avatar
Adam Barth committed
315
  String get debugLabel => '$runtimeType';
316 317

  @override
318
  String toString() => '$runtimeType(animation: $_controller)';
Adam Barth's avatar
Adam Barth committed
319
}
320

321
/// An entry in the history of a [LocalHistoryRoute].
Hixie's avatar
Hixie committed
322
class LocalHistoryEntry {
323
  /// Creates an entry in the history of a [LocalHistoryRoute].
Hixie's avatar
Hixie committed
324
  LocalHistoryEntry({ this.onRemove });
325 326

  /// Called when this entry is removed from the history of its associated [LocalHistoryRoute].
Hixie's avatar
Hixie committed
327
  final VoidCallback onRemove;
328

329
  LocalHistoryRoute<dynamic> _owner;
330 331

  /// Remove this entry from the history of its associated [LocalHistoryRoute].
Hixie's avatar
Hixie committed
332 333
  void remove() {
    _owner.removeLocalHistoryEntry(this);
334
    assert(_owner == null);
Hixie's avatar
Hixie committed
335
  }
336

Hixie's avatar
Hixie committed
337 338 339 340 341 342
  void _notifyRemoved() {
    if (onRemove != null)
      onRemove();
  }
}

343
/// A mixin used by routes to handle back navigations internally by popping a list.
344 345
///
/// When a [Navigator] is instructed to pop, the current route is given an
346
/// opportunity to handle the pop internally. A `LocalHistoryRoute` handles the
347 348
/// pop internally if its list of local history entries is non-empty. Rather
/// than being removed as the current route, the most recent [LocalHistoryEntry]
349
/// is removed from the list and its [LocalHistoryEntry.onRemove] is called.
350
mixin LocalHistoryRoute<T> on Route<T> {
Hixie's avatar
Hixie committed
351
  List<LocalHistoryEntry> _localHistory;
352 353 354

  /// Adds a local history entry to this route.
  ///
355
  /// When asked to pop, if this route has any local history entries, this route
356 357 358 359 360
  /// will handle the pop internally by removing the most recently added local
  /// history entry.
  ///
  /// The given local history entry must not already be part of another local
  /// history route.
361
  ///
362
  /// {@tool sample}
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
  ///
  /// The following example is an app with 2 pages: `HomePage` and `SecondPage`.
  /// The `HomePage` can navigate to the `SecondPage`.
  ///
  /// The `SecondPage` uses a [LocalHistoryEntry] to implement local navigation
  /// within that page. Pressing 'show rectangle' displays a red rectangle and
  /// adds a local history entry. At that point, pressing the '< back' button
  /// pops the latest route, which is the local history entry, and the red
  /// rectangle disappears. Pressing the '< back' button a second time
  /// once again pops the latest route, which is the `SecondPage`, itself.
  /// Therefore, the second press navigates back to the `HomePage`.
  ///
  /// ```dart
  /// class App extends StatelessWidget {
  ///   @override
  ///   Widget build(BuildContext context) {
379
  ///     return MaterialApp(
380 381
  ///       initialRoute: '/',
  ///       routes: {
382 383
  ///         '/': (BuildContext context) => HomePage(),
  ///         '/second_page': (BuildContext context) => SecondPage(),
384 385 386 387 388 389 390 391 392
  ///       },
  ///     );
  ///   }
  /// }
  ///
  /// class HomePage extends StatefulWidget {
  ///   HomePage();
  ///
  ///   @override
393
  ///   _HomePageState createState() => _HomePageState();
394 395 396 397 398
  /// }
  ///
  /// class _HomePageState extends State<HomePage> {
  ///   @override
  ///   Widget build(BuildContext context) {
399 400
  ///     return Scaffold(
  ///       body: Center(
401 402 403
  ///         child: Column(
  ///           mainAxisSize: MainAxisSize.min,
  ///           children: <Widget>[
404
  ///             Text('HomePage'),
405
  ///             // Press this button to open the SecondPage.
406 407
  ///             RaisedButton(
  ///               child: Text('Second Page >'),
408 409 410 411 412 413 414 415 416 417 418 419 420
  ///               onPressed: () {
  ///                 Navigator.pushNamed(context, '/second_page');
  ///               },
  ///             ),
  ///           ],
  ///         ),
  ///       ),
  ///     );
  ///   }
  /// }
  ///
  /// class SecondPage extends StatefulWidget {
  ///   @override
421
  ///   _SecondPageState createState() => _SecondPageState();
422 423 424 425 426 427 428 429 430 431 432 433
  /// }
  ///
  /// class _SecondPageState extends State<SecondPage> {
  ///
  ///   bool _showRectangle = false;
  ///
  ///   void _navigateLocallyToShowRectangle() async {
  ///     // This local history entry essentially represents the display of the red
  ///     // rectangle. When this local history entry is removed, we hide the red
  ///     // rectangle.
  ///     setState(() => _showRectangle = true);
  ///     ModalRoute.of(context).addLocalHistoryEntry(
434
  ///         LocalHistoryEntry(
435 436 437 438 439 440 441 442 443 444 445
  ///             onRemove: () {
  ///               // Hide the red rectangle.
  ///               setState(() => _showRectangle = false);
  ///             }
  ///         )
  ///     );
  ///   }
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
  ///     final localNavContent = _showRectangle
446
  ///       ? Container(
447 448 449 450
  ///           width: 100.0,
  ///           height: 100.0,
  ///           color: Colors.red,
  ///         )
451 452
  ///       : RaisedButton(
  ///           child: Text('Show Rectangle'),
453 454 455
  ///           onPressed: _navigateLocallyToShowRectangle,
  ///         );
  ///
456
  ///     return Scaffold(
457
  ///       body: Center(
458
  ///         child: Column(
459 460 461
  ///           mainAxisAlignment: MainAxisAlignment.center,
  ///           children: <Widget>[
  ///             localNavContent,
462 463
  ///             RaisedButton(
  ///               child: Text('< Back'),
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478
  ///               onPressed: () {
  ///                 // Pop a route. If this is pressed while the red rectangle is
  ///                 // visible then it will will pop our local history entry, which
  ///                 // will hide the red rectangle. Otherwise, the SecondPage will
  ///                 // navigate back to the HomePage.
  ///                 Navigator.of(context).pop();
  ///               },
  ///             ),
  ///           ],
  ///         ),
  ///       ),
  ///     );
  ///   }
  /// }
  /// ```
479
  /// {@end-tool}
Hixie's avatar
Hixie committed
480 481 482 483
  void addLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry._owner == null);
    entry._owner = this;
    _localHistory ??= <LocalHistoryEntry>[];
484
    final bool wasEmpty = _localHistory.isEmpty;
Hixie's avatar
Hixie committed
485
    _localHistory.add(entry);
486 487
    if (wasEmpty)
      changedInternalState();
Hixie's avatar
Hixie committed
488
  }
489 490 491

  /// Remove a local history entry from this route.
  ///
492 493
  /// The entry's [LocalHistoryEntry.onRemove] callback, if any, will be called
  /// synchronously.
Hixie's avatar
Hixie committed
494 495 496 497 498 499 500
  void removeLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry != null);
    assert(entry._owner == this);
    assert(_localHistory.contains(entry));
    _localHistory.remove(entry);
    entry._owner = null;
    entry._notifyRemoved();
501 502
    if (_localHistory.isEmpty)
      changedInternalState();
Hixie's avatar
Hixie committed
503
  }
504

505
  @override
506 507 508 509
  Future<RoutePopDisposition> willPop() async {
    if (willHandlePopInternally)
      return RoutePopDisposition.pop;
    return await super.willPop();
510 511
  }

512
  @override
Hixie's avatar
Hixie committed
513
  bool didPop(T result) {
514
    if (_localHistory != null && _localHistory.isNotEmpty) {
515
      final LocalHistoryEntry entry = _localHistory.removeLast();
Hixie's avatar
Hixie committed
516 517 518
      assert(entry._owner == this);
      entry._owner = null;
      entry._notifyRemoved();
519 520
      if (_localHistory.isEmpty)
        changedInternalState();
Hixie's avatar
Hixie committed
521 522 523 524
      return false;
    }
    return super.didPop(result);
  }
525 526

  @override
Hixie's avatar
Hixie committed
527
  bool get willHandlePopInternally {
528
    return _localHistory != null && _localHistory.isNotEmpty;
Hixie's avatar
Hixie committed
529
  }
Hixie's avatar
Hixie committed
530 531
}

Hixie's avatar
Hixie committed
532
class _ModalScopeStatus extends InheritedWidget {
533
  const _ModalScopeStatus({
Hixie's avatar
Hixie committed
534
    Key key,
535 536 537
    @required this.isCurrent,
    @required this.canPop,
    @required this.route,
538
    @required Widget child,
539 540 541 542 543
  }) : assert(isCurrent != null),
       assert(canPop != null),
       assert(route != null),
       assert(child != null),
       super(key: key, child: child);
Hixie's avatar
Hixie committed
544

545
  final bool isCurrent;
546
  final bool canPop;
547
  final Route<dynamic> route;
Hixie's avatar
Hixie committed
548

549
  @override
Hixie's avatar
Hixie committed
550
  bool updateShouldNotify(_ModalScopeStatus old) {
551
    return isCurrent != old.isCurrent ||
552
           canPop != old.canPop ||
Hixie's avatar
Hixie committed
553 554 555
           route != old.route;
  }

556
  @override
557
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
558
    super.debugFillProperties(description);
559 560
    description.add(FlagProperty('isCurrent', value: isCurrent, ifTrue: 'active', ifFalse: 'inactive'));
    description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop'));
Hixie's avatar
Hixie committed
561 562 563
  }
}

564
class _ModalScope<T> extends StatefulWidget {
565
  const _ModalScope({
566
    Key key,
567
    this.route,
568
  }) : super(key: key);
569

570
  final ModalRoute<T> route;
571

572
  @override
573
  _ModalScopeState<T> createState() => _ModalScopeState<T>();
574 575
}

576 577 578 579 580 581 582 583 584
class _ModalScopeState<T> extends State<_ModalScope<T>> {
  // We cache the result of calling the route's buildPage, and clear the cache
  // whenever the dependencies change. This implements the contract described in
  // the documentation for buildPage, namely that it gets called once, unless
  // something like a ModalRoute.of() dependency triggers an update.
  Widget _page;

  // This is the combination of the two animations for the route.
  Listenable _listenable;
585

586 587 588
  /// The node this scope will use for its root [FocusScope] widget.
  final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope');

589
  @override
590 591
  void initState() {
    super.initState();
592 593 594 595
    final List<Listenable> animations = <Listenable>[
      if (widget.route.animation != null) widget.route.animation,
      if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation,
    ];
596
    _listenable = Listenable.merge(animations);
597 598 599
    if (widget.route.isCurrent) {
      widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
    }
600 601
  }

602
  @override
603
  void didUpdateWidget(_ModalScope<T> oldWidget) {
604
    super.didUpdateWidget(oldWidget);
605
    assert(widget.route == oldWidget.route);
606 607 608
    if (widget.route.isCurrent) {
      widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
    }
609 610
  }

611
  @override
612 613 614
  void didChangeDependencies() {
    super.didChangeDependencies();
    _page = null;
615 616
  }

617
  void _forceRebuildPage() {
618
    setState(() {
619
      _page = null;
620 621 622
    });
  }

623 624 625 626 627 628
  @override
  void dispose() {
    focusScopeNode.dispose();
    super.dispose();
  }

629 630
  // This should be called to wrap any changes to route.isCurrent, route.canPop,
  // and route.offstage.
631 632
  void _routeSetState(VoidCallback fn) {
    setState(fn);
633 634
  }

635
  @override
636
  Widget build(BuildContext context) {
637
    return _ModalScopeStatus(
638 639 640
      route: widget.route,
      isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
      canPop: widget.route.canPop, // _routeSetState is called if this updates
641
      child: Offstage(
642
        offstage: widget.route.offstage, // _routeSetState is called if this updates
643
        child: PageStorage(
644
          bucket: widget.route._storageBucket, // immutable
645
          child: FocusScope(
646
            node: focusScopeNode, // immutable
647 648
            child: RepaintBoundary(
              child: AnimatedBuilder(
649 650 651 652 653 654
                animation: _listenable, // immutable
                builder: (BuildContext context, Widget child) {
                  return widget.route.buildTransitions(
                    context,
                    widget.route.animation,
                    widget.route.secondaryAnimation,
655 656 657 658 659 660 661 662 663 664 665 666
                    // This additional AnimatedBuilder is include because if the
                    // value of the userGestureInProgressNotifier changes, it's
                    // only necessary to rebuild the IgnorePointer widget.
                    AnimatedBuilder(
                      animation: widget.route.navigator.userGestureInProgressNotifier,
                      builder: (BuildContext context, Widget child) {
                        return IgnorePointer(
                          ignoring: widget.route.navigator.userGestureInProgress
                            || widget.route.animation?.status == AnimationStatus.reverse,
                          child: child,
                        );
                      },
667 668 669 670
                      child: child,
                    ),
                  );
                },
671
                child: _page ??= RepaintBoundary(
672
                  key: widget.route._subtreeKey, // immutable
673
                  child: Builder(
674 675 676 677 678 679 680
                    builder: (BuildContext context) {
                      return widget.route.buildPage(
                        context,
                        widget.route.animation,
                        widget.route.secondaryAnimation,
                      );
                    },
681
                  ),
682 683 684 685 686 687
                ),
              ),
            ),
          ),
        ),
      ),
688
    );
689 690 691
  }
}

692
/// A route that blocks interaction with previous routes.
693
///
694 695 696 697 698 699
/// [ModalRoute]s cover the entire [Navigator]. They are not necessarily
/// [opaque], however; for example, a pop-up menu uses a [ModalRoute] but only
/// shows the menu in a small box overlapping the previous route.
///
/// The `T` type argument is the return value of the route. If there is no
/// return value, consider using `void` as the return value.
700
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
701
  /// Creates a route that blocks interaction with previous routes.
702
  ModalRoute({
703
    RouteSettings settings,
704
  }) : super(settings: settings);
705

Hixie's avatar
Hixie committed
706 707
  // The API for general users of this class

708 709
  /// Returns the modal route most closely associated with the given context.
  ///
710
  /// Returns null if the given context is not associated with a modal route.
711 712 713 714
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
715
  /// ModalRoute route = ModalRoute.of(context);
716
  /// ```
717 718
  ///
  /// The given [BuildContext] will be rebuilt if the state of the route changes
719
  /// (specifically, if [isCurrent] or [canPop] change value).
720
  @optionalTypeArgs
721
  static ModalRoute<T> of<T extends Object>(BuildContext context) {
722
    final _ModalScopeStatus widget = context.inheritFromWidgetOfExactType(_ModalScopeStatus);
Hixie's avatar
Hixie committed
723 724 725
    return widget?.route;
  }

726 727
  /// Schedule a call to [buildTransitions].
  ///
728
  /// Whenever you need to change internal state for a [ModalRoute] object, make
729
  /// the change in a function that you pass to [setState], as in:
730 731 732 733 734
  ///
  /// ```dart
  /// setState(() { myState = newValue });
  /// ```
  ///
735
  /// If you just change the state directly without calling [setState], then the
736 737 738 739 740 741 742 743 744 745 746 747 748 749
  /// route will not be scheduled for rebuilding, meaning that its rendering
  /// will not be updated.
  @protected
  void setState(VoidCallback fn) {
    if (_scopeKey.currentState != null) {
      _scopeKey.currentState._routeSetState(fn);
    } else {
      // The route isn't currently visible, so we don't have to call its setState
      // method, but we do still need to call the fn callback, otherwise the state
      // in the route won't be updated!
      fn();
    }
  }

Hans Muller's avatar
Hans Muller committed
750 751 752 753 754 755 756 757
  /// Returns a predicate that's true if the route has the specified name and if
  /// popping the route will not yield the same route, i.e. if the route's
  /// [willHandlePopInternally] property is false.
  ///
  /// This function is typically used with [Navigator.popUntil()].
  static RoutePredicate withName(String name) {
    return (Route<dynamic> route) {
      return !route.willHandlePopInternally
758 759
          && route is ModalRoute
          && route.settings.name == name;
Hans Muller's avatar
Hans Muller committed
760 761
    };
  }
762 763 764

  // The API for subclasses to override - used by _ModalScope

765
  /// Override this method to build the primary content of this route.
766
  ///
767 768 769 770 771 772 773 774 775 776
  /// The arguments have the following meanings:
  ///
  ///  * `context`: The context in which the route is being built.
  ///  * [animation]: The animation for this route's transition. When entering,
  ///    the animation runs forward from 0.0 to 1.0. When exiting, this animation
  ///    runs backwards from 1.0 to 0.0.
  ///  * [secondaryAnimation]: The animation for the route being pushed on top of
  ///    this route. This animation lets this route coordinate with the entrance
  ///    and exit transition of routes pushed on top of this route.
  ///
777 778 779 780 781 782 783 784
  /// This method is only called when the route is first built, and rarely
  /// thereafter. In particular, it is not automatically called again when the
  /// route's state changes unless it uses [ModalRoute.of]. For a builder that
  /// is called every time the route's state changes, consider
  /// [buildTransitions]. For widgets that change their behavior when the
  /// route's state changes, consider [ModalRoute.of] to obtain a reference to
  /// the route; this will cause the widget to be rebuilt each time the route
  /// changes state.
785 786 787 788 789
  ///
  /// In general, [buildPage] should be used to build the page contents, and
  /// [buildTransitions] for the widgets that change as the page is brought in
  /// and out of view. Avoid using [buildTransitions] for content that never
  /// changes; building such content once from [buildPage] is more efficient.
790
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
791

792 793
  /// Override this method to wrap the [child] with one or more transition
  /// widgets that define how the route arrives on and leaves the screen.
794
  ///
795 796 797 798 799
  /// By default, the child (which contains the widget returned by [buildPage])
  /// is not wrapped in any transition widgets.
  ///
  /// The [buildTransitions] method, in contrast to [buildPage], is called each
  /// time the [Route]'s state changes (e.g. the value of [canPop]).
800
  ///
801
  /// The [buildTransitions] method is typically used to define transitions
802 803 804 805 806 807 808 809 810 811 812 813 814
  /// that animate the new topmost route's comings and goings. When the
  /// [Navigator] pushes a route on the top of its stack, the new route's
  /// primary [animation] runs from 0.0 to 1.0. When the Navigator pops the
  /// topmost route, e.g. because the use pressed the back button, the
  /// primary animation runs from 1.0 to 0.0.
  ///
  /// The following example uses the primary animation to drive a
  /// [SlideTransition] that translates the top of the new route vertically
  /// from the bottom of the screen when it is pushed on the Navigator's
  /// stack. When the route is popped the SlideTransition translates the
  /// route from the top of the screen back to the bottom.
  ///
  /// ```dart
815
  /// PageRouteBuilder(
816 817 818 819 820
  ///   pageBuilder: (BuildContext context,
  ///       Animation<double> animation,
  ///       Animation<double> secondaryAnimation,
  ///       Widget child,
  ///   ) {
821 822 823 824
  ///     return Scaffold(
  ///       appBar: AppBar(title: Text('Hello')),
  ///       body: Center(
  ///         child: Text('Hello World'),
825 826 827 828 829 830 831 832 833
  ///       ),
  ///     );
  ///   },
  ///   transitionsBuilder: (
  ///       BuildContext context,
  ///       Animation<double> animation,
  ///       Animation<double> secondaryAnimation,
  ///       Widget child,
  ///    ) {
834 835
  ///     return SlideTransition(
  ///       position: Tween<Offset>(
836 837
  ///         begin: const Offset(0.0, 1.0),
  ///         end: Offset.zero,
838 839 840 841 842
  ///       ).animate(animation),
  ///       child: child, // child is the value returned by pageBuilder
  ///     );
  ///   },
  /// );
843
  /// ```
844
  ///
845 846
  /// We've used [PageRouteBuilder] to demonstrate the [buildTransitions] method
  /// here. The body of an override of the [buildTransitions] method would be
847 848
  /// defined in the same way.
  ///
849
  /// When the [Navigator] pushes a route on the top of its stack, the
850 851 852 853 854
  /// [secondaryAnimation] can be used to define how the route that was on
  /// the top of the stack leaves the screen. Similarly when the topmost route
  /// is popped, the secondaryAnimation can be used to define how the route
  /// below it reappears on the screen. When the Navigator pushes a new route
  /// on the top of its stack, the old topmost route's secondaryAnimation
855
  /// runs from 0.0 to 1.0. When the Navigator pops the topmost route, the
856 857 858
  /// secondaryAnimation for the route below it runs from 1.0 to 0.0.
  ///
  /// The example below adds a transition that's driven by the
859
  /// [secondaryAnimation]. When this route disappears because a new route has
860 861 862 863 864 865 866 867 868 869 870
  /// been pushed on top of it, it translates in the opposite direction of
  /// the new route. Likewise when the route is exposed because the topmost
  /// route has been popped off.
  ///
  /// ```dart
  ///   transitionsBuilder: (
  ///       BuildContext context,
  ///       Animation<double> animation,
  ///       Animation<double> secondaryAnimation,
  ///       Widget child,
  ///   ) {
871 872
  ///     return SlideTransition(
  ///       position: AlignmentTween(
873 874
  ///         begin: const Offset(0.0, 1.0),
  ///         end: Offset.zero,
875
  ///       ).animate(animation),
876 877
  ///       child: SlideTransition(
  ///         position: TweenOffset(
878 879
  ///           begin: Offset.zero,
  ///           end: const Offset(0.0, 1.0),
880 881 882 883 884
  ///         ).animate(secondaryAnimation),
  ///         child: child,
  ///       ),
  ///     );
  ///   }
885 886 887
  /// ```
  ///
  /// In practice the `secondaryAnimation` is used pretty rarely.
888
  ///
889
  /// The arguments to this method are as follows:
890
  ///
891 892 893
  ///  * `context`: The context in which the route is being built.
  ///  * [animation]: When the [Navigator] pushes a route on the top of its stack,
  ///    the new route's primary [animation] runs from 0.0 to 1.0. When the [Navigator]
894
  ///    pops the topmost route this animation runs from 1.0 to 0.0.
895 896
  ///  * [secondaryAnimation]: When the Navigator pushes a new route
  ///    on the top of its stack, the old topmost route's [secondaryAnimation]
897
  ///    runs from 0.0 to 1.0. When the [Navigator] pops the topmost route, the
898
  ///    [secondaryAnimation] for the route below it runs from 1.0 to 0.0.
899
  ///  * `child`, the page contents, as returned by [buildPage].
900 901 902 903 904
  ///
  /// See also:
  ///
  ///  * [buildPage], which is used to describe the actual contents of the page,
  ///    and whose result is passed to the `child` argument of this method.
905
  Widget buildTransitions(
906 907 908 909
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
910
  ) {
911 912 913
    return child;
  }

914 915 916
  @override
  void install(OverlayEntry insertionPoint) {
    super.install(insertionPoint);
917 918
    _animationProxy = ProxyAnimation(super.animation);
    _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
919 920
  }

921
  @override
922
  TickerFuture didPush() {
923 924 925
    if (_scopeKey.currentState != null) {
      navigator.focusScopeNode.setFirstFocus(_scopeKey.currentState.focusScopeNode);
    }
926
    return super.didPush();
927 928
  }

929 930
  // The API for subclasses to override - used by this class

Hixie's avatar
Hixie committed
931
  /// Whether you can dismiss this route by tapping the modal barrier.
932 933 934 935 936 937 938 939 940 941 942 943 944
  ///
  /// The modal barrier is the scrim that is rendered behind each route, which
  /// generally prevents the user from interacting with the route below the
  /// current route, and normally partially obscures such routes.
  ///
  /// For example, when a dialog is on the screen, the page below the dialog is
  /// usually darkened by the modal barrier.
  ///
  /// If [barrierDismissible] is true, then tapping this barrier will cause the
  /// current route to be popped (see [Navigator.pop]) with null as the value.
  ///
  /// If [barrierDismissible] is false, then tapping the barrier has no effect.
  ///
945 946 947 948
  /// If this getter would ever start returning a different color,
  /// [changedInternalState] should be invoked so that the change can take
  /// effect.
  ///
949 950 951 952
  /// See also:
  ///
  ///  * [barrierColor], which controls the color of the scrim for this route.
  ///  * [ModalBarrier], the widget that implements this feature.
953
  bool get barrierDismissible;
954

955 956 957 958 959 960 961 962 963 964 965
  /// Whether the semantics of the modal barrier are included in the
  /// semantics tree.
  ///
  /// The modal barrier is the scrim that is rendered behind each route, which
  /// generally prevents the user from interacting with the route below the
  /// current route, and normally partially obscures such routes.
  ///
  /// If [semanticsDismissible] is true, then modal barrier semantics are
  /// included in the semantics tree.
  ///
  /// If [semanticsDismissible] is false, then modal barrier semantics are
966
  /// excluded from the semantics tree and tapping on the modal barrier
967 968 969
  /// has no effect.
  bool get semanticsDismissible => true;

Hixie's avatar
Hixie committed
970 971
  /// The color to use for the modal barrier. If this is null, the barrier will
  /// be transparent.
972
  ///
973 974 975 976 977 978 979
  /// The modal barrier is the scrim that is rendered behind each route, which
  /// generally prevents the user from interacting with the route below the
  /// current route, and normally partially obscures such routes.
  ///
  /// For example, when a dialog is on the screen, the page below the dialog is
  /// usually darkened by the modal barrier.
  ///
980 981
  /// The color is ignored, and the barrier made invisible, when [offstage] is
  /// true.
982 983 984 985
  ///
  /// While the route is animating into position, the color is animated from
  /// transparent to the specified color.
  ///
986 987 988 989
  /// If this getter would ever start returning a different color,
  /// [changedInternalState] should be invoked so that the change can take
  /// effect.
  ///
990 991 992 993 994
  /// See also:
  ///
  ///  * [barrierDismissible], which controls the behavior of the barrier when
  ///    tapped.
  ///  * [ModalBarrier], the widget that implements this feature.
Hixie's avatar
Hixie committed
995
  Color get barrierColor;
996

997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
  /// The semantic label used for a dismissible barrier.
  ///
  /// If the barrier is dismissible, this label will be read out if
  /// accessibility tools (like VoiceOver on iOS) focus on the barrier.
  ///
  /// The modal barrier is the scrim that is rendered behind each route, which
  /// generally prevents the user from interacting with the route below the
  /// current route, and normally partially obscures such routes.
  ///
  /// For example, when a dialog is on the screen, the page below the dialog is
  /// usually darkened by the modal barrier.
  ///
1009 1010 1011 1012
  /// If this getter would ever start returning a different color,
  /// [changedInternalState] should be invoked so that the change can take
  /// effect.
  ///
1013 1014 1015 1016 1017 1018 1019
  /// See also:
  ///
  ///  * [barrierDismissible], which controls the behavior of the barrier when
  ///    tapped.
  ///  * [ModalBarrier], the widget that implements this feature.
  String get barrierLabel;

1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030
  /// Whether the route should remain in memory when it is inactive.
  ///
  /// If this is true, then the route is maintained, so that any futures it is
  /// holding from the next route will properly resolve when the next route
  /// pops. If this is not necessary, this can be set to false to allow the
  /// framework to entirely discard the route's widget hierarchy when it is not
  /// visible.
  ///
  /// The value of this getter should not change during the lifetime of the
  /// object. It is used by [createOverlayEntries], which is called by
  /// [install] near the beginning of the route lifecycle.
1031 1032
  bool get maintainState;

1033 1034 1035

  // The API for _ModalScope and HeroController

1036 1037 1038 1039 1040 1041 1042
  /// Whether this route is currently offstage.
  ///
  /// On the first frame of a route's entrance transition, the route is built
  /// [Offstage] using an animation progress of 1.0. The route is invisible and
  /// non-interactive, but each widget has its final size and position. This
  /// mechanism lets the [HeroController] determine the final local of any hero
  /// widgets being animated as part of the transition.
1043 1044 1045
  ///
  /// The modal barrier, if any, is not rendered if [offstage] is true (see
  /// [barrierColor]).
1046 1047
  bool get offstage => _offstage;
  bool _offstage = false;
1048
  set offstage(bool value) {
1049 1050
    if (_offstage == value)
      return;
1051 1052 1053
    setState(() {
      _offstage = value;
    });
1054
    _animationProxy.parent = _offstage ? kAlwaysCompleteAnimation : super.animation;
1055
    _secondaryAnimationProxy.parent = _offstage ? kAlwaysDismissedAnimation : super.secondaryAnimation;
1056 1057
  }

1058
  /// The build context for the subtree containing the primary content of this route.
1059 1060
  BuildContext get subtreeContext => _subtreeKey.currentContext;

1061 1062 1063 1064 1065
  @override
  Animation<double> get animation => _animationProxy;
  ProxyAnimation _animationProxy;

  @override
1066 1067
  Animation<double> get secondaryAnimation => _secondaryAnimationProxy;
  ProxyAnimation _secondaryAnimationProxy;
1068

1069 1070
  final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];

Ian Hickson's avatar
Ian Hickson committed
1071 1072 1073
  /// Returns the value of the first callback added with
  /// [addScopedWillPopCallback] that returns false. If they all return true,
  /// returns the inherited method's result (see [Route.willPop]).
1074 1075 1076 1077 1078 1079 1080 1081
  ///
  /// Typically this method is not overridden because applications usually
  /// don't create modal routes directly, they use higher level primitives
  /// like [showDialog]. The scoped [WillPopCallback] list makes it possible
  /// for ModalRoute descendants to collectively define the value of `willPop`.
  ///
  /// See also:
  ///
1082 1083 1084 1085 1086
  ///  * [Form], which provides an `onWillPop` callback that uses this mechanism.
  ///  * [addScopedWillPopCallback], which adds a callback to the list this
  ///    method checks.
  ///  * [removeScopedWillPopCallback], which removes a callback from the list
  ///    this method checks.
1087
  @override
1088
  Future<RoutePopDisposition> willPop() async {
1089
    final _ModalScopeState<T> scope = _scopeKey.currentState;
1090
    assert(scope != null);
1091
    for (WillPopCallback callback in List<WillPopCallback>.from(_willPopCallbacks)) {
1092
      if (!await callback())
1093
        return RoutePopDisposition.doNotPop;
1094
    }
1095
    return await super.willPop();
1096 1097 1098 1099
  }

  /// Enables this route to veto attempts by the user to dismiss it.
  ///
1100 1101 1102 1103 1104 1105
  /// This callback is typically added using a [WillPopScope] widget. That
  /// widget finds the enclosing [ModalRoute] and uses this function to register
  /// this callback:
  ///
  /// ```dart
  /// Widget build(BuildContext context) {
1106
  ///   return WillPopScope(
1107 1108 1109 1110 1111 1112 1113
  ///     onWillPop: askTheUserIfTheyAreSure,
  ///     child: ...,
  ///   );
  /// }
  /// ```
  ///
  /// This callback runs asynchronously and it's possible that it will be called
1114
  /// after its route has been disposed. The callback should check [State.mounted]
1115 1116 1117 1118 1119 1120 1121
  /// before doing anything.
  ///
  /// A typical application of this callback would be to warn the user about
  /// unsaved [Form] data if the user attempts to back out of the form. In that
  /// case, use the [Form.onWillPop] property to register the callback.
  ///
  /// To register a callback manually, look up the enclosing [ModalRoute] in a
1122
  /// [State.didChangeDependencies] callback:
1123 1124
  ///
  /// ```dart
1125 1126
  /// ModalRoute<dynamic> _route;
  ///
1127
  /// @override
1128 1129
  /// void didChangeDependencies() {
  ///  super.didChangeDependencies();
1130 1131 1132
  ///  _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure);
  ///  _route = ModalRoute.of(context);
  ///  _route?.addScopedWillPopCallback(askTheUserIfTheyAreSure);
1133 1134 1135
  /// }
  /// ```
  ///
1136 1137 1138 1139
  /// If you register a callback manually, be sure to remove the callback with
  /// [removeScopedWillPopCallback] by the time the widget has been disposed. A
  /// stateful widget can do this in its dispose method (continuing the previous
  /// example):
1140 1141 1142 1143 1144
  ///
  /// ```dart
  /// @override
  /// void dispose() {
  ///   _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure);
1145
  ///   _route = null;
1146 1147 1148 1149
  ///   super.dispose();
  /// }
  /// ```
  ///
1150 1151
  /// See also:
  ///
1152 1153 1154 1155 1156 1157
  ///  * [WillPopScope], which manages the registration and unregistration
  ///    process automatically.
  ///  * [Form], which provides an `onWillPop` callback that uses this mechanism.
  ///  * [willPop], which runs the callbacks added with this method.
  ///  * [removeScopedWillPopCallback], which removes a callback from the list
  ///    that [willPop] checks.
1158
  void addScopedWillPopCallback(WillPopCallback callback) {
1159 1160
    assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
    _willPopCallbacks.add(callback);
1161 1162 1163 1164 1165 1166
  }

  /// Remove one of the callbacks run by [willPop].
  ///
  /// See also:
  ///
1167 1168 1169
  ///  * [Form], which provides an `onWillPop` callback that uses this mechanism.
  ///  * [addScopedWillPopCallback], which adds callback to the list
  ///    checked by [willPop].
1170
  void removeScopedWillPopCallback(WillPopCallback callback) {
1171 1172
    assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.');
    _willPopCallbacks.remove(callback);
1173 1174
  }

1175 1176 1177 1178 1179 1180
  /// True if one or more [WillPopCallback] callbacks exist.
  ///
  /// This method is used to disable the horizontal swipe pop gesture
  /// supported by [MaterialPageRoute] for [TargetPlatform.iOS].
  /// If a pop might be vetoed, then the back gesture is disabled.
  ///
1181 1182 1183 1184
  /// The [buildTransitions] method will not be called again if this changes,
  /// since it can change during the build as descendants of the route add or
  /// remove callbacks.
  ///
1185 1186
  /// See also:
  ///
1187 1188 1189 1190
  ///  * [addScopedWillPopCallback], which adds a callback.
  ///  * [removeScopedWillPopCallback], which removes a callback.
  ///  * [willHandlePopInternally], which reports on another reason why
  ///    a pop might be vetoed.
1191
  @protected
1192
  bool get hasScopedWillPopCallback {
1193 1194 1195 1196 1197 1198 1199
    return _willPopCallbacks.isNotEmpty;
  }

  @override
  void didChangePrevious(Route<dynamic> previousRoute) {
    super.didChangePrevious(previousRoute);
    changedInternalState();
1200
  }
1201

1202 1203 1204 1205
  @override
  void changedInternalState() {
    super.changedInternalState();
    setState(() { /* internal state already changed */ });
1206
    _modalBarrier.markNeedsBuild();
1207 1208 1209
  }

  @override
1210 1211 1212 1213
  void changedExternalState() {
    super.changedExternalState();
    if (_scopeKey.currentState != null)
      _scopeKey.currentState._forceRebuildPage();
1214 1215 1216 1217 1218 1219
  }

  /// Whether this route can be popped.
  ///
  /// When this changes, the route will rebuild, and any widgets that used
  /// [ModalRoute.of] will be notified.
1220
  bool get canPop => !isFirst || willHandlePopInternally;
1221

1222 1223
  // Internals

1224 1225 1226
  final GlobalKey<_ModalScopeState<T>> _scopeKey = GlobalKey<_ModalScopeState<T>>();
  final GlobalKey _subtreeKey = GlobalKey();
  final PageStorageBucket _storageBucket = PageStorageBucket();
1227

1228 1229
  static final Animatable<double> _easeCurveTween = CurveTween(curve: Curves.ease);

1230
  // one of the builders
1231
  OverlayEntry _modalBarrier;
1232
  Widget _buildModalBarrier(BuildContext context) {
1233
    Widget barrier;
1234
    if (barrierColor != null && !offstage) { // changedInternalState is called if these update
Hixie's avatar
Hixie committed
1235
      assert(barrierColor != _kTransparent);
1236 1237 1238 1239 1240 1241
      final Animation<Color> color = animation.drive(
        ColorTween(
          begin: _kTransparent,
          end: barrierColor, // changedInternalState is called if this updates
        ).chain(_easeCurveTween),
      );
1242
      barrier = AnimatedModalBarrier(
1243
        color: color,
1244 1245
        dismissible: barrierDismissible, // changedInternalState is called if this updates
        semanticsLabel: barrierLabel, // changedInternalState is called if this updates
1246
        barrierSemanticsDismissible: semanticsDismissible,
Hixie's avatar
Hixie committed
1247 1248
      );
    } else {
1249
      barrier = ModalBarrier(
1250 1251
        dismissible: barrierDismissible, // changedInternalState is called if this updates
        semanticsLabel: barrierLabel, // changedInternalState is called if this updates
1252
        barrierSemanticsDismissible: semanticsDismissible,
1253
      );
Hixie's avatar
Hixie committed
1254
    }
1255
    return IgnorePointer(
1256 1257 1258
      ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates
                animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
      child: barrier,
1259
    );
1260 1261
  }

1262 1263 1264 1265
  // We cache the part of the modal scope that doesn't change from frame to
  // frame so that we minimize the amount of building that happens.
  Widget _modalScopeCache;

1266
  // one of the builders
1267
  Widget _buildModalScope(BuildContext context) {
1268
    return _modalScopeCache ??= _ModalScope<T>(
1269
      key: _scopeKey,
1270
      route: this,
1271
      // _ModalScope calls buildTransitions() and buildChild(), defined above
1272 1273 1274
    );
  }

1275
  @override
1276
  Iterable<OverlayEntry> createOverlayEntries() sync* {
1277 1278
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
1279
  }
1280

1281
  @override
1282
  String toString() => '$runtimeType($settings, animation: $_animation)';
1283
}
Hixie's avatar
Hixie committed
1284 1285 1286

/// A modal route that overlays a widget over the current route.
abstract class PopupRoute<T> extends ModalRoute<T> {
1287 1288 1289 1290 1291
  /// Initializes the [PopupRoute].
  PopupRoute({
    RouteSettings settings,
  }) : super(settings: settings);

1292
  @override
Hixie's avatar
Hixie committed
1293
  bool get opaque => false;
1294

1295 1296
  @override
  bool get maintainState => true;
Hixie's avatar
Hixie committed
1297
}
1298 1299 1300 1301

/// A [Navigator] observer that notifies [RouteAware]s of changes to the
/// state of their [Route].
///
1302 1303 1304
/// [RouteObserver] informs subscribers whenever a route of type `R` is pushed
/// on top of their own route of type `R` or popped from it. This is for example
/// useful to keep track of page transitions, e.g. a `RouteObserver<PageRoute>`
1305 1306 1307
/// will inform subscribed [RouteAware]s whenever the user navigates away from
/// the current page route to another page route.
///
1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320
/// To be informed about route changes of any type, consider instantiating a
/// `RouteObserver<Route>`.
///
/// ## Type arguments
///
/// When using more aggressive
/// [lints](http://dart-lang.github.io/linter/lints/), in particular lints such
/// as `always_specify_types`, the Dart analyzer will require that certain types
/// be given with their type arguments. Since the [Route] class and its
/// subclasses have a type argument, this includes the arguments passed to this
/// class. Consider using `dynamic` to specify the entire class of routes rather
/// than only specific subtypes. For example, to watch for all [PageRoute]
/// variants, the `RouteObserver<PageRoute<dynamic>>` type may be used.
1321
///
1322
/// {@tool sample}
1323
///
1324
/// To make a [StatefulWidget] aware of its current [Route] state, implement
1325
/// [RouteAware] in its [State] and subscribe it to a [RouteObserver]:
1326 1327
///
/// ```dart
1328
/// // Register the RouteObserver as a navigation observer.
1329
/// final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
1330
/// void main() {
1331 1332
///   runApp(MaterialApp(
///     home: Container(),
1333 1334 1335 1336 1337
///     navigatorObservers: [routeObserver],
///   ));
/// }
///
/// class RouteAwareWidget extends StatefulWidget {
1338
///   State<RouteAwareWidget> createState() => RouteAwareWidgetState();
1339 1340 1341
/// }
///
/// // Implement RouteAware in a widget's state and subscribe it to the RouteObserver.
1342
/// class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
1343
///
1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357
///   @override
///   void didChangeDependencies() {
///     super.didChangeDependencies();
///     routeObserver.subscribe(this, ModalRoute.of(context));
///   }
///
///   @override
///   void dispose() {
///     routeObserver.unsubscribe(this);
///     super.dispose();
///   }
///
///   @override
///   void didPush() {
1358
///     // Route was pushed onto navigator and is now topmost route.
1359 1360 1361 1362
///   }
///
///   @override
///   void didPopNext() {
1363
///     // Covering route was popped off the navigator.
1364 1365
///   }
///
1366
///   @override
1367
///   Widget build(BuildContext context) => Container();
1368
///
1369 1370
/// }
/// ```
1371
/// {@end-tool}
1372 1373
class RouteObserver<R extends Route<dynamic>> extends NavigatorObserver {
  final Map<R, Set<RouteAware>> _listeners = <R, Set<RouteAware>>{};
1374

1375 1376 1377 1378 1379
  /// Subscribe [routeAware] to be informed about changes to [route].
  ///
  /// Going forward, [routeAware] will be informed about qualifying changes
  /// to [route], e.g. when [route] is covered by another route or when [route]
  /// is popped off the [Navigator] stack.
1380
  void subscribe(RouteAware routeAware, R route) {
1381 1382
    assert(routeAware != null);
    assert(route != null);
1383
    final Set<RouteAware> subscribers = _listeners.putIfAbsent(route, () => <RouteAware>{});
1384
    if (subscribers.add(routeAware)) {
1385 1386 1387 1388
      routeAware.didPush();
    }
  }

1389 1390
  /// Unsubscribe [routeAware].
  ///
1391 1392
  /// [routeAware] is no longer informed about changes to its route. If the given argument was
  /// subscribed to multiple types, this will unregister it (once) from each type.
1393 1394
  void unsubscribe(RouteAware routeAware) {
    assert(routeAware != null);
1395
    for (R route in _listeners.keys) {
1396 1397 1398
      final Set<RouteAware> subscribers = _listeners[route];
      subscribers?.remove(routeAware);
    }
1399 1400 1401 1402
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
1403
    if (route is R && previousRoute is R) {
1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418
      final List<RouteAware> previousSubscribers = _listeners[previousRoute]?.toList();

      if (previousSubscribers != null) {
        for (RouteAware routeAware in previousSubscribers) {
          routeAware.didPopNext();
        }
      }

      final List<RouteAware> subscribers = _listeners[route]?.toList();

      if (subscribers != null) {
        for (RouteAware routeAware in subscribers) {
          routeAware.didPop();
        }
      }
1419 1420 1421 1422 1423
    }
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
1424
    if (route is R && previousRoute is R) {
1425 1426 1427 1428 1429 1430 1431
      final Set<RouteAware> previousSubscribers = _listeners[previousRoute];

      if (previousSubscribers != null) {
        for (RouteAware routeAware in previousSubscribers) {
          routeAware.didPushNext();
        }
      }
1432 1433 1434 1435
    }
  }
}

1436 1437 1438 1439
/// An interface for objects that are aware of their current [Route].
///
/// This is used with [RouteObserver] to make a widget aware of changes to the
/// [Navigator]'s session history.
1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453
abstract class RouteAware {
  /// Called when the top route has been popped off, and the current route
  /// shows up.
  void didPopNext() { }

  /// Called when the current route has been pushed.
  void didPush() { }

  /// Called when the current route has been popped off.
  void didPop() { }

  /// Called when a new route has been pushed, and the current route is no
  /// longer visible.
  void didPushNext() { }
1454
}
1455 1456 1457 1458 1459 1460 1461 1462 1463 1464

class _DialogRoute<T> extends PopupRoute<T> {
  _DialogRoute({
    @required RoutePageBuilder pageBuilder,
    bool barrierDismissible = true,
    String barrierLabel,
    Color barrierColor = const Color(0x80000000),
    Duration transitionDuration = const Duration(milliseconds: 200),
    RouteTransitionsBuilder transitionBuilder,
    RouteSettings settings,
1465 1466 1467 1468 1469 1470 1471 1472
  }) : assert(barrierDismissible != null),
       _pageBuilder = pageBuilder,
       _barrierDismissible = barrierDismissible,
       _barrierLabel = barrierLabel,
       _barrierColor = barrierColor,
       _transitionDuration = transitionDuration,
       _transitionBuilder = transitionBuilder,
       super(settings: settings);
1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495

  final RoutePageBuilder _pageBuilder;

  @override
  bool get barrierDismissible => _barrierDismissible;
  final bool _barrierDismissible;

  @override
  String get barrierLabel => _barrierLabel;
  final String _barrierLabel;

  @override
  Color get barrierColor => _barrierColor;
  final Color _barrierColor;

  @override
  Duration get transitionDuration => _transitionDuration;
  final Duration _transitionDuration;

  final RouteTransitionsBuilder _transitionBuilder;

  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
1496
    return Semantics(
1497 1498 1499 1500 1501 1502 1503 1504 1505
      child: _pageBuilder(context, animation, secondaryAnimation),
      scopesRoute: true,
      explicitChildNodes: true,
    );
  }

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    if (_transitionBuilder == null) {
1506 1507
      return FadeTransition(
          opacity: CurvedAnimation(
1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560
            parent: animation,
            curve: Curves.linear,
          ),
          child: child);
    } // Some default transition
    return _transitionBuilder(context, animation, secondaryAnimation, child);
  }
}

/// Displays a dialog above the current contents of the app.
///
/// This function allows for customization of aspects of the dialog popup.
///
/// This function takes a `pageBuilder` which is used to build the primary
/// content of the route (typically a dialog widget). Content below the dialog
/// is dimmed with a [ModalBarrier]. The widget returned by the `pageBuilder`
/// does not share a context with the location that `showGeneralDialog` is
/// originally called from. Use a [StatefulBuilder] or a custom
/// [StatefulWidget] if the dialog needs to update dynamically. The
/// `pageBuilder` argument can not be null.
///
/// The `context` argument is used to look up the [Navigator] for the dialog.
/// It is only used when the method is called. Its corresponding widget can
/// be safely removed from the tree before the dialog is closed.
///
/// The `barrierDismissible` argument is used to determine whether this route
/// can be dismissed by tapping the modal barrier. This argument defaults
/// to true. If `barrierDismissible` is true, a non-null `barrierLabel` must be
/// provided.
///
/// The `barrierLabel` argument is the semantic label used for a dismissible
/// barrier. This argument defaults to "Dismiss".
///
/// The `barrierColor` argument is the color used for the modal barrier. This
/// argument defaults to `Color(0x80000000)`.
///
/// The `transitionDuration` argument is used to determine how long it takes
/// for the route to arrive on or leave off the screen. This argument defaults
/// to 200 milliseconds.
///
/// The `transitionBuilder` argument is used to define how the route arrives on
/// and leaves off the screen. By default, the transition is a linear fade of
/// the page's contents.
///
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
/// The dialog route created by this method is pushed to the root navigator.
/// If the application has multiple [Navigator] objects, it may be necessary to
/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
/// dialog rather than just `Navigator.pop(context, result)`.
///
/// See also:
1561
///
1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574
///  * [showDialog], which displays a Material-style dialog.
///  * [showCupertinoDialog], which displays an iOS-style dialog.
Future<T> showGeneralDialog<T>({
  @required BuildContext context,
  @required RoutePageBuilder pageBuilder,
  bool barrierDismissible,
  String barrierLabel,
  Color barrierColor,
  Duration transitionDuration,
  RouteTransitionsBuilder transitionBuilder,
}) {
  assert(pageBuilder != null);
  assert(!barrierDismissible || barrierLabel != null);
1575
  return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588
    pageBuilder: pageBuilder,
    barrierDismissible: barrierDismissible,
    barrierLabel: barrierLabel,
    barrierColor: barrierColor,
    transitionDuration: transitionDuration,
    transitionBuilder: transitionBuilder,
  ));
}

/// Signature for the function that builds a route's primary contents.
/// Used in [PageRouteBuilder] and [showGeneralDialog].
///
/// See [ModalRoute.buildPage] for complete definition of the parameters.
1589
typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
1590 1591 1592 1593 1594

/// Signature for the function that builds a route's transitions.
/// Used in [PageRouteBuilder] and [showGeneralDialog].
///
/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
1595
typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);