routes.dart 65.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Adam Barth's avatar
Adam Barth committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6
import 'dart:ui' as ui;
7

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

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

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

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

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

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

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
40
  void install() {
41
    assert(_overlayEntries.isEmpty);
42
    _overlayEntries.addAll(createOverlayEntries());
43
    super.install();
Adam Barth's avatar
Adam Barth committed
44 45
  }

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

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

67 68
  @override
  void dispose() {
Adam Barth's avatar
Adam Barth committed
69
    _overlayEntries.clear();
70
    super.dispose();
71
  }
Adam Barth's avatar
Adam Barth committed
72 73
}

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

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

90 91 92 93 94 95
  /// The duration the transition going forwards.
  ///
  /// See also:
  ///
  /// * [reverseTransitionDuration], which controls the duration of the
  /// transition when it is in reverse.
Adam Barth's avatar
Adam Barth committed
96
  Duration get transitionDuration;
97

98 99 100 101 102 103
  /// The duration the transition going in reverse.
  ///
  /// By default, the reverse transition duration is set to the value of
  /// the forwards [transitionDuration].
  Duration get reverseTransitionDuration => transitionDuration;

104 105 106 107
  /// 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
108 109
  bool get opaque;

110 111 112 113
  // This ensures that if we got to the dismissed state while still current,
  // we will still be disposed when we are eventually popped.
  //
  // This situation arises when dealing with the Cupertino dismiss gesture.
114
  @override
115
  bool get finishedWhenPopped => _controller.status == AnimationStatus.dismissed;
116

117 118
  /// The animation that drives the route's transition and the previous route's
  /// forward transition.
119 120
  Animation<double> get animation => _animation;
  Animation<double> _animation;
121

122 123 124
  /// The animation controller that the route uses to drive the transitions.
  ///
  /// The animation itself is exposed by the [animation] property.
125 126
  @protected
  AnimationController get controller => _controller;
127
  AnimationController _controller;
128

129 130 131 132 133 134
  /// 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);

135
  /// Called to create the animation controller that will drive the transitions to
136 137
  /// this route from the previous one, and back to the previous route from this
  /// one.
138
  AnimationController createAnimationController() {
139
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
140
    final Duration duration = transitionDuration;
141
    final Duration reverseDuration = reverseTransitionDuration;
142
    assert(duration != null && duration >= Duration.zero);
143
    return AnimationController(
144
      duration: duration,
145
      reverseDuration: reverseDuration,
146 147 148
      debugLabel: debugLabel,
      vsync: navigator,
    );
Adam Barth's avatar
Adam Barth committed
149 150
  }

151 152
  /// Called to create the animation that exposes the current progress of
  /// the transition controlled by the animation controller created by
153
  /// [createAnimationController()].
154
  Animation<double> createAnimation() {
155
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
156 157
    assert(_controller != null);
    return _controller.view;
158 159
  }

Hixie's avatar
Hixie committed
160
  T _result;
Adam Barth's avatar
Adam Barth committed
161

162
  void _handleStatusChanged(AnimationStatus status) {
Adam Barth's avatar
Adam Barth committed
163
    switch (status) {
164
      case AnimationStatus.completed:
Adam Barth's avatar
Adam Barth committed
165 166 167
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
168 169
      case AnimationStatus.forward:
      case AnimationStatus.reverse:
Adam Barth's avatar
Adam Barth committed
170 171 172
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
173
      case AnimationStatus.dismissed:
174
        // We might still be an active route if a subclass is controlling the
175 176
        // the transition and hits the dismissed status. For example, the iOS
        // back gesture drives this animation to the dismissed status before
177 178
        // removing the route and disposing it.
        if (!isActive) {
179 180 181
          navigator.finalizeRoute(this);
          assert(overlayEntries.isEmpty);
        }
Adam Barth's avatar
Adam Barth committed
182 183
        break;
    }
184
    changedInternalState();
Adam Barth's avatar
Adam Barth committed
185 186
  }

187
  @override
188
  void install() {
189
    assert(!_transitionCompleter.isCompleted, 'Cannot install a $runtimeType after disposing it.');
190
    _controller = createAnimationController();
191
    assert(_controller != null, '$runtimeType.createAnimationController() returned null.');
192
    _animation = createAnimation();
193
    assert(_animation != null, '$runtimeType.createAnimation() returned null.');
194
    super.install();
195 196
  }

197
  @override
198
  TickerFuture didPush() {
199 200
    assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
201
    _didPushOrReplace();
202
    super.didPush();
203
    return _controller.forward();
Adam Barth's avatar
Adam Barth committed
204 205
  }

206 207 208 209 210 211 212 213 214
  @override
  void didAdd() {
    assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
    _didPushOrReplace();
    super.didAdd();
    _controller.value = _controller.upperBound;
  }

215
  @override
216
  void didReplace(Route<dynamic> oldRoute) {
217 218
    assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
219
    if (oldRoute is TransitionRoute)
220
      _controller.value = oldRoute._controller.value;
221
    _didPushOrReplace();
222 223 224
    super.didReplace(oldRoute);
  }

225 226 227 228 229 230 231 232 233
  void _didPushOrReplace() {
    _animation.addStatusListener(_handleStatusChanged);
    // If the animation is already completed, _handleStatusChanged will not get
    // a chance to set opaqueness of OverlayEntry.
    if (_animation.isCompleted && overlayEntries.isNotEmpty) {
      overlayEntries.first.opaque = opaque;
    }
  }

234
  @override
Hixie's avatar
Hixie committed
235
  bool didPop(T result) {
236 237
    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
238
    _result = result;
239
    _controller.reverse();
240
    return super.didPop(result);
Hixie's avatar
Hixie committed
241 242
  }

243
  @override
244
  void didPopNext(Route<dynamic> nextRoute) {
245 246
    assert(_controller != null, '$runtimeType.didPopNext called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
247
    _updateSecondaryAnimation(nextRoute);
Hixie's avatar
Hixie committed
248
    super.didPopNext(nextRoute);
Adam Barth's avatar
Adam Barth committed
249 250
  }

251
  @override
252
  void didChangeNext(Route<dynamic> nextRoute) {
253 254
    assert(_controller != null, '$runtimeType.didChangeNext called before calling install() or after calling dispose().');
    assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
255
    _updateSecondaryAnimation(nextRoute);
Hixie's avatar
Hixie committed
256
    super.didChangeNext(nextRoute);
257 258
  }

259 260 261 262 263 264 265
  // A callback method that disposes existing train hopping animation and
  // removes its listener.
  //
  // This property is non-null if there is a train hopping in progress, and the
  // caller must reset this property to null after it is called.
  VoidCallback _trainHoppingListenerRemover;

266
  void _updateSecondaryAnimation(Route<dynamic> nextRoute) {
267 268 269 270 271 272
    // There is an existing train hopping in progress. Unfortunately, we cannot
    // dispose current train hopping animation until we replace it with a new
    // animation.
    final VoidCallback previousTrainHoppingListenerRemover = _trainHoppingListenerRemover;
    _trainHoppingListenerRemover = null;

273
    if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
274
      final Animation<double> current = _secondaryAnimation.parent;
Hixie's avatar
Hixie committed
275
      if (current != null) {
276 277
        final Animation<double> currentTrain = current is TrainHoppingAnimation ? current.currentTrain : current;
        final Animation<double> nextTrain = nextRoute._animation;
278 279 280 281 282
        if (
          currentTrain.value == nextTrain.value ||
          nextTrain.status == AnimationStatus.completed ||
          nextTrain.status == AnimationStatus.dismissed
        ) {
283 284
          _setSecondaryAnimation(nextTrain, nextRoute.completed);
        } else {
285 286 287 288 289 290 291 292 293 294
          // Two trains animate at different values. We have to do train hopping.
          // There are three possibilities of train hopping:
          //  1. We hop on the nextTrain when two trains meet in the middle using
          //     TrainHoppingAnimation.
          //  2. There is no chance to hop on nextTrain because two trains never
          //     cross each other. We have to directly set the animation to
          //     nextTrain once the nextTrain stops animating.
          //  3. A new _updateSecondaryAnimation is called before train hopping
          //     finishes. We leave a listener remover for the next call to
          //     properly clean up the existing train hopping.
295
          TrainHoppingAnimation newAnimation;
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
          void _jumpOnAnimationEnd(AnimationStatus status) {
            switch (status) {
              case AnimationStatus.completed:
              case AnimationStatus.dismissed:
                // The nextTrain has stopped animating without train hopping.
                // Directly sets the secondary animation and disposes the
                // TrainHoppingAnimation.
                _setSecondaryAnimation(nextTrain, nextRoute.completed);
                if (_trainHoppingListenerRemover != null) {
                  _trainHoppingListenerRemover();
                  _trainHoppingListenerRemover = null;
                }
                break;
              case AnimationStatus.forward:
              case AnimationStatus.reverse:
                break;
            }
          }
          _trainHoppingListenerRemover = () {
            nextTrain.removeStatusListener(_jumpOnAnimationEnd);
            newAnimation?.dispose();
          };
          nextTrain.addStatusListener(_jumpOnAnimationEnd);
319
          newAnimation = TrainHoppingAnimation(
320 321
            currentTrain,
            nextTrain,
Hixie's avatar
Hixie committed
322
            onSwitchedTrain: () {
323
              assert(_secondaryAnimation.parent == newAnimation);
324
              assert(newAnimation.currentTrain == nextRoute._animation);
325 326
              // We can hop on the nextTrain, so we don't need to listen to
              // whether the nextTrain has stopped.
327
              _setSecondaryAnimation(newAnimation.currentTrain, nextRoute.completed);
328 329 330 331
              if (_trainHoppingListenerRemover != null) {
                _trainHoppingListenerRemover();
                _trainHoppingListenerRemover = null;
              }
332
            },
Hixie's avatar
Hixie committed
333
          );
334 335
          _setSecondaryAnimation(newAnimation, nextRoute.completed);
        }
Hixie's avatar
Hixie committed
336
      } else {
337
        _setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
Hixie's avatar
Hixie committed
338
      }
Hixie's avatar
Hixie committed
339
    } else {
340
      _setSecondaryAnimation(kAlwaysDismissedAnimation);
Hixie's avatar
Hixie committed
341
    }
342 343 344 345 346
    // Finally, we dispose any previous train hopping animation because it
    // has been successfully updated at this point.
    if (previousTrainHoppingListenerRemover != null) {
      previousTrainHoppingListenerRemover();
    }
Hixie's avatar
Hixie committed
347 348
  }

349 350
  void _setSecondaryAnimation(Animation<double> animation, [Future<dynamic> disposed]) {
    _secondaryAnimation.parent = animation;
351
    // Releases the reference to the next route's animation when that route
352 353 354 355 356 357 358 359 360 361 362
    // is disposed.
    disposed?.then((dynamic _) {
      if (_secondaryAnimation.parent == animation) {
        _secondaryAnimation.parent = kAlwaysDismissedAnimation;
        if (animation is TrainHoppingAnimation) {
          animation.dispose();
        }
      }
    });
  }

363 364 365
  /// 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.
366
  ///
367
  /// Subclasses can override this method to restrict the set of routes they
368
  /// need to coordinate transitions with.
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
  ///
  /// 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.
387
  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true;
388

389 390
  /// 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.
391
  ///
392
  /// Subclasses can override this method to restrict the set of routes they
393
  /// need to coordinate transitions with.
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
  ///
  /// 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.
412
  bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => true;
413

414
  @override
Hixie's avatar
Hixie committed
415
  void dispose() {
416 417
    assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.');
    _controller?.dispose();
418
    _transitionCompleter.complete(_result);
Hixie's avatar
Hixie committed
419 420 421
    super.dispose();
  }

422
  /// A short description of this route useful for debugging.
423
  String get debugLabel => objectRuntimeType(this, 'TransitionRoute');
424 425

  @override
426
  String toString() => '${objectRuntimeType(this, 'TransitionRoute')}(animation: $_controller)';
Adam Barth's avatar
Adam Barth committed
427
}
428

429
/// An entry in the history of a [LocalHistoryRoute].
Hixie's avatar
Hixie committed
430
class LocalHistoryEntry {
431
  /// Creates an entry in the history of a [LocalHistoryRoute].
Hixie's avatar
Hixie committed
432
  LocalHistoryEntry({ this.onRemove });
433 434

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

437
  LocalHistoryRoute<dynamic> _owner;
438 439

  /// Remove this entry from the history of its associated [LocalHistoryRoute].
Hixie's avatar
Hixie committed
440 441
  void remove() {
    _owner.removeLocalHistoryEntry(this);
442
    assert(_owner == null);
Hixie's avatar
Hixie committed
443
  }
444

Hixie's avatar
Hixie committed
445 446 447 448 449 450
  void _notifyRemoved() {
    if (onRemove != null)
      onRemove();
  }
}

451
/// A mixin used by routes to handle back navigations internally by popping a list.
452 453
///
/// When a [Navigator] is instructed to pop, the current route is given an
454
/// opportunity to handle the pop internally. A `LocalHistoryRoute` handles the
455 456
/// pop internally if its list of local history entries is non-empty. Rather
/// than being removed as the current route, the most recent [LocalHistoryEntry]
457
/// is removed from the list and its [LocalHistoryEntry.onRemove] is called.
458
mixin LocalHistoryRoute<T> on Route<T> {
Hixie's avatar
Hixie committed
459
  List<LocalHistoryEntry> _localHistory;
460 461 462

  /// Adds a local history entry to this route.
  ///
463
  /// When asked to pop, if this route has any local history entries, this route
464 465 466 467 468
  /// 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.
469
  ///
470
  /// {@tool snippet}
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486
  ///
  /// 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) {
487
  ///     return MaterialApp(
488 489
  ///       initialRoute: '/',
  ///       routes: {
490 491
  ///         '/': (BuildContext context) => HomePage(),
  ///         '/second_page': (BuildContext context) => SecondPage(),
492 493 494 495 496 497 498 499 500
  ///       },
  ///     );
  ///   }
  /// }
  ///
  /// class HomePage extends StatefulWidget {
  ///   HomePage();
  ///
  ///   @override
501
  ///   _HomePageState createState() => _HomePageState();
502 503 504 505 506
  /// }
  ///
  /// class _HomePageState extends State<HomePage> {
  ///   @override
  ///   Widget build(BuildContext context) {
507 508
  ///     return Scaffold(
  ///       body: Center(
509 510 511
  ///         child: Column(
  ///           mainAxisSize: MainAxisSize.min,
  ///           children: <Widget>[
512
  ///             Text('HomePage'),
513
  ///             // Press this button to open the SecondPage.
514 515
  ///             RaisedButton(
  ///               child: Text('Second Page >'),
516 517 518 519 520 521 522 523 524 525 526 527 528
  ///               onPressed: () {
  ///                 Navigator.pushNamed(context, '/second_page');
  ///               },
  ///             ),
  ///           ],
  ///         ),
  ///       ),
  ///     );
  ///   }
  /// }
  ///
  /// class SecondPage extends StatefulWidget {
  ///   @override
529
  ///   _SecondPageState createState() => _SecondPageState();
530 531 532 533 534 535 536 537 538 539 540 541
  /// }
  ///
  /// 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(
542
  ///         LocalHistoryEntry(
543 544 545 546 547 548 549 550 551 552 553
  ///             onRemove: () {
  ///               // Hide the red rectangle.
  ///               setState(() => _showRectangle = false);
  ///             }
  ///         )
  ///     );
  ///   }
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
  ///     final localNavContent = _showRectangle
554
  ///       ? Container(
555 556 557 558
  ///           width: 100.0,
  ///           height: 100.0,
  ///           color: Colors.red,
  ///         )
559 560
  ///       : RaisedButton(
  ///           child: Text('Show Rectangle'),
561 562 563
  ///           onPressed: _navigateLocallyToShowRectangle,
  ///         );
  ///
564
  ///     return Scaffold(
565
  ///       body: Center(
566
  ///         child: Column(
567 568 569
  ///           mainAxisAlignment: MainAxisAlignment.center,
  ///           children: <Widget>[
  ///             localNavContent,
570 571
  ///             RaisedButton(
  ///               child: Text('< Back'),
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
  ///               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();
  ///               },
  ///             ),
  ///           ],
  ///         ),
  ///       ),
  ///     );
  ///   }
  /// }
  /// ```
587
  /// {@end-tool}
Hixie's avatar
Hixie committed
588 589 590 591
  void addLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry._owner == null);
    entry._owner = this;
    _localHistory ??= <LocalHistoryEntry>[];
592
    final bool wasEmpty = _localHistory.isEmpty;
Hixie's avatar
Hixie committed
593
    _localHistory.add(entry);
594 595
    if (wasEmpty)
      changedInternalState();
Hixie's avatar
Hixie committed
596
  }
597 598 599

  /// Remove a local history entry from this route.
  ///
600 601
  /// The entry's [LocalHistoryEntry.onRemove] callback, if any, will be called
  /// synchronously.
Hixie's avatar
Hixie committed
602 603 604 605 606 607 608
  void removeLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry != null);
    assert(entry._owner == this);
    assert(_localHistory.contains(entry));
    _localHistory.remove(entry);
    entry._owner = null;
    entry._notifyRemoved();
609 610
    if (_localHistory.isEmpty)
      changedInternalState();
Hixie's avatar
Hixie committed
611
  }
612

613
  @override
614 615 616
  Future<RoutePopDisposition> willPop() async {
    if (willHandlePopInternally)
      return RoutePopDisposition.pop;
617
    return super.willPop();
618 619
  }

620
  @override
Hixie's avatar
Hixie committed
621
  bool didPop(T result) {
622
    if (_localHistory != null && _localHistory.isNotEmpty) {
623
      final LocalHistoryEntry entry = _localHistory.removeLast();
Hixie's avatar
Hixie committed
624 625 626
      assert(entry._owner == this);
      entry._owner = null;
      entry._notifyRemoved();
627 628
      if (_localHistory.isEmpty)
        changedInternalState();
Hixie's avatar
Hixie committed
629 630 631 632
      return false;
    }
    return super.didPop(result);
  }
633 634

  @override
Hixie's avatar
Hixie committed
635
  bool get willHandlePopInternally {
636
    return _localHistory != null && _localHistory.isNotEmpty;
Hixie's avatar
Hixie committed
637
  }
Hixie's avatar
Hixie committed
638 639
}

Hixie's avatar
Hixie committed
640
class _ModalScopeStatus extends InheritedWidget {
641
  const _ModalScopeStatus({
Hixie's avatar
Hixie committed
642
    Key key,
643 644 645
    @required this.isCurrent,
    @required this.canPop,
    @required this.route,
646
    @required Widget child,
647 648 649 650 651
  }) : assert(isCurrent != null),
       assert(canPop != null),
       assert(route != null),
       assert(child != null),
       super(key: key, child: child);
Hixie's avatar
Hixie committed
652

653
  final bool isCurrent;
654
  final bool canPop;
655
  final Route<dynamic> route;
Hixie's avatar
Hixie committed
656

657
  @override
Hixie's avatar
Hixie committed
658
  bool updateShouldNotify(_ModalScopeStatus old) {
659
    return isCurrent != old.isCurrent ||
660
           canPop != old.canPop ||
Hixie's avatar
Hixie committed
661 662 663
           route != old.route;
  }

664
  @override
665
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
666
    super.debugFillProperties(description);
667 668
    description.add(FlagProperty('isCurrent', value: isCurrent, ifTrue: 'active', ifFalse: 'inactive'));
    description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop'));
Hixie's avatar
Hixie committed
669 670 671
  }
}

672
class _ModalScope<T> extends StatefulWidget {
673
  const _ModalScope({
674
    Key key,
675
    this.route,
676
  }) : super(key: key);
677

678
  final ModalRoute<T> route;
679

680
  @override
681
  _ModalScopeState<T> createState() => _ModalScopeState<T>();
682 683
}

684 685 686 687 688 689 690 691 692
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;
693

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

697
  @override
698 699
  void initState() {
    super.initState();
700 701 702 703
    final List<Listenable> animations = <Listenable>[
      if (widget.route.animation != null) widget.route.animation,
      if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation,
    ];
704
    _listenable = Listenable.merge(animations);
705 706 707
    if (widget.route.isCurrent) {
      widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
    }
708 709
  }

710
  @override
711
  void didUpdateWidget(_ModalScope<T> oldWidget) {
712
    super.didUpdateWidget(oldWidget);
713
    assert(widget.route == oldWidget.route);
714 715 716
    if (widget.route.isCurrent) {
      widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
    }
717 718
  }

719
  @override
720 721 722
  void didChangeDependencies() {
    super.didChangeDependencies();
    _page = null;
723 724
  }

725
  void _forceRebuildPage() {
726
    setState(() {
727
      _page = null;
728 729 730
    });
  }

731 732 733 734 735 736
  @override
  void dispose() {
    focusScopeNode.dispose();
    super.dispose();
  }

737 738 739 740 741
  bool get _shouldIgnoreFocusRequest {
    return widget.route.animation?.status == AnimationStatus.reverse ||
      (widget.route.navigator?.userGestureInProgress ?? false);
  }

742 743
  // This should be called to wrap any changes to route.isCurrent, route.canPop,
  // and route.offstage.
744
  void _routeSetState(VoidCallback fn) {
745
    if (widget.route.isCurrent && !_shouldIgnoreFocusRequest) {
746 747
      widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
    }
748
    setState(fn);
749 750
  }

751
  @override
752
  Widget build(BuildContext context) {
753
    return _ModalScopeStatus(
754 755 756
      route: widget.route,
      isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
      canPop: widget.route.canPop, // _routeSetState is called if this updates
757
      child: Offstage(
758
        offstage: widget.route.offstage, // _routeSetState is called if this updates
759
        child: PageStorage(
760
          bucket: widget.route._storageBucket, // immutable
761
          child: FocusScope(
762
            node: focusScopeNode, // immutable
763 764
            child: RepaintBoundary(
              child: AnimatedBuilder(
765 766 767 768 769 770
                animation: _listenable, // immutable
                builder: (BuildContext context, Widget child) {
                  return widget.route.buildTransitions(
                    context,
                    widget.route.animation,
                    widget.route.secondaryAnimation,
771 772
                    // This additional AnimatedBuilder is include because if the
                    // value of the userGestureInProgressNotifier changes, it's
773 774
                    // only necessary to rebuild the IgnorePointer widget and set
                    // the focus node's ability to focus.
775
                    AnimatedBuilder(
776
                      animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false),
777
                      builder: (BuildContext context, Widget child) {
778
                        final bool ignoreEvents = _shouldIgnoreFocusRequest;
779
                        focusScopeNode.canRequestFocus = !ignoreEvents;
780
                        return IgnorePointer(
781
                          ignoring: ignoreEvents,
782 783 784
                          child: child,
                        );
                      },
785 786 787 788
                      child: child,
                    ),
                  );
                },
789
                child: _page ??= RepaintBoundary(
790
                  key: widget.route._subtreeKey, // immutable
791
                  child: Builder(
792 793 794 795 796 797 798
                    builder: (BuildContext context) {
                      return widget.route.buildPage(
                        context,
                        widget.route.animation,
                        widget.route.secondaryAnimation,
                      );
                    },
799
                  ),
800 801 802 803 804 805
                ),
              ),
            ),
          ),
        ),
      ),
806
    );
807 808 809
  }
}

810
/// A route that blocks interaction with previous routes.
811
///
812 813 814 815 816 817
/// [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.
818
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
819
  /// Creates a route that blocks interaction with previous routes.
820
  ModalRoute({
821
    RouteSettings settings,
822 823 824 825 826 827 828 829 830
    ui.ImageFilter filter,
  }) : _filter = filter,
       super(settings: settings);

  /// The filter to add to the barrier.
  ///
  /// If given, this filter will be applied to the modal barrier using
  /// [BackdropFilter]. This allows blur effects, for example.
  final ui.ImageFilter _filter;
831

Hixie's avatar
Hixie committed
832 833
  // The API for general users of this class

834 835
  /// Returns the modal route most closely associated with the given context.
  ///
836
  /// Returns null if the given context is not associated with a modal route.
837 838 839 840
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
841
  /// ModalRoute route = ModalRoute.of(context);
842
  /// ```
843 844
  ///
  /// The given [BuildContext] will be rebuilt if the state of the route changes
845
  /// (specifically, if [isCurrent] or [canPop] change value).
846
  @optionalTypeArgs
847
  static ModalRoute<T> of<T extends Object>(BuildContext context) {
848
    final _ModalScopeStatus widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
849
    return widget?.route as ModalRoute<T>;
Hixie's avatar
Hixie committed
850 851
  }

852 853
  /// Schedule a call to [buildTransitions].
  ///
854
  /// Whenever you need to change internal state for a [ModalRoute] object, make
855
  /// the change in a function that you pass to [setState], as in:
856 857 858 859 860
  ///
  /// ```dart
  /// setState(() { myState = newValue });
  /// ```
  ///
861
  /// If you just change the state directly without calling [setState], then the
862 863 864 865 866 867 868 869 870 871 872 873 874 875
  /// 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
876 877 878 879 880 881 882 883
  /// 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
884 885
          && route is ModalRoute
          && route.settings.name == name;
Hans Muller's avatar
Hans Muller committed
886 887
    };
  }
888 889 890

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

891
  /// Override this method to build the primary content of this route.
892
  ///
893 894 895 896 897 898 899 900 901 902
  /// 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.
  ///
903 904 905 906 907 908 909 910
  /// 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.
911 912 913 914 915
  ///
  /// 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.
916
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
917

918 919
  /// Override this method to wrap the [child] with one or more transition
  /// widgets that define how the route arrives on and leaves the screen.
920
  ///
921 922 923 924 925
  /// 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]).
926
  ///
927
  /// The [buildTransitions] method is typically used to define transitions
928 929 930 931 932 933 934 935 936 937 938 939 940
  /// 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
941
  /// PageRouteBuilder(
942 943 944 945 946
  ///   pageBuilder: (BuildContext context,
  ///       Animation<double> animation,
  ///       Animation<double> secondaryAnimation,
  ///       Widget child,
  ///   ) {
947 948 949 950
  ///     return Scaffold(
  ///       appBar: AppBar(title: Text('Hello')),
  ///       body: Center(
  ///         child: Text('Hello World'),
951 952 953 954 955 956 957 958 959
  ///       ),
  ///     );
  ///   },
  ///   transitionsBuilder: (
  ///       BuildContext context,
  ///       Animation<double> animation,
  ///       Animation<double> secondaryAnimation,
  ///       Widget child,
  ///    ) {
960 961
  ///     return SlideTransition(
  ///       position: Tween<Offset>(
962 963
  ///         begin: const Offset(0.0, 1.0),
  ///         end: Offset.zero,
964 965 966 967 968
  ///       ).animate(animation),
  ///       child: child, // child is the value returned by pageBuilder
  ///     );
  ///   },
  /// );
969
  /// ```
970
  ///
971 972
  /// We've used [PageRouteBuilder] to demonstrate the [buildTransitions] method
  /// here. The body of an override of the [buildTransitions] method would be
973 974
  /// defined in the same way.
  ///
975
  /// When the [Navigator] pushes a route on the top of its stack, the
976 977 978 979 980
  /// [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
981
  /// runs from 0.0 to 1.0. When the Navigator pops the topmost route, the
982 983 984
  /// secondaryAnimation for the route below it runs from 1.0 to 0.0.
  ///
  /// The example below adds a transition that's driven by the
985
  /// [secondaryAnimation]. When this route disappears because a new route has
986 987 988 989 990 991 992 993 994 995 996
  /// 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,
  ///   ) {
997 998
  ///     return SlideTransition(
  ///       position: AlignmentTween(
999 1000
  ///         begin: const Offset(0.0, 1.0),
  ///         end: Offset.zero,
1001
  ///       ).animate(animation),
1002 1003
  ///       child: SlideTransition(
  ///         position: TweenOffset(
1004 1005
  ///           begin: Offset.zero,
  ///           end: const Offset(0.0, 1.0),
1006 1007 1008 1009 1010
  ///         ).animate(secondaryAnimation),
  ///         child: child,
  ///       ),
  ///     );
  ///   }
1011 1012 1013
  /// ```
  ///
  /// In practice the `secondaryAnimation` is used pretty rarely.
1014
  ///
1015
  /// The arguments to this method are as follows:
1016
  ///
1017 1018 1019
  ///  * `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]
1020
  ///    pops the topmost route this animation runs from 1.0 to 0.0.
1021 1022
  ///  * [secondaryAnimation]: When the Navigator pushes a new route
  ///    on the top of its stack, the old topmost route's [secondaryAnimation]
1023
  ///    runs from 0.0 to 1.0. When the [Navigator] pops the topmost route, the
1024
  ///    [secondaryAnimation] for the route below it runs from 1.0 to 0.0.
1025
  ///  * `child`, the page contents, as returned by [buildPage].
1026 1027 1028 1029 1030
  ///
  /// 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.
1031
  Widget buildTransitions(
1032 1033 1034 1035
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
1036
  ) {
1037 1038 1039
    return child;
  }

1040
  @override
1041 1042
  void install() {
    super.install();
1043 1044
    _animationProxy = ProxyAnimation(super.animation);
    _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
1045 1046
  }

1047
  @override
1048
  TickerFuture didPush() {
1049 1050 1051
    if (_scopeKey.currentState != null) {
      navigator.focusScopeNode.setFirstFocus(_scopeKey.currentState.focusScopeNode);
    }
1052
    return super.didPush();
1053 1054
  }

1055 1056 1057 1058 1059 1060 1061 1062
  @override
  void didAdd() {
    if (_scopeKey.currentState != null) {
      navigator.focusScopeNode.setFirstFocus(_scopeKey.currentState.focusScopeNode);
    }
    super.didAdd();
  }

1063 1064
  // The API for subclasses to override - used by this class

Hixie's avatar
Hixie committed
1065
  /// Whether you can dismiss this route by tapping the modal barrier.
1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078
  ///
  /// 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.
  ///
1079
  /// If this getter would ever start returning a different value,
1080 1081 1082
  /// [changedInternalState] should be invoked so that the change can take
  /// effect.
  ///
1083 1084 1085 1086
  /// See also:
  ///
  ///  * [barrierColor], which controls the color of the scrim for this route.
  ///  * [ModalBarrier], the widget that implements this feature.
1087
  bool get barrierDismissible;
1088

1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
  /// 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
1100
  /// excluded from the semantics tree and tapping on the modal barrier
1101 1102 1103
  /// has no effect.
  bool get semanticsDismissible => true;

Hixie's avatar
Hixie committed
1104 1105
  /// The color to use for the modal barrier. If this is null, the barrier will
  /// be transparent.
1106
  ///
1107 1108 1109 1110 1111 1112 1113
  /// 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.
  ///
1114 1115
  /// The color is ignored, and the barrier made invisible, when [offstage] is
  /// true.
1116 1117 1118 1119
  ///
  /// While the route is animating into position, the color is animated from
  /// transparent to the specified color.
  ///
1120 1121 1122 1123
  /// If this getter would ever start returning a different color,
  /// [changedInternalState] should be invoked so that the change can take
  /// effect.
  ///
1124 1125 1126 1127 1128
  /// See also:
  ///
  ///  * [barrierDismissible], which controls the behavior of the barrier when
  ///    tapped.
  ///  * [ModalBarrier], the widget that implements this feature.
Hixie's avatar
Hixie committed
1129
  Color get barrierColor;
1130

1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142
  /// 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.
  ///
1143
  /// If this getter would ever start returning a different label,
1144 1145 1146
  /// [changedInternalState] should be invoked so that the change can take
  /// effect.
  ///
1147 1148 1149 1150 1151 1152 1153
  /// See also:
  ///
  ///  * [barrierDismissible], which controls the behavior of the barrier when
  ///    tapped.
  ///  * [ModalBarrier], the widget that implements this feature.
  String get barrierLabel;

1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179
  /// The curve that is used for animating the modal barrier in and out.
  ///
  /// 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.
  ///
  /// While the route is animating into position, the color is animated from
  /// transparent to the specified [barrierColor].
  ///
  /// If this getter would ever start returning a different curve,
  /// [changedInternalState] should be invoked so that the change can take
  /// effect.
  ///
  /// It defaults to [Curves.ease].
  ///
  /// See also:
  ///
  ///  * [barrierColor], which determines the color that the modal transitions
  ///    to.
  ///  * [Curves] for a collection of common curves.
  ///  * [AnimatedModalBarrier], the widget that implements this feature.
  Curve get barrierCurve => Curves.ease;

1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
  /// 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.
1191 1192
  bool get maintainState;

1193 1194 1195

  // The API for _ModalScope and HeroController

1196 1197 1198 1199 1200 1201 1202
  /// 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.
1203 1204 1205
  ///
  /// The modal barrier, if any, is not rendered if [offstage] is true (see
  /// [barrierColor]).
1206 1207
  bool get offstage => _offstage;
  bool _offstage = false;
1208
  set offstage(bool value) {
1209 1210
    if (_offstage == value)
      return;
1211 1212 1213
    setState(() {
      _offstage = value;
    });
1214
    _animationProxy.parent = _offstage ? kAlwaysCompleteAnimation : super.animation;
1215
    _secondaryAnimationProxy.parent = _offstage ? kAlwaysDismissedAnimation : super.secondaryAnimation;
1216 1217
  }

1218
  /// The build context for the subtree containing the primary content of this route.
1219 1220
  BuildContext get subtreeContext => _subtreeKey.currentContext;

1221 1222 1223 1224 1225
  @override
  Animation<double> get animation => _animationProxy;
  ProxyAnimation _animationProxy;

  @override
1226 1227
  Animation<double> get secondaryAnimation => _secondaryAnimationProxy;
  ProxyAnimation _secondaryAnimationProxy;
1228

1229 1230
  final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];

Ian Hickson's avatar
Ian Hickson committed
1231 1232 1233
  /// 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]).
1234 1235 1236 1237 1238 1239 1240 1241
  ///
  /// 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:
  ///
1242 1243 1244 1245 1246
  ///  * [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.
1247
  @override
1248
  Future<RoutePopDisposition> willPop() async {
1249
    final _ModalScopeState<T> scope = _scopeKey.currentState;
1250
    assert(scope != null);
1251
    for (final WillPopCallback callback in List<WillPopCallback>.from(_willPopCallbacks)) {
1252
      if (!await callback())
1253
        return RoutePopDisposition.doNotPop;
1254
    }
1255
    return await super.willPop();
1256 1257 1258 1259
  }

  /// Enables this route to veto attempts by the user to dismiss it.
  ///
1260 1261 1262 1263 1264 1265
  /// 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) {
1266
  ///   return WillPopScope(
1267 1268 1269 1270 1271 1272 1273
  ///     onWillPop: askTheUserIfTheyAreSure,
  ///     child: ...,
  ///   );
  /// }
  /// ```
  ///
  /// This callback runs asynchronously and it's possible that it will be called
1274
  /// after its route has been disposed. The callback should check [State.mounted]
1275 1276 1277 1278 1279 1280 1281
  /// 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
1282
  /// [State.didChangeDependencies] callback:
1283 1284
  ///
  /// ```dart
1285 1286
  /// ModalRoute<dynamic> _route;
  ///
1287
  /// @override
1288 1289
  /// void didChangeDependencies() {
  ///  super.didChangeDependencies();
1290 1291 1292
  ///  _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure);
  ///  _route = ModalRoute.of(context);
  ///  _route?.addScopedWillPopCallback(askTheUserIfTheyAreSure);
1293 1294 1295
  /// }
  /// ```
  ///
1296 1297 1298 1299
  /// 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):
1300 1301 1302 1303 1304
  ///
  /// ```dart
  /// @override
  /// void dispose() {
  ///   _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure);
1305
  ///   _route = null;
1306 1307 1308 1309
  ///   super.dispose();
  /// }
  /// ```
  ///
1310 1311
  /// See also:
  ///
1312 1313 1314 1315 1316 1317
  ///  * [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.
1318
  void addScopedWillPopCallback(WillPopCallback callback) {
1319 1320
    assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
    _willPopCallbacks.add(callback);
1321 1322 1323 1324 1325 1326
  }

  /// Remove one of the callbacks run by [willPop].
  ///
  /// See also:
  ///
1327 1328 1329
  ///  * [Form], which provides an `onWillPop` callback that uses this mechanism.
  ///  * [addScopedWillPopCallback], which adds callback to the list
  ///    checked by [willPop].
1330
  void removeScopedWillPopCallback(WillPopCallback callback) {
1331 1332
    assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.');
    _willPopCallbacks.remove(callback);
1333 1334
  }

1335 1336
  /// True if one or more [WillPopCallback] callbacks exist.
  ///
1337 1338 1339 1340
  /// This method is used to disable the horizontal swipe pop gesture supported
  /// by [MaterialPageRoute] for [TargetPlatform.iOS] and
  /// [TargetPlatform.macOS]. If a pop might be vetoed, then the back gesture is
  /// disabled.
1341
  ///
1342 1343 1344 1345
  /// 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.
  ///
1346 1347
  /// See also:
  ///
1348 1349 1350 1351
  ///  * [addScopedWillPopCallback], which adds a callback.
  ///  * [removeScopedWillPopCallback], which removes a callback.
  ///  * [willHandlePopInternally], which reports on another reason why
  ///    a pop might be vetoed.
1352
  @protected
1353
  bool get hasScopedWillPopCallback {
1354 1355 1356 1357 1358 1359 1360
    return _willPopCallbacks.isNotEmpty;
  }

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

1363 1364 1365 1366
  @override
  void changedInternalState() {
    super.changedInternalState();
    setState(() { /* internal state already changed */ });
1367
    _modalBarrier.markNeedsBuild();
1368 1369 1370
  }

  @override
1371 1372 1373 1374
  void changedExternalState() {
    super.changedExternalState();
    if (_scopeKey.currentState != null)
      _scopeKey.currentState._forceRebuildPage();
1375 1376 1377 1378 1379 1380
  }

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

1383 1384
  // Internals

1385 1386 1387
  final GlobalKey<_ModalScopeState<T>> _scopeKey = GlobalKey<_ModalScopeState<T>>();
  final GlobalKey _subtreeKey = GlobalKey();
  final PageStorageBucket _storageBucket = PageStorageBucket();
1388

1389
  // one of the builders
1390
  OverlayEntry _modalBarrier;
1391
  Widget _buildModalBarrier(BuildContext context) {
1392
    Widget barrier;
1393
    if (barrierColor != null && !offstage) { // changedInternalState is called if barrierColor or offstage updates
Hixie's avatar
Hixie committed
1394
      assert(barrierColor != _kTransparent);
1395 1396 1397
      final Animation<Color> color = animation.drive(
        ColorTween(
          begin: _kTransparent,
1398 1399
          end: barrierColor, // changedInternalState is called if barrierColor updates
        ).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates
1400
      );
1401
      barrier = AnimatedModalBarrier(
1402
        color: color,
1403 1404
        dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
        semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
1405
        barrierSemanticsDismissible: semanticsDismissible,
Hixie's avatar
Hixie committed
1406 1407
      );
    } else {
1408
      barrier = ModalBarrier(
1409 1410
        dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
        semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
1411
        barrierSemanticsDismissible: semanticsDismissible,
1412
      );
Hixie's avatar
Hixie committed
1413
    }
1414 1415 1416 1417 1418 1419
    if (_filter != null) {
      barrier = BackdropFilter(
        filter: _filter,
        child: barrier,
      );
    }
1420
    return IgnorePointer(
1421
      ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when animation.status updates
1422 1423
                animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
      child: barrier,
1424
    );
1425 1426
  }

1427 1428 1429 1430
  // 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;

1431
  // one of the builders
1432
  Widget _buildModalScope(BuildContext context) {
1433
    return _modalScopeCache ??= _ModalScope<T>(
1434
      key: _scopeKey,
1435
      route: this,
1436
      // _ModalScope calls buildTransitions() and buildChild(), defined above
1437 1438 1439
    );
  }

1440
  @override
1441
  Iterable<OverlayEntry> createOverlayEntries() sync* {
1442 1443
    yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
    yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
1444
  }
1445

1446
  @override
1447
  String toString() => '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)';
1448
}
Hixie's avatar
Hixie committed
1449 1450 1451

/// A modal route that overlays a widget over the current route.
abstract class PopupRoute<T> extends ModalRoute<T> {
1452 1453 1454
  /// Initializes the [PopupRoute].
  PopupRoute({
    RouteSettings settings,
1455 1456 1457 1458 1459
    ui.ImageFilter filter,
  }) : super(
         filter: filter,
         settings: settings,
       );
1460

1461
  @override
Hixie's avatar
Hixie committed
1462
  bool get opaque => false;
1463

1464 1465
  @override
  bool get maintainState => true;
Hixie's avatar
Hixie committed
1466
}
1467 1468 1469 1470

/// A [Navigator] observer that notifies [RouteAware]s of changes to the
/// state of their [Route].
///
1471 1472 1473
/// [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>`
1474 1475 1476
/// will inform subscribed [RouteAware]s whenever the user navigates away from
/// the current page route to another page route.
///
1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489
/// 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.
1490
///
1491
/// {@tool snippet}
1492
///
1493
/// To make a [StatefulWidget] aware of its current [Route] state, implement
1494
/// [RouteAware] in its [State] and subscribe it to a [RouteObserver]:
1495 1496
///
/// ```dart
1497
/// // Register the RouteObserver as a navigation observer.
1498
/// final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
1499
/// void main() {
1500 1501
///   runApp(MaterialApp(
///     home: Container(),
1502 1503 1504 1505 1506
///     navigatorObservers: [routeObserver],
///   ));
/// }
///
/// class RouteAwareWidget extends StatefulWidget {
1507
///   State<RouteAwareWidget> createState() => RouteAwareWidgetState();
1508 1509 1510
/// }
///
/// // Implement RouteAware in a widget's state and subscribe it to the RouteObserver.
1511
/// class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware {
1512
///
1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526
///   @override
///   void didChangeDependencies() {
///     super.didChangeDependencies();
///     routeObserver.subscribe(this, ModalRoute.of(context));
///   }
///
///   @override
///   void dispose() {
///     routeObserver.unsubscribe(this);
///     super.dispose();
///   }
///
///   @override
///   void didPush() {
1527
///     // Route was pushed onto navigator and is now topmost route.
1528 1529 1530 1531
///   }
///
///   @override
///   void didPopNext() {
1532
///     // Covering route was popped off the navigator.
1533 1534
///   }
///
1535
///   @override
1536
///   Widget build(BuildContext context) => Container();
1537
///
1538 1539
/// }
/// ```
1540
/// {@end-tool}
1541 1542
class RouteObserver<R extends Route<dynamic>> extends NavigatorObserver {
  final Map<R, Set<RouteAware>> _listeners = <R, Set<RouteAware>>{};
1543

1544 1545 1546 1547 1548
  /// 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.
1549
  void subscribe(RouteAware routeAware, R route) {
1550 1551
    assert(routeAware != null);
    assert(route != null);
1552
    final Set<RouteAware> subscribers = _listeners.putIfAbsent(route, () => <RouteAware>{});
1553
    if (subscribers.add(routeAware)) {
1554 1555 1556 1557
      routeAware.didPush();
    }
  }

1558 1559
  /// Unsubscribe [routeAware].
  ///
1560 1561
  /// [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.
1562 1563
  void unsubscribe(RouteAware routeAware) {
    assert(routeAware != null);
1564
    for (final R route in _listeners.keys) {
1565 1566 1567
      final Set<RouteAware> subscribers = _listeners[route];
      subscribers?.remove(routeAware);
    }
1568 1569 1570 1571
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
1572
    if (route is R && previousRoute is R) {
1573 1574 1575
      final List<RouteAware> previousSubscribers = _listeners[previousRoute]?.toList();

      if (previousSubscribers != null) {
1576
        for (final RouteAware routeAware in previousSubscribers) {
1577 1578 1579 1580 1581 1582 1583
          routeAware.didPopNext();
        }
      }

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

      if (subscribers != null) {
1584
        for (final RouteAware routeAware in subscribers) {
1585 1586 1587
          routeAware.didPop();
        }
      }
1588 1589 1590 1591 1592
    }
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
1593
    if (route is R && previousRoute is R) {
1594 1595 1596
      final Set<RouteAware> previousSubscribers = _listeners[previousRoute];

      if (previousSubscribers != null) {
1597
        for (final RouteAware routeAware in previousSubscribers) {
1598 1599 1600
          routeAware.didPushNext();
        }
      }
1601 1602 1603 1604
    }
  }
}

1605 1606 1607 1608
/// 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.
1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622
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() { }
1623
}
1624 1625 1626 1627 1628 1629 1630 1631 1632 1633

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,
1634 1635 1636 1637 1638 1639 1640 1641
  }) : assert(barrierDismissible != null),
       _pageBuilder = pageBuilder,
       _barrierDismissible = barrierDismissible,
       _barrierLabel = barrierLabel,
       _barrierColor = barrierColor,
       _transitionDuration = transitionDuration,
       _transitionBuilder = transitionBuilder,
       super(settings: settings);
1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664

  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) {
1665
    return Semantics(
1666 1667 1668 1669 1670 1671 1672 1673 1674
      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) {
1675 1676
      return FadeTransition(
          opacity: CurvedAnimation(
1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697
            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.
///
1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709
/// 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 `useRootNavigator` argument is used to determine whether to push the
/// dialog to the [Navigator] furthest from or nearest to the given `context`.
/// By default, `useRootNavigator` is `true` and 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)`.
1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729
///
/// 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.
///
1730 1731 1732
/// The `routeSettings` will be used in the construction of the dialog's route.
/// See [RouteSettings] for more details.
///
1733 1734 1735 1736
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
/// See also:
1737
///
1738 1739 1740 1741 1742 1743 1744 1745 1746 1747
///  * [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,
1748
  bool useRootNavigator = true,
1749
  RouteSettings routeSettings,
1750 1751
}) {
  assert(pageBuilder != null);
1752
  assert(useRootNavigator != null);
1753
  assert(!barrierDismissible || barrierLabel != null);
1754
  return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(
1755 1756 1757 1758 1759 1760
    pageBuilder: pageBuilder,
    barrierDismissible: barrierDismissible,
    barrierLabel: barrierLabel,
    barrierColor: barrierColor,
    transitionDuration: transitionDuration,
    transitionBuilder: transitionBuilder,
1761
    settings: routeSettings,
1762 1763 1764 1765 1766 1767 1768
  ));
}

/// 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.
1769
typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
1770 1771 1772 1773 1774

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