routes.dart 22.5 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 8
import 'package:meta/meta.dart';

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

18
const Color _kTransparent = const Color(0x00000000);
Hixie's avatar
Hixie committed
19

20
/// A route that displays widgets in the [Navigator]'s [Overlay].
Hixie's avatar
Hixie committed
21
abstract class OverlayRoute<T> extends Route<T> {
22
  /// Subclasses should override this getter to return the builders for the overlay.
23
  Iterable<OverlayEntry> createOverlayEntries();
Adam Barth's avatar
Adam Barth committed
24

25
  /// The entries this route has placed in the overlay.
26
  @override
Adam Barth's avatar
Adam Barth committed
27
  List<OverlayEntry> get overlayEntries => _overlayEntries;
28
  final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
Adam Barth's avatar
Adam Barth committed
29

30
  @override
Hixie's avatar
Hixie committed
31
  void install(OverlayEntry insertionPoint) {
32
    assert(_overlayEntries.isEmpty);
33
    _overlayEntries.addAll(createOverlayEntries());
Hixie's avatar
Hixie committed
34
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
Adam Barth's avatar
Adam Barth committed
35 36
  }

37 38 39 40 41 42 43 44
  /// 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().
  ///
  /// If this is called, the Navigator will not call dispose(). It is the
  /// responsibility of the Route to later call dispose().
  ///
45
  /// Subclasses shouldn't call this if they want to delay the finished() call.
46
  @override
Hixie's avatar
Hixie committed
47
  bool didPop(T result) {
Hixie's avatar
Hixie committed
48
    finished();
Hixie's avatar
Hixie committed
49
    return true;
Hixie's avatar
Hixie committed
50 51 52 53 54 55 56 57 58 59
  }

  /// Clears out the overlay entries.
  ///
  /// This method is intended to be used by subclasses who don't call
  /// super.didPop() because they want to have control over the timing of the
  /// overlay removal.
  ///
  /// Do not call this method outside of this context.
  void finished() {
Adam Barth's avatar
Adam Barth committed
60 61 62 63
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
  }
64

65
  @override
66 67 68
  void dispose() {
    finished();
  }
Adam Barth's avatar
Adam Barth committed
69 70
}

71
/// A route with entrance and exit transitions.
Hixie's avatar
Hixie committed
72
abstract class TransitionRoute<T> extends OverlayRoute<T> {
73
  /// Creates a route with entrance and exit transitions.
Hixie's avatar
Hixie committed
74 75 76 77 78
  TransitionRoute({
    Completer<T> popCompleter,
    Completer<T> transitionCompleter
  }) : _popCompleter = popCompleter,
       _transitionCompleter = transitionCompleter;
79

80
  /// The same as the default constructor but callable with mixins.
81 82 83 84 85
  TransitionRoute.explicit(
    Completer<T> popCompleter,
    Completer<T> transitionCompleter
  ) : this(popCompleter: popCompleter, transitionCompleter: transitionCompleter);

Hixie's avatar
Hixie committed
86 87 88 89 90
  /// This future completes once the animation has been dismissed. For
  /// ModalRoutes, this will be after the completer that's passed in, since that
  /// one completes before the animation even starts, as soon as the route is
  /// popped.
  Future<T> get popped => _popCompleter?.future;
Hixie's avatar
Hixie committed
91
  final Completer<T> _popCompleter;
Hixie's avatar
Hixie committed
92 93 94

  /// This future completes only once the transition itself has finished, after
  /// the overlay entries have been removed from the navigator's overlay.
Hixie's avatar
Hixie committed
95
  Future<T> get completed => _transitionCompleter?.future;
Hixie's avatar
Hixie committed
96
  final Completer<T> _transitionCompleter;
97

98
  /// The duration the transition lasts.
Adam Barth's avatar
Adam Barth committed
99
  Duration get transitionDuration;
100 101 102 103 104

  /// 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
105 106
  bool get opaque;

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

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

119
  /// Called to create the animation controller that will drive the transitions to
120 121
  /// this route from the previous one, and back to the previous route from this
  /// one.
122
  AnimationController createAnimationController() {
Adam Barth's avatar
Adam Barth committed
123 124
    Duration duration = transitionDuration;
    assert(duration != null && duration >= Duration.ZERO);
125
    return new AnimationController(duration: duration, debugLabel: debugLabel);
Adam Barth's avatar
Adam Barth committed
126 127
  }

128 129
  /// Called to create the animation that exposes the current progress of
  /// the transition controlled by the animation controller created by
130
  /// [createAnimationController()].
131
  Animation<double> createAnimation() {
132 133
    assert(_controller != null);
    return _controller.view;
134 135
  }

Hixie's avatar
Hixie committed
136
  T _result;
Adam Barth's avatar
Adam Barth committed
137

138
  void _handleStatusChanged(AnimationStatus status) {
Adam Barth's avatar
Adam Barth committed
139
    switch (status) {
140
      case AnimationStatus.completed:
Adam Barth's avatar
Adam Barth committed
141 142 143
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
144 145
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Adam Barth's avatar
Adam Barth committed
146 147 148
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
149
      case AnimationStatus.dismissed:
Hixie's avatar
Hixie committed
150 151 152
        assert(!overlayEntries.first.opaque);
        finished(); // clear the overlays
        assert(overlayEntries.isEmpty);
Adam Barth's avatar
Adam Barth committed
153 154 155 156
        break;
    }
  }

157 158 159
  /// 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.
160
  Animation<double> get forwardAnimation => _forwardAnimation;
161
  final ProxyAnimation _forwardAnimation = new ProxyAnimation(kAlwaysDismissedAnimation);
Hixie's avatar
Hixie committed
162

163
  @override
Hixie's avatar
Hixie committed
164
  void install(OverlayEntry insertionPoint) {
165 166 167 168
    _controller = createAnimationController();
    assert(_controller != null);
    _animation = createAnimation();
    assert(_animation != null);
Hixie's avatar
Hixie committed
169
    super.install(insertionPoint);
170 171
  }

172
  @override
173
  void didPush() {
174
    _animation.addStatusListener(_handleStatusChanged);
175
    _controller.forward();
176
    super.didPush();
Adam Barth's avatar
Adam Barth committed
177 178
  }

179
  @override
180 181
  void didReplace(Route<dynamic> oldRoute) {
    if (oldRoute is TransitionRoute<dynamic>)
182
      _controller.value = oldRoute._controller.value;
183
    _animation.addStatusListener(_handleStatusChanged);
184 185 186
    super.didReplace(oldRoute);
  }

187
  @override
Hixie's avatar
Hixie committed
188
  bool didPop(T result) {
Adam Barth's avatar
Adam Barth committed
189
    _result = result;
190
    _controller.reverse();
Hixie's avatar
Hixie committed
191
    _popCompleter?.complete(_result);
Hixie's avatar
Hixie committed
192
    return true;
Hixie's avatar
Hixie committed
193 194
  }

195
  @override
196
  void didPopNext(Route<dynamic> nextRoute) {
197
    _updateForwardAnimation(nextRoute);
Hixie's avatar
Hixie committed
198
    super.didPopNext(nextRoute);
Adam Barth's avatar
Adam Barth committed
199 200
  }

201
  @override
202
  void didChangeNext(Route<dynamic> nextRoute) {
203
    _updateForwardAnimation(nextRoute);
Hixie's avatar
Hixie committed
204
    super.didChangeNext(nextRoute);
205 206
  }

207 208
  void _updateForwardAnimation(Route<dynamic> nextRoute) {
    if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
209
      Animation<double> current = _forwardAnimation.parent;
Hixie's avatar
Hixie committed
210
      if (current != null) {
211
        if (current is TrainHoppingAnimation) {
212 213
          TrainHoppingAnimation newAnimation;
          newAnimation = new TrainHoppingAnimation(
Hixie's avatar
Hixie committed
214
            current.currentTrain,
215
            nextRoute._animation,
Hixie's avatar
Hixie committed
216
            onSwitchedTrain: () {
217
              assert(_forwardAnimation.parent == newAnimation);
218
              assert(newAnimation.currentTrain == nextRoute._animation);
219
              _forwardAnimation.parent = newAnimation.currentTrain;
220
              newAnimation.dispose();
Hixie's avatar
Hixie committed
221 222
            }
          );
223
          _forwardAnimation.parent = newAnimation;
Hixie's avatar
Hixie committed
224 225
          current.dispose();
        } else {
226
          _forwardAnimation.parent = new TrainHoppingAnimation(current, nextRoute._animation);
Hixie's avatar
Hixie committed
227 228
        }
      } else {
229
        _forwardAnimation.parent = nextRoute._animation;
Hixie's avatar
Hixie committed
230
      }
Hixie's avatar
Hixie committed
231
    } else {
232
      _forwardAnimation.parent = kAlwaysDismissedAnimation;
Hixie's avatar
Hixie committed
233 234 235
    }
  }

236 237
  /// Whether this route can perform a transition to the given route.
  ///
238
  /// Subclasses can override this method to restrict the set of routes they
239
  /// need to coordinate transitions with.
240
  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true;
241 242 243

  /// Whether this route can perform a transition from the given route.
  ///
244
  /// Subclasses can override this method to restrict the set of routes they
245
  /// need to coordinate transitions with.
246
  bool canTransitionFrom(TransitionRoute<dynamic> nextRoute) => true;
247

248
  @override
Hixie's avatar
Hixie committed
249 250 251 252 253
  void finished() {
    super.finished();
    _transitionCompleter?.complete(_result);
  }

254
  @override
Hixie's avatar
Hixie committed
255
  void dispose() {
256
    _controller.dispose();
Hixie's avatar
Hixie committed
257 258 259
    super.dispose();
  }

260
  /// A short description of this route useful for debugging.
Adam Barth's avatar
Adam Barth committed
261
  String get debugLabel => '$runtimeType';
262 263

  @override
264
  String toString() => '$runtimeType(animation: $_controller)';
Adam Barth's avatar
Adam Barth committed
265
}
266

267
/// An entry in the history of a [LocalHistoryRoute].
Hixie's avatar
Hixie committed
268
class LocalHistoryEntry {
269
  /// Creates an entry in the history of a [LocalHistoryRoute].
Hixie's avatar
Hixie committed
270
  LocalHistoryEntry({ this.onRemove });
271 272

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

275
  LocalHistoryRoute<dynamic> _owner;
276 277

  /// Remove this entry from the history of its associated [LocalHistoryRoute].
Hixie's avatar
Hixie committed
278 279
  void remove() {
    _owner.removeLocalHistoryEntry(this);
280
    assert(_owner == null);
Hixie's avatar
Hixie committed
281
  }
282

Hixie's avatar
Hixie committed
283 284 285 286 287 288
  void _notifyRemoved() {
    if (onRemove != null)
      onRemove();
  }
}

289 290 291 292 293 294 295
/// A route that can handle back navigations internally by popping a list.
///
/// When a [Navigator] is instructed to pop, the current route is given an
/// opportunity to handle the pop internally. A LocalHistoryRoute handles the
/// pop internally if its list of local history entries is non-empty. Rather
/// than being removed as the current route, the most recent [LocalHistoryEntry]
/// is removed from the list and its [onRemove] is called.
296
abstract class LocalHistoryRoute<T> extends Route<T> {
Hixie's avatar
Hixie committed
297
  List<LocalHistoryEntry> _localHistory;
298 299 300

  /// Adds a local history entry to this route.
  ///
301
  /// When asked to pop, if this route has any local history entries, this route
302 303 304 305 306
  /// 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.
Hixie's avatar
Hixie committed
307 308 309 310 311 312
  void addLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry._owner == null);
    entry._owner = this;
    _localHistory ??= <LocalHistoryEntry>[];
    _localHistory.add(entry);
  }
313 314 315 316

  /// Remove a local history entry from this route.
  ///
  /// The entry's [onRemove] callback, if any, will be called synchronously.
Hixie's avatar
Hixie committed
317 318 319 320 321 322 323 324
  void removeLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry != null);
    assert(entry._owner == this);
    assert(_localHistory.contains(entry));
    _localHistory.remove(entry);
    entry._owner = null;
    entry._notifyRemoved();
  }
325 326

  @override
Hixie's avatar
Hixie committed
327 328 329 330 331 332 333 334 335 336
  bool didPop(T result) {
    if (_localHistory != null && _localHistory.length > 0) {
      LocalHistoryEntry entry = _localHistory.removeLast();
      assert(entry._owner == this);
      entry._owner = null;
      entry._notifyRemoved();
      return false;
    }
    return super.didPop(result);
  }
337 338

  @override
Hixie's avatar
Hixie committed
339 340 341
  bool get willHandlePopInternally {
    return _localHistory != null && _localHistory.length > 0;
  }
Hixie's avatar
Hixie committed
342 343
}

Hixie's avatar
Hixie committed
344 345 346
class _ModalScopeStatus extends InheritedWidget {
  _ModalScopeStatus({
    Key key,
347
    this.isCurrent,
Hixie's avatar
Hixie committed
348 349 350
    this.route,
    Widget child
  }) : super(key: key, child: child) {
351
    assert(isCurrent != null);
Hixie's avatar
Hixie committed
352 353 354 355
    assert(route != null);
    assert(child != null);
  }

356
  final bool isCurrent;
357
  final Route<dynamic> route;
Hixie's avatar
Hixie committed
358

359
  @override
Hixie's avatar
Hixie committed
360
  bool updateShouldNotify(_ModalScopeStatus old) {
361
    return isCurrent != old.isCurrent ||
Hixie's avatar
Hixie committed
362 363 364
           route != old.route;
  }

365
  @override
Hixie's avatar
Hixie committed
366 367
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
368
    description.add('${isCurrent ? "active" : "inactive"}');
Hixie's avatar
Hixie committed
369 370 371
  }
}

372
class _ModalScope extends StatefulWidget {
373 374
  _ModalScope({
    Key key,
375 376
    this.route,
    this.page
377
  }) : super(key: key);
378

379
  final ModalRoute<dynamic> route;
380
  final Widget page;
381

382
  @override
383 384 385 386
  _ModalScopeState createState() => new _ModalScopeState();
}

class _ModalScopeState extends State<_ModalScope> {
387
  @override
388 389
  void initState() {
    super.initState();
390 391
    config.route.animation?.addStatusListener(_animationStatusChanged);
    config.route.forwardAnimation?.addStatusListener(_animationStatusChanged);
392 393
  }

394
  @override
395
  void didUpdateConfig(_ModalScope oldConfig) {
396
    assert(config.route == oldConfig.route);
397 398
  }

399
  @override
400
  void dispose() {
401 402
    config.route.animation?.removeStatusListener(_animationStatusChanged);
    config.route.forwardAnimation?.removeStatusListener(_animationStatusChanged);
403 404 405
    super.dispose();
  }

406
  void _animationStatusChanged(AnimationStatus status) {
407
    setState(() {
408
      // The animation's states are our build state, and they changed already.
409 410 411
    });
  }

412 413
  void _routeSetState(VoidCallback fn) {
    setState(fn);
414 415
  }

416
  @override
417
  Widget build(BuildContext context) {
418
    return new Focus(
419
      key: config.route.focusKey,
420
      child: new Offstage(
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
        offstage: config.route.offstage,
        child: new IgnorePointer(
          ignoring: config.route.animation?.status == AnimationStatus.reverse,
          child: config.route.buildTransitions(
            context,
            config.route.animation,
            config.route.forwardAnimation,
            new RepaintBoundary(
              child: new PageStorage(
                key: config.route._subtreeKey,
                bucket: config.route._storageBucket,
                child: new _ModalScopeStatus(
                  route: config.route,
                  isCurrent: config.route.isCurrent,
                  child: config.page
                )
              )
            )
          )
        )
      )
442
    );
443 444 445
  }
}

446
/// A route that blocks interaction with previous routes.
447 448 449 450
///
/// ModalRoutes 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.
451
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
452
  /// Creates a route that blocks interaction with previous routes.
453
  ModalRoute({
Hixie's avatar
Hixie committed
454
    Completer<T> completer,
455
    this.settings: const RouteSettings()
456
  }) : super.explicit(completer, null);
457

Hixie's avatar
Hixie committed
458 459
  // The API for general users of this class

460 461 462
  /// The settings for this route.
  ///
  /// See [RouteSettings] for details.
463
  final RouteSettings settings;
464

465 466
  /// Returns the modal route most closely associated with the given context.
  ///
467
  /// Returns `null` if the given context is not associated with a modal route.
468
  static ModalRoute<dynamic> of(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
469
    _ModalScopeStatus widget = context.inheritFromWidgetOfExactType(_ModalScopeStatus);
Hixie's avatar
Hixie committed
470 471 472
    return widget?.route;
  }

473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
  /// Whenever you need to change internal state for a ModalRoute object, make
  /// the change in a function that you pass to setState(), as in:
  ///
  /// ```dart
  /// setState(() { myState = newValue });
  /// ```
  ///
  /// If you just change the state directly without calling setState(), then the
  /// 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
495 496 497 498 499 500 501 502 503 504 505
  /// 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
        && route is ModalRoute && route.settings.name == name;
    };
  }
506 507 508

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

509
  /// Override this method to build the primary content of this route.
510 511 512 513 514 515 516 517
  ///
  /// * [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.
  /// * [forwardAnimation] 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.
518
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation);
519

520
  /// Override this method to wrap the route in a number of transition widgets.
521 522 523 524 525 526 527 528 529 530 531 532 533
  ///
  /// For example, to create a fade entrance transition, wrap the given child
  /// widget in a [FadeTransition] using the given animation as the opacity.
  ///
  /// By default, the child is not wrapped in any transition widgets.
  ///
  /// * [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.
  /// * [forwardAnimation] 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.
534
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation, Widget child) {
535 536 537
    return child;
  }

538 539 540
  @override
  GlobalKey get focusKey => new GlobalObjectKey(this);

541 542 543 544 545 546 547
  @override
  void install(OverlayEntry insertionPoint) {
    super.install(insertionPoint);
    _animationProxy = new ProxyAnimation(super.animation);
    _forwardAnimationProxy = new ProxyAnimation(super.forwardAnimation);
  }

548
  @override
549
  void didPush() {
550
    if (!settings.isInitialRoute) {
551 552 553 554 555 556 557 558 559 560 561
      BuildContext overlayContext = navigator.overlay?.context;
      assert(() {
        if (overlayContext == null) {
          throw new FlutterError(
            'Unable to find the BuildContext for the Navigator\'s overlay.\n'
            'Did you remember to pass the settings object to the route\'s '
            'constructor in your onGenerateRoute callback?'
          );
        }
        return true;
      });
562 563
      Focus.moveScopeTo(focusKey, context: overlayContext);
    }
564 565
    super.didPush();
  }
Hixie's avatar
Hixie committed
566

567 568 569 570 571 572
  @override
  void didPopNext(Route<dynamic> nextRoute) {
    Focus.moveScopeTo(focusKey, context: navigator.overlay.context);
    super.didPopNext(nextRoute);
  }

573 574
  // The API for subclasses to override - used by this class

Hixie's avatar
Hixie committed
575 576
  /// Whether you can dismiss this route by tapping the modal barrier.
  bool get barrierDismissable;
577

Hixie's avatar
Hixie committed
578 579 580
  /// The color to use for the modal barrier. If this is null, the barrier will
  /// be transparent.
  Color get barrierColor;
581

582 583 584 585 586 587 588
  /// 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.
  bool get maintainState;

589 590 591

  // The API for _ModalScope and HeroController

592 593 594 595 596 597 598
  /// 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.
599 600
  bool get offstage => _offstage;
  bool _offstage = false;
601
  set offstage (bool value) {
602 603
    if (_offstage == value)
      return;
604 605 606
    setState(() {
      _offstage = value;
    });
607 608
    _animationProxy.parent = _offstage ? kAlwaysCompleteAnimation : super.animation;
    _forwardAnimationProxy.parent = _offstage ? kAlwaysDismissedAnimation : super.forwardAnimation;
609 610
  }

611
  /// The build context for the subtree containing the primary content of this route.
612 613
  BuildContext get subtreeContext => _subtreeKey.currentContext;

614 615 616 617 618 619 620 621
  @override
  Animation<double> get animation => _animationProxy;
  ProxyAnimation _animationProxy;

  @override
  Animation<double> get forwardAnimation => _forwardAnimationProxy;
  ProxyAnimation _forwardAnimationProxy;

622 623 624

  // Internals

625
  final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>();
626 627 628
  final GlobalKey _subtreeKey = new GlobalKey();
  final PageStorageBucket _storageBucket = new PageStorageBucket();

629
  // one of the builders
630
  Widget _buildModalBarrier(BuildContext context) {
631
    Widget barrier;
Hixie's avatar
Hixie committed
632 633
    if (barrierColor != null) {
      assert(barrierColor != _kTransparent);
634
      Animation<Color> color = new ColorTween(
635 636 637 638 639 640
        begin: _kTransparent,
        end: barrierColor
      ).animate(new CurvedAnimation(
        parent: animation,
        curve: Curves.ease
      ));
641
      barrier = new AnimatedModalBarrier(
642
        color: color,
Hixie's avatar
Hixie committed
643 644 645
        dismissable: barrierDismissable
      );
    } else {
646
      barrier = new ModalBarrier(dismissable: barrierDismissable);
Hixie's avatar
Hixie committed
647
    }
648
    assert(animation.status != AnimationStatus.dismissed);
649
    return new IgnorePointer(
650
      ignoring: animation.status == AnimationStatus.reverse,
651 652
      child: barrier
    );
653 654
  }

655
  // one of the builders
656 657 658
  Widget _buildModalScope(BuildContext context) {
    return new _ModalScope(
      key: _scopeKey,
659 660 661
      route: this,
      page: buildPage(context, animation, forwardAnimation)
      // _ModalScope calls buildTransitions(), defined above
662 663 664
    );
  }

665
  @override
666 667 668 669
  Iterable<OverlayEntry> createOverlayEntries() sync* {
    yield new OverlayEntry(builder: _buildModalBarrier);
    yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
  }
670

671
  @override
672
  String toString() => '$runtimeType($settings, animation: $_animation)';
673
}
Hixie's avatar
Hixie committed
674 675 676

/// A modal route that overlays a widget over the current route.
abstract class PopupRoute<T> extends ModalRoute<T> {
677
  /// Creates a modal route that overlays a widget over the current route.
Hixie's avatar
Hixie committed
678
  PopupRoute({ Completer<T> completer }) : super(completer: completer);
679 680

  @override
Hixie's avatar
Hixie committed
681
  bool get opaque => false;
682

683 684 685
  @override
  bool get maintainState => true;

686
  @override
687 688
  void didChangeNext(Route<dynamic> nextRoute) {
    assert(nextRoute is! PageRoute<dynamic>);
Hixie's avatar
Hixie committed
689
    super.didChangeNext(nextRoute);
690
  }
Hixie's avatar
Hixie committed
691
}