routes.dart 19.8 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';

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

16
const Color _kTransparent = const Color(0x00000000);
Hixie's avatar
Hixie committed
17

18
/// A route that displays widgets in the [Navigator]'s [Overlay].
Hixie's avatar
Hixie committed
19
abstract class OverlayRoute<T> extends Route<T> {
20
  /// Subclasses should override this getter to return the builders for the overlay.
21
  List<WidgetBuilder> get builders;
Adam Barth's avatar
Adam Barth committed
22

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

28
  @override
Hixie's avatar
Hixie committed
29
  void install(OverlayEntry insertionPoint) {
30
    assert(_overlayEntries.isEmpty);
31
    for (WidgetBuilder builder in builders)
32
      _overlayEntries.add(new OverlayEntry(builder: builder));
Hixie's avatar
Hixie committed
33
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
Adam Barth's avatar
Adam Barth committed
34 35
  }

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

  /// 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
59 60 61 62
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
  }
63

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

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

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

Hixie's avatar
Hixie committed
84 85 86 87 88
  /// 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
89
  final Completer<T> _popCompleter;
Hixie's avatar
Hixie committed
90 91 92

  /// 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
93
  Future<T> get completed => _transitionCompleter?.future;
Hixie's avatar
Hixie committed
94
  final Completer<T> _transitionCompleter;
95

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

  /// 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
103 104
  bool get opaque;

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
  AnimationController _controller;
110

111
  /// Called to create the animation controller that will drive the transitions to
112 113
  /// this route from the previous one, and back to the previous route from this
  /// one.
114
  AnimationController createAnimationController() {
Adam Barth's avatar
Adam Barth committed
115 116
    Duration duration = transitionDuration;
    assert(duration != null && duration >= Duration.ZERO);
117
    return new AnimationController(duration: duration, debugLabel: debugLabel);
Adam Barth's avatar
Adam Barth committed
118 119
  }

120 121
  /// Called to create the animation that exposes the current progress of
  /// the transition controlled by the animation controller created by
122
  /// [createAnimationController()].
123
  Animation<double> createAnimation() {
124 125
    assert(_controller != null);
    return _controller.view;
126 127
  }

Hixie's avatar
Hixie committed
128
  T _result;
Adam Barth's avatar
Adam Barth committed
129

130
  void _handleStatusChanged(AnimationStatus status) {
Adam Barth's avatar
Adam Barth committed
131
    switch (status) {
132
      case AnimationStatus.completed:
Adam Barth's avatar
Adam Barth committed
133 134 135
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
136 137
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Adam Barth's avatar
Adam Barth committed
138 139 140
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
141
      case AnimationStatus.dismissed:
Hixie's avatar
Hixie committed
142 143 144
        assert(!overlayEntries.first.opaque);
        finished(); // clear the overlays
        assert(overlayEntries.isEmpty);
Adam Barth's avatar
Adam Barth committed
145 146 147 148
        break;
    }
  }

149
  Animation<double> get forwardAnimation => _forwardAnimation;
150
  final ProxyAnimation _forwardAnimation = new ProxyAnimation(kAlwaysDismissedAnimation);
Hixie's avatar
Hixie committed
151

152
  @override
Hixie's avatar
Hixie committed
153
  void install(OverlayEntry insertionPoint) {
154 155 156 157
    _controller = createAnimationController();
    assert(_controller != null);
    _animation = createAnimation();
    assert(_animation != null);
Hixie's avatar
Hixie committed
158
    super.install(insertionPoint);
159 160
  }

161
  @override
162
  void didPush() {
163
    _animation.addStatusListener(_handleStatusChanged);
164
    _controller.forward();
165
    super.didPush();
Adam Barth's avatar
Adam Barth committed
166 167
  }

168
  @override
169 170
  void didReplace(Route<dynamic> oldRoute) {
    if (oldRoute is TransitionRoute<dynamic>)
171
      _controller.value = oldRoute._controller.value;
172
    _animation.addStatusListener(_handleStatusChanged);
173 174 175
    super.didReplace(oldRoute);
  }

176
  @override
Hixie's avatar
Hixie committed
177
  bool didPop(T result) {
Adam Barth's avatar
Adam Barth committed
178
    _result = result;
179
    _controller.reverse();
Hixie's avatar
Hixie committed
180
    _popCompleter?.complete(_result);
Hixie's avatar
Hixie committed
181
    return true;
Hixie's avatar
Hixie committed
182 183
  }

184
  @override
185
  void didPopNext(Route<dynamic> nextRoute) {
186
    _updateForwardAnimation(nextRoute);
Hixie's avatar
Hixie committed
187
    super.didPopNext(nextRoute);
Adam Barth's avatar
Adam Barth committed
188 189
  }

190
  @override
191
  void didChangeNext(Route<dynamic> nextRoute) {
192
    _updateForwardAnimation(nextRoute);
Hixie's avatar
Hixie committed
193
    super.didChangeNext(nextRoute);
194 195
  }

196 197
  void _updateForwardAnimation(Route<dynamic> nextRoute) {
    if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
198
      Animation<double> current = _forwardAnimation.parent;
Hixie's avatar
Hixie committed
199
      if (current != null) {
200
        if (current is TrainHoppingAnimation) {
201 202
          TrainHoppingAnimation newAnimation;
          newAnimation = new TrainHoppingAnimation(
Hixie's avatar
Hixie committed
203
            current.currentTrain,
204
            nextRoute.animation,
Hixie's avatar
Hixie committed
205
            onSwitchedTrain: () {
206
              assert(_forwardAnimation.parent == newAnimation);
207
              assert(newAnimation.currentTrain == nextRoute.animation);
208
              _forwardAnimation.parent = newAnimation.currentTrain;
209
              newAnimation.dispose();
Hixie's avatar
Hixie committed
210 211
            }
          );
212
          _forwardAnimation.parent = newAnimation;
Hixie's avatar
Hixie committed
213 214
          current.dispose();
        } else {
215
          _forwardAnimation.parent = new TrainHoppingAnimation(current, nextRoute.animation);
Hixie's avatar
Hixie committed
216 217
        }
      } else {
218
        _forwardAnimation.parent = nextRoute.animation;
Hixie's avatar
Hixie committed
219
      }
Hixie's avatar
Hixie committed
220
    } else {
221
      _forwardAnimation.parent = kAlwaysDismissedAnimation;
Hixie's avatar
Hixie committed
222 223 224
    }
  }

225 226 227 228
  /// Whether this route can perform a transition to the given route.
  ///
  /// Subclasses can override this function to restrict the set of routes they
  /// need to coordinate transitions with.
229
  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true;
230 231 232 233 234

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

237
  @override
Hixie's avatar
Hixie committed
238 239 240 241 242
  void finished() {
    super.finished();
    _transitionCompleter?.complete(_result);
  }

243
  @override
Hixie's avatar
Hixie committed
244
  void dispose() {
245
    _controller.stop();
Hixie's avatar
Hixie committed
246 247 248
    super.dispose();
  }

Adam Barth's avatar
Adam Barth committed
249
  String get debugLabel => '$runtimeType';
250 251

  @override
252
  String toString() => '$runtimeType(animation: $_controller)';
Adam Barth's avatar
Adam Barth committed
253
}
254

255
/// An entry in the history of a [LocalHistoryRoute].
Hixie's avatar
Hixie committed
256 257
class LocalHistoryEntry {
  LocalHistoryEntry({ this.onRemove });
258 259

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

262
  LocalHistoryRoute<dynamic> _owner;
263 264

  /// Remove this entry from the history of its associated [LocalHistoryRoute].
Hixie's avatar
Hixie committed
265 266
  void remove() {
    _owner.removeLocalHistoryEntry(this);
267
    assert(_owner == null);
Hixie's avatar
Hixie committed
268
  }
269

Hixie's avatar
Hixie committed
270 271 272 273 274 275
  void _notifyRemoved() {
    if (onRemove != null)
      onRemove();
  }
}

276 277 278 279 280 281 282
/// 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.
283
abstract class LocalHistoryRoute<T> extends Route<T> {
Hixie's avatar
Hixie committed
284
  List<LocalHistoryEntry> _localHistory;
285 286 287 288 289 290 291 292 293

  /// Adds a local history entry to this route.
  ///
  /// When asked to pop, if this route has any local history entires, this route
  /// 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
294 295 296 297 298 299
  void addLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry._owner == null);
    entry._owner = this;
    _localHistory ??= <LocalHistoryEntry>[];
    _localHistory.add(entry);
  }
300 301 302 303

  /// Remove a local history entry from this route.
  ///
  /// The entry's [onRemove] callback, if any, will be called synchronously.
Hixie's avatar
Hixie committed
304 305 306 307 308 309 310 311
  void removeLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry != null);
    assert(entry._owner == this);
    assert(_localHistory.contains(entry));
    _localHistory.remove(entry);
    entry._owner = null;
    entry._notifyRemoved();
  }
312 313

  @override
Hixie's avatar
Hixie committed
314 315 316 317 318 319 320 321 322 323
  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);
  }
324 325

  @override
Hixie's avatar
Hixie committed
326 327 328
  bool get willHandlePopInternally {
    return _localHistory != null && _localHistory.length > 0;
  }
Hixie's avatar
Hixie committed
329 330
}

Hixie's avatar
Hixie committed
331 332 333
class _ModalScopeStatus extends InheritedWidget {
  _ModalScopeStatus({
    Key key,
334
    this.isCurrent,
Hixie's avatar
Hixie committed
335 336 337
    this.route,
    Widget child
  }) : super(key: key, child: child) {
338
    assert(isCurrent != null);
Hixie's avatar
Hixie committed
339 340 341 342
    assert(route != null);
    assert(child != null);
  }

343
  final bool isCurrent;
344
  final Route<dynamic> route;
Hixie's avatar
Hixie committed
345

346
  @override
Hixie's avatar
Hixie committed
347
  bool updateShouldNotify(_ModalScopeStatus old) {
348
    return isCurrent != old.isCurrent ||
Hixie's avatar
Hixie committed
349 350 351
           route != old.route;
  }

352
  @override
Hixie's avatar
Hixie committed
353 354
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
355
    description.add('${isCurrent ? "active" : "inactive"}');
Hixie's avatar
Hixie committed
356 357 358
  }
}

359
class _ModalScope extends StatefulWidget {
360 361 362
  _ModalScope({
    Key key,
    this.route
363
  }) : super(key: key);
364

365
  final ModalRoute<dynamic> route;
366

367
  @override
368 369 370 371
  _ModalScopeState createState() => new _ModalScopeState();
}

class _ModalScopeState extends State<_ModalScope> {
372
  @override
373 374
  void initState() {
    super.initState();
375 376
    config.route.animation?.addStatusListener(_animationStatusChanged);
    config.route.forwardAnimation?.addStatusListener(_animationStatusChanged);
377 378
  }

379
  @override
380
  void didUpdateConfig(_ModalScope oldConfig) {
381
    assert(config.route == oldConfig.route);
382 383
  }

384
  @override
385
  void dispose() {
386 387
    config.route.animation?.removeStatusListener(_animationStatusChanged);
    config.route.forwardAnimation?.removeStatusListener(_animationStatusChanged);
388 389 390
    super.dispose();
  }

391
  void _animationStatusChanged(AnimationStatus status) {
392
    setState(() {
393
      // The animation's states are our build state, and they changed already.
394 395 396
    });
  }

397
  @override
398 399
  Widget build(BuildContext context) {
    Widget contents = new PageStorage(
400 401
      key: config.route._subtreeKey,
      bucket: config.route._storageBucket,
Hixie's avatar
Hixie committed
402
      child: new _ModalScopeStatus(
403
        route: config.route,
404
        isCurrent: config.route.isCurrent,
405
        child: config.route.buildPage(context, config.route.animation, config.route.forwardAnimation)
Hixie's avatar
Hixie committed
406
      )
407
    );
408
    if (config.route.offstage) {
409 410
      contents = new OffStage(child: contents);
    } else {
411 412 413 414 415 416 417
      contents = new IgnorePointer(
        ignoring: config.route.animation?.status == AnimationStatus.reverse,
        child: config.route.buildTransitions(
          context,
          config.route.animation,
          config.route.forwardAnimation,
          contents
418 419 420
        )
      );
    }
421 422 423 424
    contents = new Focus(
      key: new GlobalObjectKey(config.route),
      child: new RepaintBoundary(child: contents)
    );
425
    ModalPosition position = config.route.getPosition(context);
426 427 428 429 430 431 432 433 434 435 436 437
    if (position == null)
      return contents;
    return new Positioned(
      top: position.top,
      right: position.right,
      bottom: position.bottom,
      left: position.left,
      child: contents
    );
  }
}

438
/// Where a [ModalRoute] should be positioned within the [Navigator]'s [Overlay].
439 440
class ModalPosition {
  const ModalPosition({ this.top, this.right, this.bottom, this.left });
441 442

  /// The offset of the route's top edge from the top of the overlay.
443
  final double top;
444 445

  /// The offset of the route's right edge from the right of the overlay.
446
  final double right;
447 448

  /// The offset of the route's bottom edge from the bottom of the overlay.
449
  final double bottom;
450 451

  /// The offset of the route's left edge from the left of the overlay.
452 453 454
  final double left;
}

455
/// A route that blocks interaction with previous routes.
456
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
457
  ModalRoute({
Hixie's avatar
Hixie committed
458
    Completer<T> completer,
459
    this.settings: const RouteSettings()
460
  }) : super.explicit(completer, null);
461

Hixie's avatar
Hixie committed
462 463
  // The API for general users of this class

464 465 466
  /// The settings for this route.
  ///
  /// See [RouteSettings] for details.
467
  final RouteSettings settings;
468

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

477 478 479

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

480 481 482
  /// Override to provide a position for this route within the [Navigator]'s [Overlay].
  ///
  /// By default, the route expands to fill the entire overlay.
483
  ModalPosition getPosition(BuildContext context) => null;
484 485 486 487 488 489 490 491 492 493

  /// Override this function to build the primary content of this route.
  ///
  /// * [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.
494
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation);
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509

  /// Override this function to wrap the route in a number of transition widgets.
  ///
  /// 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.
510
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation, Widget child) {
511 512 513
    return child;
  }

514
  @override
515 516 517 518
  void didPush() {
    Focus.moveScopeTo(new GlobalObjectKey(this), context: navigator.context);
    super.didPush();
  }
Hixie's avatar
Hixie committed
519

520 521
  // The API for subclasses to override - used by this class

Hixie's avatar
Hixie committed
522 523
  /// Whether you can dismiss this route by tapping the modal barrier.
  bool get barrierDismissable;
524

Hixie's avatar
Hixie committed
525 526 527
  /// The color to use for the modal barrier. If this is null, the barrier will
  /// be transparent.
  Color get barrierColor;
528 529 530 531


  // The API for _ModalScope and HeroController

532 533 534 535 536 537 538
  /// 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.
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
  bool get offstage => _offstage;
  bool _offstage = false;
  void set offstage (bool value) {
    if (_offstage == value)
      return;
    _offstage = value;
    _scopeKey.currentState?.setState(() {
      // _offstage is the value we're setting, but since there might not be a
      // state, we set it outside of this callback (which will only be called if
      // there's a state currently built).
      // _scopeKey is the key for the _ModalScope built in _buildModalScope().
      // When we mark that state dirty, it'll rebuild itself, and use our
      // offstage (via their config.route.offstage) when building.
    });
  }

555
  /// The build context for the subtree containing the primary content of this route.
556 557 558 559 560
  BuildContext get subtreeContext => _subtreeKey.currentContext;


  // Internals

561
  final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>();
562 563 564 565
  final GlobalKey _subtreeKey = new GlobalKey();
  final PageStorageBucket _storageBucket = new PageStorageBucket();

  Widget _buildModalBarrier(BuildContext context) {
566
    Widget barrier;
Hixie's avatar
Hixie committed
567 568
    if (barrierColor != null) {
      assert(barrierColor != _kTransparent);
569
      Animation<Color> color = new ColorTween(
570 571 572 573 574 575
        begin: _kTransparent,
        end: barrierColor
      ).animate(new CurvedAnimation(
        parent: animation,
        curve: Curves.ease
      ));
576
      barrier = new AnimatedModalBarrier(
577
        color: color,
Hixie's avatar
Hixie committed
578 579 580
        dismissable: barrierDismissable
      );
    } else {
581
      barrier = new ModalBarrier(dismissable: barrierDismissable);
Hixie's avatar
Hixie committed
582
    }
583
    assert(animation.status != AnimationStatus.dismissed);
584
    return new IgnorePointer(
585
      ignoring: animation.status == AnimationStatus.reverse,
586 587
      child: barrier
    );
588 589 590 591 592 593
  }

  Widget _buildModalScope(BuildContext context) {
    return new _ModalScope(
      key: _scopeKey,
      route: this
594
      // calls buildTransitions() and buildPage(), defined above
595 596 597
    );
  }

598
  @override
599 600 601 602 603
  List<WidgetBuilder> get builders => <WidgetBuilder>[
    _buildModalBarrier,
    _buildModalScope
  ];

604
  @override
605
  String toString() => '$runtimeType($settings, animation: $_animation)';
606
}
Hixie's avatar
Hixie committed
607 608 609 610

/// A modal route that overlays a widget over the current route.
abstract class PopupRoute<T> extends ModalRoute<T> {
  PopupRoute({ Completer<T> completer }) : super(completer: completer);
611 612

  @override
Hixie's avatar
Hixie committed
613
  bool get opaque => false;
614 615

  @override
616 617
  void didChangeNext(Route<dynamic> nextRoute) {
    assert(nextRoute is! PageRoute<dynamic>);
Hixie's avatar
Hixie committed
618
    super.didChangeNext(nextRoute);
619
  }
Hixie's avatar
Hixie committed
620
}