1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'dart:ui';
6/// @docImport 'package:flutter/cupertino.dart';
7/// @docImport 'package:flutter/material.dart';
8///
9/// @docImport 'app.dart';
10/// @docImport 'form.dart';
11/// @docImport 'heroes.dart';
12/// @docImport 'pages.dart';
13/// @docImport 'pop_scope.dart';
14/// @docImport 'will_pop_scope.dart';
15library;
16
17import 'dart:async';
18import 'dart:ui' as ui;
19
20import 'package:flutter/foundation.dart';
21import 'package:flutter/rendering.dart';
22import 'package:flutter/scheduler.dart';
23import 'package:flutter/services.dart';
24
25import 'actions.dart';
26import 'basic.dart';
27import 'display_feature_sub_screen.dart';
28import 'focus_manager.dart';
29import 'focus_scope.dart';
30import 'focus_traversal.dart';
31import 'framework.dart';
32import 'inherited_model.dart';
33import 'modal_barrier.dart';
34import 'navigator.dart';
35import 'overlay.dart';
36import 'page_storage.dart';
37import 'primary_scroll_controller.dart';
38import 'restoration.dart';
39import 'scroll_controller.dart';
40import 'transitions.dart';
41
42// Examples can assume:
43// late NavigatorState navigator;
44// late BuildContext context;
45// Future askTheUserIfTheyAreSure() async { return true; }
46// abstract class MyWidget extends StatefulWidget { const MyWidget({super.key}); }
47// late dynamic _myState, newValue;
48// late StateSetter setState;
49
50/// A route that displays widgets in the [Navigator]'s [Overlay].
51///
52/// See also:
53///
54/// * [Route], which documents the meaning of the `T` generic type argument.
55abstract class OverlayRoute<T> extends Route<T> {
56 /// Creates a route that knows how to interact with an [Overlay].
57 OverlayRoute({super.settings, super.requestFocus});
58
59 /// Subclasses should override this getter to return the builders for the overlay.
60 @factory
61 Iterable<OverlayEntry> createOverlayEntries();
62
63 @override
64 List<OverlayEntry> get overlayEntries => _overlayEntries;
65 final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
66
67 @override
68 void install() {
69 assert(_overlayEntries.isEmpty);
70 _overlayEntries.addAll(createOverlayEntries());
71 super.install();
72 }
73
74 /// Controls whether [didPop] calls [NavigatorState.finalizeRoute].
75 ///
76 /// If true, this route removes its overlay entries during [didPop].
77 /// Subclasses can override this getter if they want to delay finalization
78 /// (for example to animate the route's exit before removing it from the
79 /// overlay).
80 ///
81 /// Subclasses that return false from [finishedWhenPopped] are responsible for
82 /// calling [NavigatorState.finalizeRoute] themselves.
83 @protected
84 bool get finishedWhenPopped => true;
85
86 @override
87 bool didPop(T? result) {
88 final bool returnValue = super.didPop(result);
89 assert(returnValue);
90 if (finishedWhenPopped) {
91 navigator!.finalizeRoute(this);
92 }
93 return returnValue;
94 }
95
96 @override
97 void dispose() {
98 for (final OverlayEntry entry in _overlayEntries) {
99 entry.dispose();
100 }
101 _overlayEntries.clear();
102 super.dispose();
103 }
104}
105
106/// A route with entrance and exit transitions.
107///
108/// See also:
109///
110/// * [Route], which documents the meaning of the `T` generic type argument.
111abstract class TransitionRoute<T> extends OverlayRoute<T> implements PredictiveBackRoute {
112 /// Creates a route that animates itself when it is pushed or popped.
113 TransitionRoute({super.settings, super.requestFocus});
114
115 /// This future completes only once the transition itself has finished, after
116 /// the overlay entries have been removed from the navigator's overlay.
117 ///
118 /// This future completes once the animation has been dismissed. That will be
119 /// after [popped], because [popped] typically completes before the animation
120 /// even starts, as soon as the route is popped.
121 Future<T?> get completed => _transitionCompleter.future;
122 final Completer<T?> _transitionCompleter = Completer<T?>();
123
124 /// Handle to the performance mode request.
125 ///
126 /// When the route is animating, the performance mode is requested. It is then
127 /// disposed when the animation ends. Requesting [DartPerformanceMode.latency]
128 /// indicates to the engine that the transition is latency sensitive and to delay
129 /// non-essential work while this handle is active.
130 PerformanceModeRequestHandle? _performanceModeRequestHandle;
131
132 /// {@template flutter.widgets.TransitionRoute.transitionDuration}
133 /// The duration the transition going forwards.
134 ///
135 /// See also:
136 ///
137 /// * [reverseTransitionDuration], which controls the duration of the
138 /// transition when it is in reverse.
139 /// {@endtemplate}
140 Duration get transitionDuration;
141
142 /// {@template flutter.widgets.TransitionRoute.reverseTransitionDuration}
143 /// The duration the transition going in reverse.
144 ///
145 /// By default, the reverse transition duration is set to the value of
146 /// the forwards [transitionDuration].
147 /// {@endtemplate}
148 Duration get reverseTransitionDuration => transitionDuration;
149
150 /// {@template flutter.widgets.TransitionRoute.opaque}
151 /// Whether the route obscures previous routes when the transition is complete.
152 ///
153 /// When an opaque route's entrance transition is complete, the routes behind
154 /// the opaque route will not be built to save resources.
155 /// {@endtemplate}
156 bool get opaque;
157
158 /// {@template flutter.widgets.TransitionRoute.allowSnapshotting}
159 /// Whether the route transition will prefer to animate a snapshot of the
160 /// entering/exiting routes.
161 ///
162 /// When this value is true, certain route transitions (such as the Android
163 /// zoom page transition) will snapshot the entering and exiting routes.
164 /// These snapshots are then animated in place of the underlying widgets to
165 /// improve performance of the transition.
166 ///
167 /// Generally this means that animations that occur on the entering/exiting
168 /// route while the route animation plays may appear frozen - unless they
169 /// are a hero animation or something that is drawn in a separate overlay.
170 /// {@endtemplate}
171 bool get allowSnapshotting => true;
172
173 // This ensures that if we got to the dismissed state while still current,
174 // we will still be disposed when we are eventually popped.
175 //
176 // This situation arises when dealing with the Cupertino dismiss gesture.
177 @override
178 bool get finishedWhenPopped => _controller!.isDismissed && !_popFinalized;
179
180 bool _popFinalized = false;
181
182 /// The animation that drives the route's transition and the previous route's
183 /// forward transition.
184 Animation<double>? get animation => _animation;
185 Animation<double>? _animation;
186
187 /// The animation controller that the route uses to drive the transitions.
188 ///
189 /// The animation itself is exposed by the [animation] property.
190 @protected
191 AnimationController? get controller => _controller;
192 AnimationController? _controller;
193
194 /// The animation for the route being pushed on top of this route. This
195 /// animation lets this route coordinate with the entrance and exit transition
196 /// of route pushed on top of this route.
197 Animation<double>? get secondaryAnimation => _secondaryAnimation;
198 final ProxyAnimation _secondaryAnimation = ProxyAnimation(kAlwaysDismissedAnimation);
199
200 /// Whether to takeover the [controller] created by [createAnimationController].
201 ///
202 /// If true, this route will call [AnimationController.dispose] when the
203 /// controller is no longer needed.
204 /// If false, the controller should be disposed by whoever owned it.
205 ///
206 /// It defaults to `true`.
207 bool willDisposeAnimationController = true;
208
209 /// Returns true if the transition has completed.
210 ///
211 /// It is equivalent to whether the future returned by [completed] has
212 /// completed.
213 ///
214 /// This method only works if assert is enabled. Otherwise it always returns
215 /// false.
216 @protected
217 bool debugTransitionCompleted() {
218 bool disposed = false;
219 assert(() {
220 disposed = _transitionCompleter.isCompleted;
221 return true;
222 }());
223 return disposed;
224 }
225
226 /// Called to create the animation controller that will drive the transitions to
227 /// this route from the previous one, and back to the previous route from this
228 /// one.
229 ///
230 /// The returned controller will be disposed by [AnimationController.dispose]
231 /// if the [willDisposeAnimationController] is `true`.
232 AnimationController createAnimationController() {
233 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
234 final Duration duration = transitionDuration;
235 final Duration reverseDuration = reverseTransitionDuration;
236 return AnimationController(
237 duration: duration,
238 reverseDuration: reverseDuration,
239 debugLabel: debugLabel,
240 vsync: navigator!,
241 );
242 }
243
244 /// Called to create the animation that exposes the current progress of
245 /// the transition controlled by the animation controller created by
246 /// [createAnimationController()].
247 Animation<double> createAnimation() {
248 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
249 assert(_controller != null);
250 return _controller!.view;
251 }
252
253 Simulation? _simulation;
254
255 /// Creates the simulation that drives the transition animation for this route.
256 ///
257 /// By default, this method returns null, indicating that the route doesn't
258 /// use simulations, but initiates the transition by calling either
259 /// [AnimationController.forward] or [AnimationController.reverse] with
260 /// [transitionDuration] and the controller's curve.
261 ///
262 /// Subclasses can override this method to return a non-null [Simulation]. In
263 /// this case, the [controller] will instead use the provided simulation to
264 /// animate the transition using [AnimationController.animateWith] or
265 /// [AnimationController.animateBackWith], and the [Simulation.x] is forwarded
266 /// to the value of [animation]. The [controller]'s curve and
267 /// [transitionDuration] are ignored.
268 ///
269 /// This method is invoked each time the navigator pushes or pops this route.
270 /// The `forward` parameter indicates the direction of the transition: true when
271 /// the route is pushed, and false when it is popped.
272 Simulation? createSimulation({required bool forward}) {
273 assert(
274 transitionDuration >= Duration.zero,
275 'The `duration` must be positive for a non-simulation animation. Received $transitionDuration.',
276 );
277 return null;
278 }
279
280 Simulation? _createSimulationAndVerify({required bool forward}) {
281 final Simulation? simulation = createSimulation(forward: forward);
282 assert(
283 transitionDuration >= Duration.zero,
284 "The `duration` must be positive for an animation that doesn't use simulation. "
285 'Either set `transitionDuration` or set `createSimulation`. '
286 'Received $transitionDuration.',
287 );
288 return simulation;
289 }
290
291 T? _result;
292
293 void _handleStatusChanged(AnimationStatus status) {
294 switch (status) {
295 case AnimationStatus.completed:
296 if (overlayEntries.isNotEmpty) {
297 overlayEntries.first.opaque = opaque;
298 }
299 _performanceModeRequestHandle?.dispose();
300 _performanceModeRequestHandle = null;
301 case AnimationStatus.forward:
302 case AnimationStatus.reverse:
303 if (overlayEntries.isNotEmpty) {
304 overlayEntries.first.opaque = false;
305 }
306 _performanceModeRequestHandle ??= SchedulerBinding.instance.requestPerformanceMode(
307 ui.DartPerformanceMode.latency,
308 );
309 case AnimationStatus.dismissed:
310 // We might still be an active route if a subclass is controlling the
311 // transition and hits the dismissed status. For example, the iOS
312 // back gesture drives this animation to the dismissed status before
313 // removing the route and disposing it.
314 if (!isActive) {
315 navigator!.finalizeRoute(this);
316 _popFinalized = true;
317 _performanceModeRequestHandle?.dispose();
318 _performanceModeRequestHandle = null;
319 }
320 }
321 }
322
323 @override
324 void install() {
325 assert(!debugTransitionCompleted(), 'Cannot install a $runtimeType after disposing it.');
326 _controller = createAnimationController();
327 assert(_controller != null, '$runtimeType.createAnimationController() returned null.');
328 _animation = createAnimation()..addStatusListener(_handleStatusChanged);
329 assert(_animation != null, '$runtimeType.createAnimation() returned null.');
330 super.install();
331 if (_animation!.isCompleted && overlayEntries.isNotEmpty) {
332 overlayEntries.first.opaque = opaque;
333 }
334 }
335
336 @override
337 TickerFuture didPush() {
338 assert(
339 _controller != null,
340 '$runtimeType.didPush called before calling install() or after calling dispose().',
341 );
342 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
343 super.didPush();
344 _simulation = _createSimulationAndVerify(forward: true);
345 if (_simulation == null) {
346 return _controller!.forward();
347 } else {
348 return _controller!.animateWith(_simulation!);
349 }
350 }
351
352 @override
353 void didAdd() {
354 assert(
355 _controller != null,
356 '$runtimeType.didPush called before calling install() or after calling dispose().',
357 );
358 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
359 super.didAdd();
360 _controller!.value = _controller!.upperBound;
361 }
362
363 @override
364 void didReplace(Route<dynamic>? oldRoute) {
365 assert(
366 _controller != null,
367 '$runtimeType.didReplace called before calling install() or after calling dispose().',
368 );
369 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
370 if (oldRoute is TransitionRoute) {
371 _controller!.value = oldRoute._controller!.value;
372 }
373 super.didReplace(oldRoute);
374 }
375
376 @override
377 bool didPop(T? result) {
378 assert(
379 _controller != null,
380 '$runtimeType.didPop called before calling install() or after calling dispose().',
381 );
382 assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
383 _result = result;
384 _simulation = _createSimulationAndVerify(forward: false);
385 if (_simulation == null) {
386 _controller!.reverse();
387 } else {
388 _controller!.animateBackWith(_simulation!);
389 }
390 return super.didPop(result);
391 }
392
393 @override
394 void didPopNext(Route<dynamic> nextRoute) {
395 assert(
396 _controller != null,
397 '$runtimeType.didPopNext called before calling install() or after calling dispose().',
398 );
399 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
400 _updateSecondaryAnimation(nextRoute);
401 super.didPopNext(nextRoute);
402 }
403
404 @override
405 void didChangeNext(Route<dynamic>? nextRoute) {
406 assert(
407 _controller != null,
408 '$runtimeType.didChangeNext called before calling install() or after calling dispose().',
409 );
410 assert(!debugTransitionCompleted(), 'Cannot reuse a $runtimeType after disposing it.');
411 _updateSecondaryAnimation(nextRoute);
412 super.didChangeNext(nextRoute);
413 }
414
415 // A callback method that disposes existing train hopping animation and
416 // removes its listener.
417 //
418 // This property is non-null if there is a train hopping in progress, and the
419 // caller must reset this property to null after it is called.
420 VoidCallback? _trainHoppingListenerRemover;
421
422 void _updateSecondaryAnimation(Route<dynamic>? nextRoute) {
423 // There is an existing train hopping in progress. Unfortunately, we cannot
424 // dispose current train hopping animation until we replace it with a new
425 // animation.
426 final VoidCallback? previousTrainHoppingListenerRemover = _trainHoppingListenerRemover;
427 _trainHoppingListenerRemover = null;
428
429 if (nextRoute is TransitionRoute<dynamic> &&
430 canTransitionTo(nextRoute) &&
431 nextRoute.canTransitionFrom(this)) {
432 final Animation<double>? current = _secondaryAnimation.parent;
433 if (current != null) {
434 final Animation<double> currentTrain = (current is TrainHoppingAnimation
435 ? current.currentTrain
436 : current)!;
437 final Animation<double> nextTrain = nextRoute._animation!;
438 if (currentTrain.value == nextTrain.value || !nextTrain.isAnimating) {
439 _setSecondaryAnimation(nextTrain, nextRoute.completed);
440 } else {
441 // Two trains animate at different values. We have to do train hopping.
442 // There are three possibilities of train hopping:
443 // 1. We hop on the nextTrain when two trains meet in the middle using
444 // TrainHoppingAnimation.
445 // 2. There is no chance to hop on nextTrain because two trains never
446 // cross each other. We have to directly set the animation to
447 // nextTrain once the nextTrain stops animating.
448 // 3. A new _updateSecondaryAnimation is called before train hopping
449 // finishes. We leave a listener remover for the next call to
450 // properly clean up the existing train hopping.
451 TrainHoppingAnimation? newAnimation;
452 void jumpOnAnimationEnd(AnimationStatus status) {
453 if (!status.isAnimating) {
454 // The nextTrain has stopped animating without train hopping.
455 // Directly sets the secondary animation and disposes the
456 // TrainHoppingAnimation.
457 _setSecondaryAnimation(nextTrain, nextRoute.completed);
458 if (_trainHoppingListenerRemover != null) {
459 _trainHoppingListenerRemover!();
460 _trainHoppingListenerRemover = null;
461 }
462 }
463 }
464
465 _trainHoppingListenerRemover = () {
466 nextTrain.removeStatusListener(jumpOnAnimationEnd);
467 newAnimation?.dispose();
468 };
469 nextTrain.addStatusListener(jumpOnAnimationEnd);
470 newAnimation = TrainHoppingAnimation(
471 currentTrain,
472 nextTrain,
473 onSwitchedTrain: () {
474 assert(_secondaryAnimation.parent == newAnimation);
475 assert(newAnimation!.currentTrain == nextRoute._animation);
476 // We can hop on the nextTrain, so we don't need to listen to
477 // whether the nextTrain has stopped.
478 _setSecondaryAnimation(newAnimation!.currentTrain, nextRoute.completed);
479 if (_trainHoppingListenerRemover != null) {
480 _trainHoppingListenerRemover!();
481 _trainHoppingListenerRemover = null;
482 }
483 },
484 );
485 _setSecondaryAnimation(newAnimation, nextRoute.completed);
486 }
487 } else {
488 _setSecondaryAnimation(nextRoute._animation, nextRoute.completed);
489 }
490 } else {
491 _setSecondaryAnimation(kAlwaysDismissedAnimation);
492 }
493 // Finally, we dispose any previous train hopping animation because it
494 // has been successfully updated at this point.
495 previousTrainHoppingListenerRemover?.call();
496 }
497
498 void _setSecondaryAnimation(Animation<double>? animation, [Future<dynamic>? disposed]) {
499 _secondaryAnimation.parent = animation;
500 // Releases the reference to the next route's animation when that route
501 // is disposed.
502 disposed?.then((dynamic _) {
503 if (_secondaryAnimation.parent == animation) {
504 _secondaryAnimation.parent = kAlwaysDismissedAnimation;
505 if (animation is TrainHoppingAnimation) {
506 animation.dispose();
507 }
508 }
509 });
510 }
511
512 /// Returns true if this route supports a transition animation that runs
513 /// when [nextRoute] is pushed on top of it or when [nextRoute] is popped
514 /// off of it.
515 ///
516 /// Subclasses can override this method to restrict the set of routes they
517 /// need to coordinate transitions with.
518 ///
519 /// If true, and `nextRoute.canTransitionFrom()` is true, then the
520 /// [ModalRoute.buildTransitions] `secondaryAnimation` will run from 0.0 - 1.0
521 /// when [nextRoute] is pushed on top of this one. Similarly, if
522 /// the [nextRoute] is popped off of this route, the
523 /// `secondaryAnimation` will run from 1.0 - 0.0.
524 ///
525 /// If false, this route's [ModalRoute.buildTransitions] `secondaryAnimation` parameter
526 /// value will be [kAlwaysDismissedAnimation]. In other words, this route
527 /// will not animate when [nextRoute] is pushed on top of it or when
528 /// [nextRoute] is popped off of it.
529 ///
530 /// Returns true by default.
531 ///
532 /// See also:
533 ///
534 /// * [canTransitionFrom], which must be true for [nextRoute] for the
535 /// [ModalRoute.buildTransitions] `secondaryAnimation` to run.
536 bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true;
537
538 /// Returns true if [previousRoute] should animate when this route
539 /// is pushed on top of it or when then this route is popped off of it.
540 ///
541 /// Subclasses can override this method to restrict the set of routes they
542 /// need to coordinate transitions with.
543 ///
544 /// If true, and `previousRoute.canTransitionTo()` is true, then the
545 /// previous route's [ModalRoute.buildTransitions] `secondaryAnimation` will
546 /// run from 0.0 - 1.0 when this route is pushed on top of
547 /// it. Similarly, if this route is popped off of [previousRoute]
548 /// the previous route's `secondaryAnimation` will run from 1.0 - 0.0.
549 ///
550 /// If false, then the previous route's [ModalRoute.buildTransitions]
551 /// `secondaryAnimation` value will be kAlwaysDismissedAnimation. In
552 /// other words [previousRoute] will not animate when this route is
553 /// pushed on top of it or when then this route is popped off of it.
554 ///
555 /// Returns true by default.
556 ///
557 /// See also:
558 ///
559 /// * [canTransitionTo], which must be true for [previousRoute] for its
560 /// [ModalRoute.buildTransitions] `secondaryAnimation` to run.
561 bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => true;
562
563 // Begin PredictiveBackRoute.
564
565 @override
566 void handleStartBackGesture({double progress = 0.0}) {
567 assert(isCurrent);
568 _controller?.value = progress;
569 navigator?.didStartUserGesture();
570 }
571
572 @override
573 void handleUpdateBackGestureProgress({required double progress}) {
574 // If some other navigation happened during this gesture, don't mess with
575 // the transition anymore.
576 if (!isCurrent) {
577 return;
578 }
579 _controller?.value = progress;
580 }
581
582 @override
583 void handleCancelBackGesture() {
584 _handleDragEnd(animateForward: true);
585 }
586
587 @override
588 void handleCommitBackGesture() {
589 _handleDragEnd(animateForward: false);
590 }
591
592 void _handleDragEnd({required bool animateForward}) {
593 if (isCurrent) {
594 if (animateForward) {
595 // Typically, handleUpdateBackGestureProgress will have already
596 // completed the animation. If not, animate to completion.
597 if (!_controller!.isCompleted) {
598 _controller!.forward();
599 }
600 } else {
601 // This route is destined to pop at this point. Reuse navigator's pop.
602 navigator?.pop();
603
604 // The popping may have finished inline if already at the target destination.
605 if (_controller?.isAnimating ?? false) {
606 _controller!.reverse(from: _controller!.upperBound);
607 }
608 }
609 }
610
611 if (_controller?.isAnimating ?? false) {
612 // Keep the userGestureInProgress in true state since AndroidBackGesturePageTransitionsBuilder
613 // depends on userGestureInProgress.
614 late final AnimationStatusListener animationStatusCallback;
615 animationStatusCallback = (AnimationStatus status) {
616 navigator?.didStopUserGesture();
617 _controller!.removeStatusListener(animationStatusCallback);
618 };
619 _controller!.addStatusListener(animationStatusCallback);
620 } else {
621 navigator?.didStopUserGesture();
622 }
623 }
624
625 // End PredictiveBackRoute.
626
627 @override
628 void dispose() {
629 assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.');
630 assert(!debugTransitionCompleted(), 'Cannot dispose a $runtimeType twice.');
631 _animation?.removeStatusListener(_handleStatusChanged);
632 _performanceModeRequestHandle?.dispose();
633 _performanceModeRequestHandle = null;
634 if (willDisposeAnimationController) {
635 _controller?.dispose();
636 }
637 _transitionCompleter.complete(_result);
638 super.dispose();
639 }
640
641 /// A short description of this route useful for debugging.
642 String get debugLabel => objectRuntimeType(this, 'TransitionRoute');
643
644 @override
645 String toString() => '${objectRuntimeType(this, 'TransitionRoute')}(animation: $_controller)';
646}
647
648/// An interface for a route that supports predictive back gestures.
649///
650/// See also:
651///
652/// * [PredictiveBackPageTransitionsBuilder], which builds page transitions for
653/// predictive back.
654abstract interface class PredictiveBackRoute {
655 /// Whether this route is the top-most route on the navigator.
656 bool get isCurrent;
657
658 /// Whether a pop gesture can be started by the user for this route.
659 bool get popGestureEnabled;
660
661 /// Handles a predictive back gesture starting.
662 ///
663 /// The `progress` parameter indicates the progress of the gesture from 0.0 to
664 /// 1.0, as in [PredictiveBackEvent.progress].
665 void handleStartBackGesture({double progress = 0.0});
666
667 /// Handles a predictive back gesture updating as the user drags across the
668 /// screen.
669 ///
670 /// The `progress` parameter indicates the progress of the gesture from 0.0 to
671 /// 1.0, as in [PredictiveBackEvent.progress].
672 void handleUpdateBackGestureProgress({required double progress});
673
674 /// Handles a predictive back gesture ending successfully.
675 void handleCommitBackGesture();
676
677 /// Handles a predictive back gesture ending in cancellation.
678 void handleCancelBackGesture();
679}
680
681/// An entry in the history of a [LocalHistoryRoute].
682class LocalHistoryEntry {
683 /// Creates an entry in the history of a [LocalHistoryRoute].
684 ///
685 /// The [impliesAppBarDismissal] defaults to true if not provided.
686 LocalHistoryEntry({this.onRemove, this.impliesAppBarDismissal = true});
687
688 /// Called when this entry is removed from the history of its associated [LocalHistoryRoute].
689 final VoidCallback? onRemove;
690
691 LocalHistoryRoute<dynamic>? _owner;
692
693 /// Whether an [AppBar] in the route this entry belongs to should
694 /// automatically add a back button or close button.
695 ///
696 /// Defaults to true.
697 final bool impliesAppBarDismissal;
698
699 /// Remove this entry from the history of its associated [LocalHistoryRoute].
700 void remove() {
701 _owner?.removeLocalHistoryEntry(this);
702 assert(_owner == null);
703 }
704
705 void _notifyRemoved() {
706 onRemove?.call();
707 }
708}
709
710/// A mixin used by routes to handle back navigations internally by popping a list.
711///
712/// When a [Navigator] is instructed to pop, the current route is given an
713/// opportunity to handle the pop internally. A [LocalHistoryRoute] handles the
714/// pop internally if its list of local history entries is non-empty. Rather
715/// than being removed as the current route, the most recent [LocalHistoryEntry]
716/// is removed from the list and its [LocalHistoryEntry.onRemove] is called.
717///
718/// See also:
719///
720/// * [Route], which documents the meaning of the `T` generic type argument.
721mixin LocalHistoryRoute<T> on Route<T> {
722 List<LocalHistoryEntry>? _localHistory;
723 int _entriesImpliesAppBarDismissal = 0;
724
725 /// Adds a local history entry to this route.
726 ///
727 /// When asked to pop, if this route has any local history entries, this route
728 /// will handle the pop internally by removing the most recently added local
729 /// history entry.
730 ///
731 /// The given local history entry must not already be part of another local
732 /// history route.
733 ///
734 /// {@tool snippet}
735 ///
736 /// The following example is an app with 2 pages: `HomePage` and `SecondPage`.
737 /// The `HomePage` can navigate to the `SecondPage`.
738 ///
739 /// The `SecondPage` uses a [LocalHistoryEntry] to implement local navigation
740 /// within that page. Pressing 'show rectangle' displays a red rectangle and
741 /// adds a local history entry. At that point, pressing the '< back' button
742 /// pops the latest route, which is the local history entry, and the red
743 /// rectangle disappears. Pressing the '< back' button a second time
744 /// once again pops the latest route, which is the `SecondPage`, itself.
745 /// Therefore, the second press navigates back to the `HomePage`.
746 ///
747 /// ```dart
748 /// class App extends StatelessWidget {
749 /// const App({super.key});
750 ///
751 /// @override
752 /// Widget build(BuildContext context) {
753 /// return MaterialApp(
754 /// initialRoute: '/',
755 /// routes: <String, WidgetBuilder>{
756 /// '/': (BuildContext context) => const HomePage(),
757 /// '/second_page': (BuildContext context) => const SecondPage(),
758 /// },
759 /// );
760 /// }
761 /// }
762 ///
763 /// class HomePage extends StatefulWidget {
764 /// const HomePage({super.key});
765 ///
766 /// @override
767 /// State<HomePage> createState() => _HomePageState();
768 /// }
769 ///
770 /// class _HomePageState extends State<HomePage> {
771 /// @override
772 /// Widget build(BuildContext context) {
773 /// return Scaffold(
774 /// body: Center(
775 /// child: Column(
776 /// mainAxisSize: MainAxisSize.min,
777 /// children: <Widget>[
778 /// const Text('HomePage'),
779 /// // Press this button to open the SecondPage.
780 /// ElevatedButton(
781 /// child: const Text('Second Page >'),
782 /// onPressed: () {
783 /// Navigator.pushNamed(context, '/second_page');
784 /// },
785 /// ),
786 /// ],
787 /// ),
788 /// ),
789 /// );
790 /// }
791 /// }
792 ///
793 /// class SecondPage extends StatefulWidget {
794 /// const SecondPage({super.key});
795 ///
796 /// @override
797 /// State<SecondPage> createState() => _SecondPageState();
798 /// }
799 ///
800 /// class _SecondPageState extends State<SecondPage> {
801 ///
802 /// bool _showRectangle = false;
803 ///
804 /// Future<void> _navigateLocallyToShowRectangle() async {
805 /// // This local history entry essentially represents the display of the red
806 /// // rectangle. When this local history entry is removed, we hide the red
807 /// // rectangle.
808 /// setState(() => _showRectangle = true);
809 /// ModalRoute.of(context)?.addLocalHistoryEntry(
810 /// LocalHistoryEntry(
811 /// onRemove: () {
812 /// // Hide the red rectangle.
813 /// setState(() => _showRectangle = false);
814 /// }
815 /// )
816 /// );
817 /// }
818 ///
819 /// @override
820 /// Widget build(BuildContext context) {
821 /// final Widget localNavContent = _showRectangle
822 /// ? Container(
823 /// width: 100.0,
824 /// height: 100.0,
825 /// color: Colors.red,
826 /// )
827 /// : ElevatedButton(
828 /// onPressed: _navigateLocallyToShowRectangle,
829 /// child: const Text('Show Rectangle'),
830 /// );
831 ///
832 /// return Scaffold(
833 /// body: Center(
834 /// child: Column(
835 /// mainAxisAlignment: MainAxisAlignment.center,
836 /// children: <Widget>[
837 /// localNavContent,
838 /// ElevatedButton(
839 /// child: const Text('< Back'),
840 /// onPressed: () {
841 /// // Pop a route. If this is pressed while the red rectangle is
842 /// // visible then it will pop our local history entry, which
843 /// // will hide the red rectangle. Otherwise, the SecondPage will
844 /// // navigate back to the HomePage.
845 /// Navigator.of(context).pop();
846 /// },
847 /// ),
848 /// ],
849 /// ),
850 /// ),
851 /// );
852 /// }
853 /// }
854 /// ```
855 /// {@end-tool}
856 void addLocalHistoryEntry(LocalHistoryEntry entry) {
857 assert(entry._owner == null);
858 entry._owner = this;
859 _localHistory ??= <LocalHistoryEntry>[];
860 final bool wasEmpty = _localHistory!.isEmpty;
861 _localHistory!.add(entry);
862 bool internalStateChanged = false;
863 if (entry.impliesAppBarDismissal) {
864 internalStateChanged = _entriesImpliesAppBarDismissal == 0;
865 _entriesImpliesAppBarDismissal += 1;
866 }
867 if (wasEmpty || internalStateChanged) {
868 changedInternalState();
869 }
870 }
871
872 /// Remove a local history entry from this route.
873 ///
874 /// The entry's [LocalHistoryEntry.onRemove] callback, if any, will be called
875 /// synchronously.
876 void removeLocalHistoryEntry(LocalHistoryEntry entry) {
877 assert(entry._owner == this);
878 assert(_localHistory!.contains(entry));
879 bool internalStateChanged = false;
880 if (_localHistory!.remove(entry) && entry.impliesAppBarDismissal) {
881 _entriesImpliesAppBarDismissal -= 1;
882 internalStateChanged = _entriesImpliesAppBarDismissal == 0;
883 }
884 entry._owner = null;
885 entry._notifyRemoved();
886 if (_localHistory!.isEmpty || internalStateChanged) {
887 assert(_entriesImpliesAppBarDismissal == 0);
888 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
889 // The local history might be removed as a result of disposing inactive
890 // elements during finalizeTree. The state is locked at this moment, and
891 // we can only notify state has changed in the next frame.
892 SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
893 if (isActive) {
894 changedInternalState();
895 }
896 }, debugLabel: 'LocalHistoryRoute.changedInternalState');
897 } else {
898 changedInternalState();
899 }
900 }
901 }
902
903 @Deprecated(
904 'Use popDisposition instead. '
905 'This feature was deprecated after v3.12.0-1.0.pre.',
906 )
907 @override
908 Future<RoutePopDisposition> willPop() async {
909 if (willHandlePopInternally) {
910 return RoutePopDisposition.pop;
911 }
912 return super.willPop();
913 }
914
915 @override
916 RoutePopDisposition get popDisposition {
917 if (willHandlePopInternally) {
918 return RoutePopDisposition.pop;
919 }
920 return super.popDisposition;
921 }
922
923 @override
924 bool didPop(T? result) {
925 if (_localHistory != null && _localHistory!.isNotEmpty) {
926 final LocalHistoryEntry entry = _localHistory!.removeLast();
927 assert(entry._owner == this);
928 entry._owner = null;
929 entry._notifyRemoved();
930 bool internalStateChanged = false;
931 if (entry.impliesAppBarDismissal) {
932 _entriesImpliesAppBarDismissal -= 1;
933 internalStateChanged = _entriesImpliesAppBarDismissal == 0;
934 }
935 if (_localHistory!.isEmpty || internalStateChanged) {
936 changedInternalState();
937 }
938 return false;
939 }
940 return super.didPop(result);
941 }
942
943 @override
944 bool get willHandlePopInternally {
945 return _localHistory != null && _localHistory!.isNotEmpty;
946 }
947}
948
949class _DismissModalAction extends DismissAction {
950 _DismissModalAction(this.context);
951
952 final BuildContext context;
953
954 @override
955 bool isEnabled(DismissIntent intent) {
956 final ModalRoute<dynamic> route = ModalRoute.of<dynamic>(context)!;
957 return route.barrierDismissible;
958 }
959
960 @override
961 Object invoke(DismissIntent intent) {
962 return Navigator.of(context).maybePop();
963 }
964}
965
966enum _ModalRouteAspect {
967 /// Specifies the aspect corresponding to [ModalRoute.isCurrent].
968 isCurrent,
969
970 /// Specifies the aspect corresponding to [ModalRoute.canPop].
971 canPop,
972
973 /// Specifies the aspect corresponding to [ModalRoute.settings].
974 settings,
975
976 /// Specifies the aspect corresponding to [ModalRoute.isActive].
977 isActive,
978
979 /// Specifies the aspect corresponding to [ModalRoute.isFirst].
980 isFirst,
981
982 /// Specifies the aspect corresponding to [ModalRoute.opaque].
983 opaque,
984
985 /// Specifies the aspect corresponding to [ModalRoute.popDisposition].
986 popDisposition,
987}
988
989class _ModalScopeStatus extends InheritedModel<_ModalRouteAspect> {
990 const _ModalScopeStatus({
991 required this.isCurrent,
992 required this.canPop,
993 required this.impliesAppBarDismissal,
994 required this.route,
995 required this.opaque,
996 required super.child,
997 });
998
999 final bool isCurrent;
1000 final bool canPop;
1001 final bool impliesAppBarDismissal;
1002 final bool opaque;
1003 final Route<dynamic> route;
1004
1005 @override
1006 bool updateShouldNotify(_ModalScopeStatus old) {
1007 return isCurrent != old.isCurrent ||
1008 canPop != old.canPop ||
1009 impliesAppBarDismissal != old.impliesAppBarDismissal ||
1010 route != old.route ||
1011 opaque != old.opaque;
1012 }
1013
1014 @override
1015 void debugFillProperties(DiagnosticPropertiesBuilder description) {
1016 super.debugFillProperties(description);
1017 description.add(
1018 FlagProperty('isCurrent', value: isCurrent, ifTrue: 'active', ifFalse: 'inactive'),
1019 );
1020 description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop'));
1021 description.add(
1022 FlagProperty(
1023 'impliesAppBarDismissal',
1024 value: impliesAppBarDismissal,
1025 ifTrue: 'implies app bar dismissal',
1026 ),
1027 );
1028 }
1029
1030 @override
1031 bool updateShouldNotifyDependent(
1032 covariant _ModalScopeStatus oldWidget,
1033 Set<_ModalRouteAspect> dependencies,
1034 ) {
1035 return dependencies.any(
1036 (_ModalRouteAspect dependency) => switch (dependency) {
1037 _ModalRouteAspect.isCurrent => isCurrent != oldWidget.isCurrent,
1038 _ModalRouteAspect.canPop => canPop != oldWidget.canPop,
1039 _ModalRouteAspect.settings => route.settings != oldWidget.route.settings,
1040 _ModalRouteAspect.isActive => route.isActive != oldWidget.route.isActive,
1041 _ModalRouteAspect.isFirst => route.isFirst != oldWidget.route.isFirst,
1042 _ModalRouteAspect.opaque => opaque != oldWidget.opaque,
1043 _ModalRouteAspect.popDisposition => route.popDisposition != oldWidget.route.popDisposition,
1044 },
1045 );
1046 }
1047}
1048
1049class _ModalScope<T> extends StatefulWidget {
1050 const _ModalScope({super.key, required this.route});
1051
1052 final ModalRoute<T> route;
1053
1054 @override
1055 _ModalScopeState<T> createState() => _ModalScopeState<T>();
1056}
1057
1058class _ModalScopeState<T> extends State<_ModalScope<T>> {
1059 // We cache the result of calling the route's buildPage, and clear the cache
1060 // whenever the dependencies change. This implements the contract described in
1061 // the documentation for buildPage, namely that it gets called once, unless
1062 // something like a ModalRoute.of() dependency triggers an update.
1063 Widget? _page;
1064
1065 // This is the combination of the two animations for the route.
1066 late Listenable _listenable;
1067
1068 /// The node this scope will use for its root [FocusScope] widget.
1069 final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope');
1070 final ScrollController primaryScrollController = ScrollController();
1071
1072 @override
1073 void initState() {
1074 super.initState();
1075 final List<Listenable> animations = <Listenable>[
1076 if (widget.route.animation != null) widget.route.animation!,
1077 if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation!,
1078 ];
1079 _listenable = Listenable.merge(animations);
1080 }
1081
1082 @override
1083 void didUpdateWidget(_ModalScope<T> oldWidget) {
1084 super.didUpdateWidget(oldWidget);
1085 assert(widget.route == oldWidget.route);
1086 _updateFocusScopeNode();
1087 }
1088
1089 @override
1090 void didChangeDependencies() {
1091 super.didChangeDependencies();
1092 _page = null;
1093 _updateFocusScopeNode();
1094 }
1095
1096 void _updateFocusScopeNode() {
1097 final TraversalEdgeBehavior traversalEdgeBehavior;
1098 final TraversalEdgeBehavior directionalTraversalEdgeBehavior;
1099 final ModalRoute<T> route = widget.route;
1100 if (route.traversalEdgeBehavior != null) {
1101 traversalEdgeBehavior = route.traversalEdgeBehavior!;
1102 } else {
1103 traversalEdgeBehavior = route.navigator!.widget.routeTraversalEdgeBehavior;
1104 }
1105 if (route.directionalTraversalEdgeBehavior != null) {
1106 directionalTraversalEdgeBehavior = route.directionalTraversalEdgeBehavior!;
1107 } else {
1108 directionalTraversalEdgeBehavior =
1109 route.navigator!.widget.routeDirectionalTraversalEdgeBehavior;
1110 }
1111 focusScopeNode.traversalEdgeBehavior = traversalEdgeBehavior;
1112 focusScopeNode.directionalTraversalEdgeBehavior = directionalTraversalEdgeBehavior;
1113 if (route.isCurrent && _shouldRequestFocus) {
1114 route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode);
1115 }
1116 }
1117
1118 void _forceRebuildPage() {
1119 setState(() {
1120 _page = null;
1121 });
1122 }
1123
1124 @override
1125 void dispose() {
1126 focusScopeNode.dispose();
1127 primaryScrollController.dispose();
1128 super.dispose();
1129 }
1130
1131 bool get _shouldIgnoreFocusRequest {
1132 return widget.route.animation?.status == AnimationStatus.reverse ||
1133 (widget.route.navigator?.userGestureInProgress ?? false);
1134 }
1135
1136 bool get _shouldRequestFocus {
1137 return widget.route.requestFocus;
1138 }
1139
1140 // This should be called to wrap any changes to route.isCurrent, route.canPop,
1141 // and route.offstage.
1142 void _routeSetState(VoidCallback fn) {
1143 if (widget.route.isCurrent && !_shouldIgnoreFocusRequest && _shouldRequestFocus) {
1144 widget.route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode);
1145 }
1146 setState(fn);
1147 }
1148
1149 @override
1150 Widget build(BuildContext context) {
1151 // Only top most route can participate in focus traversal.
1152 focusScopeNode.skipTraversal = !widget.route.isCurrent;
1153 return AnimatedBuilder(
1154 animation: widget.route.restorationScopeId,
1155 builder: (BuildContext context, Widget? child) {
1156 assert(child != null);
1157 return RestorationScope(
1158 restorationId: widget.route.restorationScopeId.value,
1159 child: child!,
1160 );
1161 },
1162 child: _ModalScopeStatus(
1163 route: widget.route,
1164 isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
1165 canPop: widget.route.canPop, // _routeSetState is called if this updates
1166 opaque: widget.route.opaque, // _routeSetState is called if this updates
1167 impliesAppBarDismissal: widget.route.impliesAppBarDismissal,
1168 child: Offstage(
1169 offstage: widget.route.offstage, // _routeSetState is called if this updates
1170 child: PageStorage(
1171 bucket: widget.route._storageBucket, // immutable
1172 child: Builder(
1173 builder: (BuildContext context) {
1174 return Actions(
1175 actions: <Type, Action<Intent>>{DismissIntent: _DismissModalAction(context)},
1176 child: PrimaryScrollController(
1177 controller: primaryScrollController,
1178 child: FocusScope.withExternalFocusNode(
1179 focusScopeNode: focusScopeNode, // immutable
1180 child: RepaintBoundary(
1181 child: ListenableBuilder(
1182 listenable: _listenable, // immutable
1183 builder: (BuildContext context, Widget? child) {
1184 return widget.route._buildFlexibleTransitions(
1185 context,
1186 widget.route.animation!,
1187 widget.route.secondaryAnimation!,
1188 // This additional ListenableBuilder is include because if the
1189 // value of the userGestureInProgressNotifier changes, it's
1190 // only necessary to rebuild the IgnorePointer widget and set
1191 // the focus node's ability to focus.
1192 ListenableBuilder(
1193 listenable:
1194 widget.route.navigator?.userGestureInProgressNotifier ??
1195 ValueNotifier<bool>(false),
1196 builder: (BuildContext context, Widget? child) {
1197 final bool ignoreEvents = _shouldIgnoreFocusRequest;
1198 focusScopeNode.canRequestFocus = !ignoreEvents;
1199 return IgnorePointer(ignoring: ignoreEvents, child: child);
1200 },
1201 child: child,
1202 ),
1203 );
1204 },
1205 child: _page ??= RepaintBoundary(
1206 key: widget.route._subtreeKey, // immutable
1207 child: Builder(
1208 builder: (BuildContext context) {
1209 return widget.route.buildPage(
1210 context,
1211 widget.route.animation!,
1212 widget.route.secondaryAnimation!,
1213 );
1214 },
1215 ),
1216 ),
1217 ),
1218 ),
1219 ),
1220 ),
1221 );
1222 },
1223 ),
1224 ),
1225 ),
1226 ),
1227 );
1228 }
1229}
1230
1231/// A route that blocks interaction with previous routes.
1232///
1233/// [ModalRoute]s cover the entire [Navigator]. They are not necessarily
1234/// [opaque], however; for example, a pop-up menu uses a [ModalRoute] but only
1235/// shows the menu in a small box overlapping the previous route.
1236///
1237/// The `T` type argument is the return value of the route. If there is no
1238/// return value, consider using `void` as the return value.
1239///
1240/// See also:
1241///
1242/// * [Route], which further documents the meaning of the `T` generic type argument.
1243abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
1244 /// Creates a route that blocks interaction with previous routes.
1245 ModalRoute({
1246 super.settings,
1247 super.requestFocus,
1248 this.filter,
1249 this.traversalEdgeBehavior,
1250 this.directionalTraversalEdgeBehavior,
1251 });
1252
1253 /// The filter to add to the barrier.
1254 ///
1255 /// If given, this filter will be applied to the modal barrier using
1256 /// [BackdropFilter]. This allows blur effects, for example.
1257 final ui.ImageFilter? filter;
1258
1259 /// Controls the transfer of focus beyond the first and the last items of a
1260 /// [FocusScopeNode].
1261 ///
1262 /// If set to null, [Navigator.routeTraversalEdgeBehavior] is used.
1263 final TraversalEdgeBehavior? traversalEdgeBehavior;
1264
1265 /// Controls the directional transfer of focus beyond the first and the last
1266 /// items of a [FocusScopeNode].
1267 ///
1268 /// If set to null, [Navigator.routeDirectionalTraversalEdgeBehavior] is used.
1269 final TraversalEdgeBehavior? directionalTraversalEdgeBehavior;
1270
1271 // The API for general users of this class
1272
1273 /// Returns the modal route most closely associated with the given context.
1274 ///
1275 /// Returns null if the given context is not associated with a modal route.
1276 ///
1277 /// {@tool snippet}
1278 ///
1279 /// Typical usage is as follows:
1280 ///
1281 /// ```dart
1282 /// ModalRoute<int>? route = ModalRoute.of<int>(context);
1283 /// ```
1284 /// {@end-tool}
1285 ///
1286 /// The given [BuildContext] will be rebuilt if the state of the route changes
1287 /// while it is visible (specifically, if [isCurrent] or [canPop] change value).
1288 @optionalTypeArgs
1289 static ModalRoute<T>? of<T extends Object?>(BuildContext context) {
1290 return _of<T>(context);
1291 }
1292
1293 static ModalRoute<T>? _of<T extends Object?>(BuildContext context, [_ModalRouteAspect? aspect]) {
1294 return InheritedModel.inheritFrom<_ModalScopeStatus>(context, aspect: aspect)?.route
1295 as ModalRoute<T>?;
1296 }
1297
1298 /// Returns [ModalRoute.isCurrent] for the modal route most closely associated
1299 /// with the given context.
1300 ///
1301 /// Returns null if the given context is not associated with a modal route.
1302 ///
1303 /// Use of this method will cause the given [context] to rebuild any time that
1304 /// the [ModalRoute.isCurrent] property of the ancestor [_ModalScopeStatus] changes.
1305 static bool? isCurrentOf(BuildContext context) =>
1306 _of(context, _ModalRouteAspect.isCurrent)?.isCurrent;
1307
1308 /// Returns [ModalRoute.canPop] for the modal route most closely associated
1309 /// with the given context.
1310 ///
1311 /// Returns null if the given context is not associated with a modal route.
1312 ///
1313 /// Use of this method will cause the given [context] to rebuild any time that
1314 /// the [ModalRoute.canPop] property of the ancestor [_ModalScopeStatus] changes.
1315 static bool? canPopOf(BuildContext context) => _of(context, _ModalRouteAspect.canPop)?.canPop;
1316
1317 /// Returns [ModalRoute.settings] for the modal route most closely associated
1318 /// with the given context.
1319 ///
1320 /// Returns null if the given context is not associated with a modal route.
1321 ///
1322 /// Calling this method creates a dependency on the [ModalRoute] associated
1323 /// with the given [context]. As a result, the widget corresponding to [context]
1324 /// will be rebuilt whenever the route's [ModalRoute.settings] changes.
1325 static RouteSettings? settingsOf(BuildContext context) =>
1326 _of(context, _ModalRouteAspect.settings)?.settings;
1327
1328 /// Returns [ModalRoute.isActive] for the modal route most closely associated
1329 /// with the given context.
1330 ///
1331 /// Returns null if the given context is not associated with a modal route.
1332 ///
1333 /// Calling this method creates a dependency on the [ModalRoute] associated
1334 /// with the given [context]. As a result, the widget corresponding to [context]
1335 /// will be rebuilt whenever the route's [ModalRoute.isActive] changes.
1336 static bool? isActiveOf(BuildContext context) =>
1337 _of(context, _ModalRouteAspect.isActive)?.isActive;
1338
1339 /// Returns [ModalRoute.isFirst] for the modal route most closely associated
1340 /// with the given context.
1341 ///
1342 /// Returns null if the given context is not associated with a modal route.
1343 ///
1344 /// Calling this method creates a dependency on the [ModalRoute] associated
1345 /// with the given [context]. As a result, the widget corresponding to [context]
1346 /// will be rebuilt whenever the route's [ModalRoute.isFirst] changes.
1347 static bool? isFirstOf(BuildContext context) => _of(context, _ModalRouteAspect.isFirst)?.isFirst;
1348
1349 /// Returns [ModalRoute.opaque] for the modal route most closely associated
1350 /// with the given context.
1351 ///
1352 /// Returns null if the given context is not associated with a modal route.
1353 ///
1354 /// Calling this method creates a dependency on the [ModalRoute] associated
1355 /// with the given [context]. As a result, the widget corresponding to [context]
1356 /// will be rebuilt whenever the route's [ModalRoute.opaque] changes.
1357 static bool? opaqueOf(BuildContext context) => _of(context, _ModalRouteAspect.opaque)?.opaque;
1358
1359 /// Returns [ModalRoute.popDisposition] for the modal route most closely associated
1360 /// with the given context.
1361 ///
1362 /// Returns null if the given context is not associated with a modal route.
1363 ///
1364 /// Calling this method creates a dependency on the [ModalRoute] associated
1365 /// with the given [context]. As a result, the widget corresponding to [context]
1366 /// will be rebuilt whenever the route's [ModalRoute.popDisposition] changes.
1367 static RoutePopDisposition? popDispositionOf(BuildContext context) =>
1368 _of(context, _ModalRouteAspect.popDisposition)?.popDisposition;
1369
1370 /// Schedule a call to [buildTransitions].
1371 ///
1372 /// Whenever you need to change internal state for a [ModalRoute] object, make
1373 /// the change in a function that you pass to [setState], as in:
1374 ///
1375 /// ```dart
1376 /// setState(() { _myState = newValue; });
1377 /// ```
1378 ///
1379 /// If you just change the state directly without calling [setState], then the
1380 /// route will not be scheduled for rebuilding, meaning that its rendering
1381 /// will not be updated.
1382 @protected
1383 void setState(VoidCallback fn) {
1384 if (_scopeKey.currentState != null) {
1385 _scopeKey.currentState!._routeSetState(fn);
1386 } else {
1387 // The route isn't currently visible, so we don't have to call its setState
1388 // method, but we do still need to call the fn callback, otherwise the state
1389 // in the route won't be updated!
1390 fn();
1391 }
1392 }
1393
1394 /// Returns a predicate that's true if the route has the specified name and if
1395 /// popping the route will not yield the same route, i.e. if the route's
1396 /// [willHandlePopInternally] property is false.
1397 ///
1398 /// This function is typically used with [Navigator.popUntil()].
1399 static RoutePredicate withName(String name) {
1400 return (Route<dynamic> route) {
1401 return !route.willHandlePopInternally && route is ModalRoute && route.settings.name == name;
1402 };
1403 }
1404
1405 // The API for subclasses to override - used by _ModalScope
1406
1407 /// Override this method to build the primary content of this route.
1408 ///
1409 /// The arguments have the following meanings:
1410 ///
1411 /// * `context`: The context in which the route is being built.
1412 /// * [animation]: The animation for this route's transition. When entering,
1413 /// the animation runs forward from 0.0 to 1.0. When exiting, this animation
1414 /// runs backwards from 1.0 to 0.0.
1415 /// * [secondaryAnimation]: The animation for the route being pushed on top of
1416 /// this route. This animation lets this route coordinate with the entrance
1417 /// and exit transition of routes pushed on top of this route.
1418 ///
1419 /// This method is only called when the route is first built, and rarely
1420 /// thereafter. In particular, it is not automatically called again when the
1421 /// route's state changes unless it uses [ModalRoute.of]. For a builder that
1422 /// is called every time the route's state changes, consider
1423 /// [buildTransitions]. For widgets that change their behavior when the
1424 /// route's state changes, consider [ModalRoute.of] to obtain a reference to
1425 /// the route; this will cause the widget to be rebuilt each time the route
1426 /// changes state.
1427 ///
1428 /// In general, [buildPage] should be used to build the page contents, and
1429 /// [buildTransitions] for the widgets that change as the page is brought in
1430 /// and out of view. Avoid using [buildTransitions] for content that never
1431 /// changes; building such content once from [buildPage] is more efficient.
1432 Widget buildPage(
1433 BuildContext context,
1434 Animation<double> animation,
1435 Animation<double> secondaryAnimation,
1436 );
1437
1438 /// Override this method to wrap the [child] with one or more transition
1439 /// widgets that define how the route arrives on and leaves the screen.
1440 ///
1441 /// By default, the child (which contains the widget returned by [buildPage])
1442 /// is not wrapped in any transition widgets.
1443 ///
1444 /// The [buildTransitions] method, in contrast to [buildPage], is called each
1445 /// time the [Route]'s state changes while it is visible (e.g. if the value of
1446 /// [canPop] changes on the active route).
1447 ///
1448 /// The [buildTransitions] method is typically used to define transitions
1449 /// that animate the new topmost route's comings and goings. When the
1450 /// [Navigator] pushes a route on the top of its stack, the new route's
1451 /// primary [animation] runs from 0.0 to 1.0. When the Navigator pops the
1452 /// topmost route, e.g. because the use pressed the back button, the
1453 /// primary animation runs from 1.0 to 0.0.
1454 ///
1455 /// {@tool snippet}
1456 /// The following example uses the primary animation to drive a
1457 /// [SlideTransition] that translates the top of the new route vertically
1458 /// from the bottom of the screen when it is pushed on the Navigator's
1459 /// stack. When the route is popped the SlideTransition translates the
1460 /// route from the top of the screen back to the bottom.
1461 ///
1462 /// We've used [PageRouteBuilder] to demonstrate the [buildTransitions] method
1463 /// here. The body of an override of the [buildTransitions] method would be
1464 /// defined in the same way.
1465 ///
1466 /// ```dart
1467 /// PageRouteBuilder<void>(
1468 /// pageBuilder: (BuildContext context,
1469 /// Animation<double> animation,
1470 /// Animation<double> secondaryAnimation,
1471 /// ) {
1472 /// return Scaffold(
1473 /// appBar: AppBar(title: const Text('Hello')),
1474 /// body: const Center(
1475 /// child: Text('Hello World'),
1476 /// ),
1477 /// );
1478 /// },
1479 /// transitionsBuilder: (
1480 /// BuildContext context,
1481 /// Animation<double> animation,
1482 /// Animation<double> secondaryAnimation,
1483 /// Widget child,
1484 /// ) {
1485 /// return SlideTransition(
1486 /// position: Tween<Offset>(
1487 /// begin: const Offset(0.0, 1.0),
1488 /// end: Offset.zero,
1489 /// ).animate(animation),
1490 /// child: child, // child is the value returned by pageBuilder
1491 /// );
1492 /// },
1493 /// )
1494 /// ```
1495 /// {@end-tool}
1496 ///
1497 /// When the [Navigator] pushes a route on the top of its stack, the
1498 /// [secondaryAnimation] can be used to define how the route that was on
1499 /// the top of the stack leaves the screen. Similarly when the topmost route
1500 /// is popped, the secondaryAnimation can be used to define how the route
1501 /// below it reappears on the screen. When the Navigator pushes a new route
1502 /// on the top of its stack, the old topmost route's secondaryAnimation
1503 /// runs from 0.0 to 1.0. When the Navigator pops the topmost route, the
1504 /// secondaryAnimation for the route below it runs from 1.0 to 0.0.
1505 ///
1506 /// {@tool snippet}
1507 /// The example below adds a transition that's driven by the
1508 /// [secondaryAnimation]. When this route disappears because a new route has
1509 /// been pushed on top of it, it translates in the opposite direction of
1510 /// the new route. Likewise when the route is exposed because the topmost
1511 /// route has been popped off.
1512 ///
1513 /// ```dart
1514 /// PageRouteBuilder<void>(
1515 /// pageBuilder: (BuildContext context,
1516 /// Animation<double> animation,
1517 /// Animation<double> secondaryAnimation,
1518 /// ) {
1519 /// return Scaffold(
1520 /// appBar: AppBar(title: const Text('Hello')),
1521 /// body: const Center(
1522 /// child: Text('Hello World'),
1523 /// ),
1524 /// );
1525 /// },
1526 /// transitionsBuilder: (
1527 /// BuildContext context,
1528 /// Animation<double> animation,
1529 /// Animation<double> secondaryAnimation,
1530 /// Widget child,
1531 /// ) {
1532 /// return SlideTransition(
1533 /// position: Tween<Offset>(
1534 /// begin: const Offset(0.0, 1.0),
1535 /// end: Offset.zero,
1536 /// ).animate(animation),
1537 /// child: SlideTransition(
1538 /// position: Tween<Offset>(
1539 /// begin: Offset.zero,
1540 /// end: const Offset(0.0, 1.0),
1541 /// ).animate(secondaryAnimation),
1542 /// child: child,
1543 /// ),
1544 /// );
1545 /// },
1546 /// )
1547 /// ```
1548 /// {@end-tool}
1549 ///
1550 /// In practice the `secondaryAnimation` is used pretty rarely.
1551 ///
1552 /// The arguments to this method are as follows:
1553 ///
1554 /// * `context`: The context in which the route is being built.
1555 /// * [animation]: When the [Navigator] pushes a route on the top of its stack,
1556 /// the new route's primary [animation] runs from 0.0 to 1.0. When the [Navigator]
1557 /// pops the topmost route this animation runs from 1.0 to 0.0.
1558 /// * [secondaryAnimation]: When the Navigator pushes a new route
1559 /// on the top of its stack, the old topmost route's [secondaryAnimation]
1560 /// runs from 0.0 to 1.0. When the [Navigator] pops the topmost route, the
1561 /// [secondaryAnimation] for the route below it runs from 1.0 to 0.0.
1562 /// * `child`, the page contents, as returned by [buildPage].
1563 ///
1564 /// See also:
1565 ///
1566 /// * [buildPage], which is used to describe the actual contents of the page,
1567 /// and whose result is passed to the `child` argument of this method.
1568 Widget buildTransitions(
1569 BuildContext context,
1570 Animation<double> animation,
1571 Animation<double> secondaryAnimation,
1572 Widget child,
1573 ) {
1574 return child;
1575 }
1576
1577 /// The [DelegatedTransitionBuilder] provided to the route below this one in the
1578 /// navigation stack.
1579 ///
1580 /// {@template flutter.widgets.delegatedTransition}
1581 /// Used for the purposes of coordinating transitions between two routes with
1582 /// different route transitions. When a route is added to the stack, the original
1583 /// topmost route will look for this transition, and if available, it will use
1584 /// the `delegatedTransition` from the incoming transition to animate off the
1585 /// screen.
1586 ///
1587 /// If the return of the [DelegatedTransitionBuilder] is null, then by default
1588 /// the original transition of the routes will be used. This is useful if a
1589 /// route can conditionally provide a transition based on the [BuildContext].
1590 /// {@endtemplate}
1591 ///
1592 /// The [ModalRoute] receiving this transition will set it to their
1593 /// [receivedTransition] property.
1594 ///
1595 /// {@tool dartpad}
1596 /// This sample shows an app that uses three different page transitions, a
1597 /// Material Zoom transition, the standard Cupertino sliding transition, and a
1598 /// custom vertical transition. All of the page routes are able to inform the
1599 /// previous page how to transition off the screen to sync with the new page.
1600 ///
1601 /// ** See code in examples/api/lib/widgets/routes/flexible_route_transitions.0.dart **
1602 /// {@end-tool}
1603 ///
1604 /// {@tool dartpad}
1605 /// This sample shows an app that uses the same transitions as the previous
1606 /// sample, this time in a [MaterialApp.router].
1607 ///
1608 /// ** See code in examples/api/lib/widgets/routes/flexible_route_transitions.1.dart **
1609 /// {@end-tool}
1610 DelegatedTransitionBuilder? get delegatedTransition => null;
1611
1612 /// The [DelegatedTransitionBuilder] received from the route above this one in
1613 /// the navigation stack.
1614 ///
1615 /// {@macro flutter.widgets.delegatedTransition}
1616 ///
1617 /// The `receivedTransition` will use the above route's [delegatedTransition] in
1618 /// order to show the right route transition when the above route either enters
1619 /// or leaves the navigation stack. If not null, the `receivedTransition` will
1620 /// wrap the route content.
1621 @visibleForTesting
1622 DelegatedTransitionBuilder? receivedTransition;
1623
1624 // Wraps the transitions of this route with a DelegatedTransitionBuilder, when
1625 // _receivedTransition is not null.
1626 Widget _buildFlexibleTransitions(
1627 BuildContext context,
1628 Animation<double> animation,
1629 Animation<double> secondaryAnimation,
1630 Widget child,
1631 ) {
1632 if (receivedTransition == null || secondaryAnimation.isDismissed) {
1633 return buildTransitions(context, animation, secondaryAnimation, child);
1634 }
1635
1636 // Create a static proxy animation to suppress the original secondary transition.
1637 final ProxyAnimation proxyAnimation = ProxyAnimation();
1638
1639 final Widget proxiedOriginalTransitions = buildTransitions(
1640 context,
1641 animation,
1642 proxyAnimation,
1643 child,
1644 );
1645
1646 // If receivedTransitions return null, then we want to return the original transitions,
1647 // but with the secondary animation still proxied. This keeps a desynched
1648 // animation from playing.
1649 return receivedTransition!(
1650 context,
1651 animation,
1652 secondaryAnimation,
1653 allowSnapshotting,
1654 proxiedOriginalTransitions,
1655 ) ??
1656 proxiedOriginalTransitions;
1657 }
1658
1659 @override
1660 void install() {
1661 super.install();
1662 _animationProxy = ProxyAnimation(super.animation);
1663 _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
1664 }
1665
1666 @override
1667 TickerFuture didPush() {
1668 if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
1669 navigator!.focusNode.enclosingScope?.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
1670 }
1671 return super.didPush();
1672 }
1673
1674 @override
1675 void didAdd() {
1676 if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
1677 navigator!.focusNode.enclosingScope?.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
1678 }
1679 super.didAdd();
1680 }
1681
1682 // The API for subclasses to override - used by this class
1683
1684 /// {@template flutter.widgets.ModalRoute.barrierDismissible}
1685 /// Whether you can dismiss this route by tapping the modal barrier.
1686 ///
1687 /// The modal barrier is the scrim that is rendered behind each route, which
1688 /// generally prevents the user from interacting with the route below the
1689 /// current route, and normally partially obscures such routes.
1690 ///
1691 /// For example, when a dialog is on the screen, the page below the dialog is
1692 /// usually darkened by the modal barrier.
1693 ///
1694 /// If [barrierDismissible] is true, then tapping this barrier, pressing
1695 /// the escape key on the keyboard, or calling route popping functions
1696 /// such as [Navigator.pop] will cause the current route to be popped
1697 /// with null as the value.
1698 ///
1699 /// If [barrierDismissible] is false, then tapping the barrier has no effect.
1700 ///
1701 /// If this getter would ever start returning a different value,
1702 /// either [changedInternalState] or [changedExternalState] should
1703 /// be invoked so that the change can take effect.
1704 ///
1705 /// It is safe to use `navigator.context` to look up inherited
1706 /// widgets here, because the [Navigator] calls
1707 /// [changedExternalState] whenever its dependencies change, and
1708 /// [changedExternalState] causes the modal barrier to rebuild.
1709 ///
1710 /// See also:
1711 ///
1712 /// * [Navigator.pop], which is used to dismiss the route.
1713 /// * [barrierColor], which controls the color of the scrim for this route.
1714 /// * [ModalBarrier], the widget that implements this feature.
1715 /// {@endtemplate}
1716 bool get barrierDismissible;
1717
1718 /// Whether the semantics of the modal barrier are included in the
1719 /// semantics tree.
1720 ///
1721 /// The modal barrier is the scrim that is rendered behind each route, which
1722 /// generally prevents the user from interacting with the route below the
1723 /// current route, and normally partially obscures such routes.
1724 ///
1725 /// If [semanticsDismissible] is true, then modal barrier semantics are
1726 /// included in the semantics tree.
1727 ///
1728 /// If [semanticsDismissible] is false, then modal barrier semantics are
1729 /// excluded from the semantics tree and tapping on the modal barrier
1730 /// has no effect.
1731 ///
1732 /// If this getter would ever start returning a different value,
1733 /// either [changedInternalState] or [changedExternalState] should
1734 /// be invoked so that the change can take effect.
1735 ///
1736 /// It is safe to use `navigator.context` to look up inherited
1737 /// widgets here, because the [Navigator] calls
1738 /// [changedExternalState] whenever its dependencies change, and
1739 /// [changedExternalState] causes the modal barrier to rebuild.
1740 bool get semanticsDismissible => true;
1741
1742 /// {@template flutter.widgets.ModalRoute.barrierColor}
1743 /// The color to use for the modal barrier. If this is null, the barrier will
1744 /// be transparent.
1745 ///
1746 /// The modal barrier is the scrim that is rendered behind each route, which
1747 /// generally prevents the user from interacting with the route below the
1748 /// current route, and normally partially obscures such routes.
1749 ///
1750 /// For example, when a dialog is on the screen, the page below the dialog is
1751 /// usually darkened by the modal barrier.
1752 ///
1753 /// The color is ignored, and the barrier made invisible, when
1754 /// [ModalRoute.offstage] is true.
1755 ///
1756 /// While the route is animating into position, the color is animated from
1757 /// transparent to the specified color.
1758 /// {@endtemplate}
1759 ///
1760 /// If this getter would ever start returning a different color, one
1761 /// of the [changedInternalState] or [changedExternalState] methods
1762 /// should be invoked so that the change can take effect.
1763 ///
1764 /// It is safe to use `navigator.context` to look up inherited
1765 /// widgets here, because the [Navigator] calls
1766 /// [changedExternalState] whenever its dependencies change, and
1767 /// [changedExternalState] causes the modal barrier to rebuild.
1768 ///
1769 /// {@tool snippet}
1770 ///
1771 /// For example, to make the barrier color use the theme's
1772 /// background color, one could say:
1773 ///
1774 /// ```dart
1775 /// Color get barrierColor => Theme.of(navigator.context).colorScheme.surface;
1776 /// ```
1777 ///
1778 /// {@end-tool}
1779 ///
1780 /// See also:
1781 ///
1782 /// * [barrierDismissible], which controls the behavior of the barrier when
1783 /// tapped.
1784 /// * [ModalBarrier], the widget that implements this feature.
1785 Color? get barrierColor;
1786
1787 /// {@template flutter.widgets.ModalRoute.barrierLabel}
1788 /// The semantic label used for a dismissible barrier.
1789 ///
1790 /// If the barrier is dismissible, this label will be read out if
1791 /// accessibility tools (like VoiceOver on iOS) focus on the barrier.
1792 ///
1793 /// The modal barrier is the scrim that is rendered behind each route, which
1794 /// generally prevents the user from interacting with the route below the
1795 /// current route, and normally partially obscures such routes.
1796 ///
1797 /// For example, when a dialog is on the screen, the page below the dialog is
1798 /// usually darkened by the modal barrier.
1799 /// {@endtemplate}
1800 ///
1801 /// If this getter would ever start returning a different label,
1802 /// either [changedInternalState] or [changedExternalState] should
1803 /// be invoked so that the change can take effect.
1804 ///
1805 /// It is safe to use `navigator.context` to look up inherited
1806 /// widgets here, because the [Navigator] calls
1807 /// [changedExternalState] whenever its dependencies change, and
1808 /// [changedExternalState] causes the modal barrier to rebuild.
1809 ///
1810 /// See also:
1811 ///
1812 /// * [barrierDismissible], which controls the behavior of the barrier when
1813 /// tapped.
1814 /// * [ModalBarrier], the widget that implements this feature.
1815 String? get barrierLabel;
1816
1817 /// The curve that is used for animating the modal barrier in and out.
1818 ///
1819 /// The modal barrier is the scrim that is rendered behind each route, which
1820 /// generally prevents the user from interacting with the route below the
1821 /// current route, and normally partially obscures such routes.
1822 ///
1823 /// For example, when a dialog is on the screen, the page below the dialog is
1824 /// usually darkened by the modal barrier.
1825 ///
1826 /// While the route is animating into position, the color is animated from
1827 /// transparent to the specified [barrierColor].
1828 ///
1829 /// If this getter would ever start returning a different curve,
1830 /// either [changedInternalState] or [changedExternalState] should
1831 /// be invoked so that the change can take effect.
1832 ///
1833 /// It is safe to use `navigator.context` to look up inherited
1834 /// widgets here, because the [Navigator] calls
1835 /// [changedExternalState] whenever its dependencies change, and
1836 /// [changedExternalState] causes the modal barrier to rebuild.
1837 ///
1838 /// It defaults to [Curves.ease].
1839 ///
1840 /// See also:
1841 ///
1842 /// * [barrierColor], which determines the color that the modal transitions
1843 /// to.
1844 /// * [Curves] for a collection of common curves.
1845 /// * [AnimatedModalBarrier], the widget that implements this feature.
1846 Curve get barrierCurve => Curves.ease;
1847
1848 /// {@template flutter.widgets.ModalRoute.maintainState}
1849 /// Whether the route should remain in memory when it is inactive.
1850 ///
1851 /// If this is true, then the route is maintained, so that any futures it is
1852 /// holding from the next route will properly resolve when the next route
1853 /// pops. If this is not necessary, this can be set to false to allow the
1854 /// framework to entirely discard the route's widget hierarchy when it is not
1855 /// visible.
1856 ///
1857 /// Setting [maintainState] to false does not guarantee that the route will be
1858 /// discarded. For instance, it will not be discarded if it is still visible
1859 /// because the next above it is not opaque (e.g. it is a popup dialog).
1860 /// {@endtemplate}
1861 ///
1862 /// If this getter would ever start returning a different value, the
1863 /// [changedInternalState] should be invoked so that the change can take
1864 /// effect.
1865 ///
1866 /// See also:
1867 ///
1868 /// * [OverlayEntry.maintainState], which is the underlying implementation
1869 /// of this property.
1870 bool get maintainState;
1871
1872 /// True if a back gesture (iOS-style back swipe or Android predictive back)
1873 /// is currently underway for this route.
1874 ///
1875 /// See also:
1876 ///
1877 /// * [popGestureEnabled], which returns true if a user-triggered pop gesture
1878 /// would be allowed.
1879 bool get popGestureInProgress => navigator!.userGestureInProgress;
1880
1881 /// Whether a pop gesture can be started by the user for this route.
1882 ///
1883 /// Returns true if the user can edge-swipe to a previous route.
1884 ///
1885 /// This should only be used between frames, not during build.
1886 @override
1887 bool get popGestureEnabled {
1888 // If there's nothing to go back to, then obviously we don't support
1889 // the back gesture.
1890 if (isFirst) {
1891 return false;
1892 }
1893 // If the route wouldn't actually pop if we popped it, then the gesture
1894 // would be really confusing (or would skip internal routes), so disallow it.
1895 if (willHandlePopInternally) {
1896 return false;
1897 }
1898 // If attempts to dismiss this route might be vetoed such as in a page
1899 // with forms, then do not allow the user to dismiss the route with a swipe.
1900 if (hasScopedWillPopCallback || popDisposition == RoutePopDisposition.doNotPop) {
1901 return false;
1902 }
1903 // If we're in an animation already, we cannot be manually swiped.
1904 if (!animation!.isCompleted) {
1905 return false;
1906 }
1907
1908 // Looks like a back gesture would be welcome!
1909 return true;
1910 }
1911
1912 // The API for _ModalScope and HeroController
1913
1914 /// Whether this route is currently offstage.
1915 ///
1916 /// On the first frame of a route's entrance transition, the route is built
1917 /// [Offstage] using an animation progress of 1.0. The route is invisible and
1918 /// non-interactive, but each widget has its final size and position. This
1919 /// mechanism lets the [HeroController] determine the final local of any hero
1920 /// widgets being animated as part of the transition.
1921 ///
1922 /// The modal barrier, if any, is not rendered if [offstage] is true (see
1923 /// [barrierColor]).
1924 ///
1925 /// Whenever this changes value, [changedInternalState] is called.
1926 bool get offstage => _offstage;
1927 bool _offstage = false;
1928 set offstage(bool value) {
1929 if (_offstage == value) {
1930 return;
1931 }
1932 setState(() {
1933 _offstage = value;
1934 });
1935 _animationProxy!.parent = _offstage ? kAlwaysCompleteAnimation : super.animation;
1936 _secondaryAnimationProxy!.parent = _offstage
1937 ? kAlwaysDismissedAnimation
1938 : super.secondaryAnimation;
1939 changedInternalState();
1940 }
1941
1942 /// The build context for the subtree containing the primary content of this route.
1943 BuildContext? get subtreeContext => _subtreeKey.currentContext;
1944
1945 @override
1946 Animation<double>? get animation => _animationProxy;
1947 ProxyAnimation? _animationProxy;
1948
1949 @override
1950 Animation<double>? get secondaryAnimation => _secondaryAnimationProxy;
1951 ProxyAnimation? _secondaryAnimationProxy;
1952
1953 final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];
1954
1955 // Holding as Object? instead of T so that PopScope in this route can be
1956 // declared with any supertype of T.
1957 final Set<PopEntry<Object?>> _popEntries = <PopEntry<Object?>>{};
1958
1959 /// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with
1960 /// [addScopedWillPopCallback] returns either false or null. If they all
1961 /// return true, the base [Route.willPop]'s result will be returned. The
1962 /// callbacks will be called in the order they were added, and will only be
1963 /// called if all previous callbacks returned true.
1964 ///
1965 /// Typically this method is not overridden because applications usually
1966 /// don't create modal routes directly, they use higher level primitives
1967 /// like [showDialog]. The scoped [WillPopCallback] list makes it possible
1968 /// for ModalRoute descendants to collectively define the value of [willPop].
1969 ///
1970 /// See also:
1971 ///
1972 /// * [Form], which provides an `onWillPop` callback that uses this mechanism.
1973 /// * [addScopedWillPopCallback], which adds a callback to the list this
1974 /// method checks.
1975 /// * [removeScopedWillPopCallback], which removes a callback from the list
1976 /// this method checks.
1977 @Deprecated(
1978 'Use popDisposition instead. '
1979 'This feature was deprecated after v3.12.0-1.0.pre.',
1980 )
1981 @override
1982 Future<RoutePopDisposition> willPop() async {
1983 final _ModalScopeState<T>? scope = _scopeKey.currentState;
1984 assert(scope != null);
1985 for (final WillPopCallback callback in List<WillPopCallback>.of(_willPopCallbacks)) {
1986 if (!await callback()) {
1987 return RoutePopDisposition.doNotPop;
1988 }
1989 }
1990 return super.willPop();
1991 }
1992
1993 /// Returns [RoutePopDisposition.doNotPop] if any of the [PopEntry] instances
1994 /// registered with [registerPopEntry] have [PopEntry.canPopNotifier] set to
1995 /// false.
1996 ///
1997 /// Typically this method is not overridden because applications usually
1998 /// don't create modal routes directly, they use higher level primitives
1999 /// like [showDialog]. The scoped [PopEntry] list makes it possible for
2000 /// ModalRoute descendants to collectively define the value of
2001 /// [popDisposition].
2002 ///
2003 /// See also:
2004 ///
2005 /// * [Form], which provides an `onPopInvokedWithResult` callback that is similar.
2006 /// * [registerPopEntry], which adds a [PopEntry] to the list this method
2007 /// checks.
2008 /// * [unregisterPopEntry], which removes a [PopEntry] from the list this
2009 /// method checks.
2010 @override
2011 RoutePopDisposition get popDisposition {
2012 for (final PopEntry<Object?> popEntry in _popEntries) {
2013 if (!popEntry.canPopNotifier.value) {
2014 return RoutePopDisposition.doNotPop;
2015 }
2016 }
2017
2018 return super.popDisposition;
2019 }
2020
2021 @override
2022 void onPopInvokedWithResult(bool didPop, T? result) {
2023 for (final PopEntry<Object?> popEntry in _popEntries) {
2024 popEntry.onPopInvokedWithResult(didPop, result);
2025 }
2026 super.onPopInvokedWithResult(didPop, result);
2027 }
2028
2029 /// Enables this route to veto attempts by the user to dismiss it.
2030 ///
2031 /// This callback runs asynchronously and it's possible that it will be called
2032 /// after its route has been disposed. The callback should check [State.mounted]
2033 /// before doing anything.
2034 ///
2035 /// A typical application of this callback would be to warn the user about
2036 /// unsaved [Form] data if the user attempts to back out of the form. In that
2037 /// case, use the [Form.onWillPop] property to register the callback.
2038 ///
2039 /// See also:
2040 ///
2041 /// * [WillPopScope], which manages the registration and unregistration
2042 /// process automatically.
2043 /// * [Form], which provides an `onWillPop` callback that uses this mechanism.
2044 /// * [willPop], which runs the callbacks added with this method.
2045 /// * [removeScopedWillPopCallback], which removes a callback from the list
2046 /// that [willPop] checks.
2047 @Deprecated(
2048 'Use registerPopEntry or PopScope instead. '
2049 'This feature was deprecated after v3.12.0-1.0.pre.',
2050 )
2051 void addScopedWillPopCallback(WillPopCallback callback) {
2052 assert(
2053 _scopeKey.currentState != null,
2054 'Tried to add a willPop callback to a route that is not currently in the tree.',
2055 );
2056 _willPopCallbacks.add(callback);
2057 if (_willPopCallbacks.length == 1) {
2058 _maybeDispatchNavigationNotification();
2059 }
2060 }
2061
2062 /// Remove one of the callbacks run by [willPop].
2063 ///
2064 /// See also:
2065 ///
2066 /// * [Form], which provides an `onWillPop` callback that uses this mechanism.
2067 /// * [addScopedWillPopCallback], which adds callback to the list
2068 /// checked by [willPop].
2069 @Deprecated(
2070 'Use unregisterPopEntry or PopScope instead. '
2071 'This feature was deprecated after v3.12.0-1.0.pre.',
2072 )
2073 void removeScopedWillPopCallback(WillPopCallback callback) {
2074 assert(
2075 _scopeKey.currentState != null,
2076 'Tried to remove a willPop callback from a route that is not currently in the tree.',
2077 );
2078 _willPopCallbacks.remove(callback);
2079 if (_willPopCallbacks.isEmpty) {
2080 _maybeDispatchNavigationNotification();
2081 }
2082 }
2083
2084 /// Registers the existence of a [PopEntry] in the route.
2085 ///
2086 /// [PopEntry] instances registered in this way will have their
2087 /// [PopEntry.onPopInvokedWithResult] callbacks called when a route is popped or a pop
2088 /// is attempted. They will also be able to block pop operations with
2089 /// [PopEntry.canPopNotifier] through this route's [popDisposition] method.
2090 ///
2091 /// See also:
2092 ///
2093 /// * [unregisterPopEntry], which performs the opposite operation.
2094 void registerPopEntry(PopEntry<Object?> popEntry) {
2095 _popEntries.add(popEntry);
2096 popEntry.canPopNotifier.addListener(_maybeDispatchNavigationNotification);
2097 _maybeDispatchNavigationNotification();
2098 }
2099
2100 /// Unregisters a [PopEntry] in the route's widget subtree.
2101 ///
2102 /// See also:
2103 ///
2104 /// * [registerPopEntry], which performs the opposite operation.
2105 void unregisterPopEntry(PopEntry<Object?> popEntry) {
2106 _popEntries.remove(popEntry);
2107 popEntry.canPopNotifier.removeListener(_maybeDispatchNavigationNotification);
2108 _maybeDispatchNavigationNotification();
2109 }
2110
2111 void _maybeDispatchNavigationNotification() {
2112 if (!isCurrent) {
2113 return;
2114 }
2115 final NavigationNotification notification = NavigationNotification(
2116 // canPop indicates that the originator of the Notification can handle a
2117 // pop. In the case of PopScope, it handles pops when canPop is
2118 // false. Hence the seemingly backward logic here.
2119 canHandlePop: popDisposition == RoutePopDisposition.doNotPop || _willPopCallbacks.isNotEmpty,
2120 );
2121 // Avoid dispatching a notification in the middle of a build.
2122 switch (SchedulerBinding.instance.schedulerPhase) {
2123 case SchedulerPhase.postFrameCallbacks:
2124 notification.dispatch(subtreeContext);
2125 case SchedulerPhase.idle:
2126 case SchedulerPhase.midFrameMicrotasks:
2127 case SchedulerPhase.persistentCallbacks:
2128 case SchedulerPhase.transientCallbacks:
2129 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
2130 if (!(subtreeContext?.mounted ?? false)) {
2131 return;
2132 }
2133 notification.dispatch(subtreeContext);
2134 }, debugLabel: 'ModalRoute.dispatchNotification');
2135 }
2136 }
2137
2138 /// True if one or more [WillPopCallback] callbacks exist.
2139 ///
2140 /// This method is used to disable the horizontal swipe pop gesture supported
2141 /// by [MaterialPageRoute] for [TargetPlatform.iOS] and
2142 /// [TargetPlatform.macOS]. If a pop might be vetoed, then the back gesture is
2143 /// disabled.
2144 ///
2145 /// The [buildTransitions] method will not be called again if this changes,
2146 /// since it can change during the build as descendants of the route add or
2147 /// remove callbacks.
2148 ///
2149 /// See also:
2150 ///
2151 /// * [addScopedWillPopCallback], which adds a callback.
2152 /// * [removeScopedWillPopCallback], which removes a callback.
2153 /// * [willHandlePopInternally], which reports on another reason why
2154 /// a pop might be vetoed.
2155 @Deprecated(
2156 'Use popDisposition instead. '
2157 'This feature was deprecated after v3.12.0-1.0.pre.',
2158 )
2159 @protected
2160 bool get hasScopedWillPopCallback {
2161 return _willPopCallbacks.isNotEmpty;
2162 }
2163
2164 @override
2165 void didChangePrevious(Route<dynamic>? previousRoute) {
2166 super.didChangePrevious(previousRoute);
2167 changedInternalState();
2168 }
2169
2170 @override
2171 void didChangeNext(Route<dynamic>? nextRoute) {
2172 if (nextRoute is ModalRoute<T> &&
2173 canTransitionTo(nextRoute) &&
2174 nextRoute.delegatedTransition != delegatedTransition) {
2175 receivedTransition = nextRoute.delegatedTransition;
2176 } else {
2177 receivedTransition = null;
2178 }
2179 super.didChangeNext(nextRoute);
2180 changedInternalState();
2181 }
2182
2183 @override
2184 void didPopNext(Route<dynamic> nextRoute) {
2185 if (nextRoute is ModalRoute<T> &&
2186 canTransitionTo(nextRoute) &&
2187 nextRoute.delegatedTransition != delegatedTransition) {
2188 receivedTransition = nextRoute.delegatedTransition;
2189 } else {
2190 receivedTransition = null;
2191 }
2192 super.didPopNext(nextRoute);
2193 changedInternalState();
2194 _maybeDispatchNavigationNotification();
2195 }
2196
2197 @override
2198 void changedInternalState() {
2199 super.changedInternalState();
2200 // No need to mark dirty if this method is called during build phase.
2201 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
2202 setState(() {
2203 /* internal state already changed */
2204 });
2205 _modalBarrier.markNeedsBuild();
2206 }
2207 _modalScope.maintainState = maintainState;
2208 }
2209
2210 @override
2211 void changedExternalState() {
2212 super.changedExternalState();
2213 _modalBarrier.markNeedsBuild();
2214 if (_scopeKey.currentState != null) {
2215 _scopeKey.currentState!._forceRebuildPage();
2216 }
2217 }
2218
2219 /// Whether this route can be popped.
2220 ///
2221 /// A route can be popped if there is at least one active route below it, or
2222 /// if [willHandlePopInternally] returns true.
2223 ///
2224 /// When this changes, if the route is visible, the route will
2225 /// rebuild, and any widgets that used [ModalRoute.of] will be
2226 /// notified.
2227 bool get canPop => hasActiveRouteBelow || willHandlePopInternally;
2228
2229 /// Whether an [AppBar] in the route should automatically add a back button or
2230 /// close button.
2231 ///
2232 /// This getter returns true if there is at least one active route below it,
2233 /// or there is at least one [LocalHistoryEntry] with [impliesAppBarDismissal]
2234 /// set to true
2235 bool get impliesAppBarDismissal => hasActiveRouteBelow || _entriesImpliesAppBarDismissal > 0;
2236
2237 /// {@macro flutter.widgets.RawDialogRoute.fullscreenDialog}
2238 // TODO(dkwingsmt): Rename `ModalRoute.fullscreenDialog` something semantically suitable for a modal.
2239 // https://github.com/flutter/flutter/issues/168949
2240 bool get fullscreenDialog => false;
2241
2242 // Internals
2243
2244 final GlobalKey<_ModalScopeState<T>> _scopeKey = GlobalKey<_ModalScopeState<T>>();
2245 final GlobalKey _subtreeKey = GlobalKey();
2246 final PageStorageBucket _storageBucket = PageStorageBucket();
2247
2248 // one of the builders
2249 late OverlayEntry _modalBarrier;
2250 Widget _buildModalBarrier(BuildContext context) {
2251 Widget barrier = buildModalBarrier();
2252 if (filter != null) {
2253 barrier = BackdropFilter(filter: filter!, child: barrier);
2254 }
2255 barrier = IgnorePointer(
2256 ignoring: !animation!
2257 .isForwardOrCompleted, // changedInternalState is called when animation.status updates
2258 child: barrier, // dismissed is possible when doing a manual pop gesture
2259 );
2260 if (semanticsDismissible && barrierDismissible) {
2261 // To be sorted after the _modalScope.
2262 barrier = Semantics(sortKey: const OrdinalSortKey(1.0), child: barrier);
2263 }
2264 return barrier;
2265 }
2266
2267 /// Build the barrier for this [ModalRoute], subclasses can override
2268 /// this method to create their own barrier with customized features such as
2269 /// color or accessibility focus size.
2270 ///
2271 /// See also:
2272 /// * [ModalBarrier], which is typically used to build a barrier.
2273 /// * [ModalBottomSheetRoute], which overrides this method to build a
2274 /// customized barrier.
2275 Widget buildModalBarrier() {
2276 Widget barrier;
2277 if (barrierColor != null && barrierColor!.alpha != 0 && !offstage) {
2278 // changedInternalState is called if barrierColor or offstage updates
2279 assert(barrierColor != barrierColor!.withOpacity(0.0));
2280 final Animation<Color?> color = animation!.drive(
2281 ColorTween(
2282 begin: barrierColor!.withOpacity(0.0),
2283 end: barrierColor, // changedInternalState is called if barrierColor updates
2284 ).chain(
2285 CurveTween(curve: barrierCurve),
2286 ), // changedInternalState is called if barrierCurve updates
2287 );
2288 barrier = AnimatedModalBarrier(
2289 color: color,
2290 dismissible:
2291 barrierDismissible, // changedInternalState is called if barrierDismissible updates
2292 semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
2293 barrierSemanticsDismissible: semanticsDismissible,
2294 );
2295 } else {
2296 barrier = ModalBarrier(
2297 dismissible:
2298 barrierDismissible, // changedInternalState is called if barrierDismissible updates
2299 semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
2300 barrierSemanticsDismissible: semanticsDismissible,
2301 );
2302 }
2303
2304 return barrier;
2305 }
2306
2307 // We cache the part of the modal scope that doesn't change from frame to
2308 // frame so that we minimize the amount of building that happens.
2309 Widget? _modalScopeCache;
2310
2311 // one of the builders
2312 Widget _buildModalScope(BuildContext context) {
2313 // To be sorted before the _modalBarrier.
2314 return _modalScopeCache ??= Semantics(
2315 sortKey: const OrdinalSortKey(0.0),
2316 child: _ModalScope<T>(
2317 key: _scopeKey,
2318 route: this,
2319 // _ModalScope calls buildTransitions() and buildChild(), defined above
2320 ),
2321 );
2322 }
2323
2324 late OverlayEntry _modalScope;
2325
2326 @override
2327 Iterable<OverlayEntry> createOverlayEntries() {
2328 return <OverlayEntry>[
2329 _modalBarrier = OverlayEntry(builder: _buildModalBarrier),
2330 _modalScope = OverlayEntry(
2331 builder: _buildModalScope,
2332 maintainState: maintainState,
2333 canSizeOverlay: opaque,
2334 ),
2335 ];
2336 }
2337
2338 @override
2339 String toString() =>
2340 '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)';
2341}
2342
2343/// A modal route that overlays a widget over the current route.
2344///
2345/// {@macro flutter.widgets.ModalRoute.barrierDismissible}
2346///
2347/// {@tool dartpad}
2348/// This example shows how to create a dialog box that is dismissible.
2349///
2350/// ** See code in examples/api/lib/widgets/routes/popup_route.0.dart **
2351/// {@end-tool}
2352///
2353/// See also:
2354///
2355/// * [ModalRoute], which is the base class for this class.
2356/// * [Navigator.pop], which is used to dismiss the route.
2357abstract class PopupRoute<T> extends ModalRoute<T> {
2358 /// Initializes the [PopupRoute].
2359 PopupRoute({
2360 super.settings,
2361 super.requestFocus,
2362 super.filter,
2363 super.traversalEdgeBehavior,
2364 super.directionalTraversalEdgeBehavior,
2365 });
2366
2367 @override
2368 bool get opaque => false;
2369
2370 @override
2371 bool get maintainState => true;
2372
2373 @override
2374 bool get allowSnapshotting => false;
2375}
2376
2377/// A [Navigator] observer that notifies [RouteAware]s of changes to the
2378/// state of their [Route].
2379///
2380/// [RouteObserver] informs subscribers whenever a route of type `R` is pushed
2381/// on top of their own route of type `R` or popped from it. This is for example
2382/// useful to keep track of page transitions, e.g. a `RouteObserver<PageRoute>`
2383/// will inform subscribed [RouteAware]s whenever the user navigates away from
2384/// the current page route to another page route.
2385///
2386/// To be informed about route changes of any type, consider instantiating a
2387/// `RouteObserver<Route>`.
2388///
2389/// ## Type arguments
2390///
2391/// When using more aggressive [lints](https://dart.dev/lints),
2392/// in particular lints such as `always_specify_types`,
2393/// the Dart analyzer will require that certain types
2394/// be given with their type arguments. Since the [Route] class and its
2395/// subclasses have a type argument, this includes the arguments passed to this
2396/// class. Consider using `dynamic` to specify the entire class of routes rather
2397/// than only specific subtypes. For example, to watch for all [ModalRoute]
2398/// variants, the `RouteObserver<ModalRoute<dynamic>>` type may be used.
2399///
2400/// {@tool dartpad}
2401/// This example demonstrates how to implement a [RouteObserver] that notifies
2402/// [RouteAware] widget of changes to the state of their [Route].
2403///
2404/// ** See code in examples/api/lib/widgets/routes/route_observer.0.dart **
2405/// {@end-tool}
2406///
2407/// See also:
2408/// * [RouteAware], this is used with [RouteObserver] to make a widget aware
2409/// of changes to the [Navigator]'s session history.
2410class RouteObserver<R extends Route<dynamic>> extends NavigatorObserver {
2411 final Map<R, Set<RouteAware>> _listeners = <R, Set<RouteAware>>{};
2412
2413 /// Whether this observer is managing changes for the specified route.
2414 ///
2415 /// If asserts are disabled, this method will throw an exception.
2416 @visibleForTesting
2417 bool debugObservingRoute(R route) {
2418 late bool contained;
2419 assert(() {
2420 contained = _listeners.containsKey(route);
2421 return true;
2422 }());
2423 return contained;
2424 }
2425
2426 /// Subscribe [routeAware] to be informed about changes to [route].
2427 ///
2428 /// Going forward, [routeAware] will be informed about qualifying changes
2429 /// to [route], e.g. when [route] is covered by another route or when [route]
2430 /// is popped off the [Navigator] stack.
2431 void subscribe(RouteAware routeAware, R route) {
2432 final Set<RouteAware> subscribers = _listeners.putIfAbsent(route, () => <RouteAware>{});
2433 if (subscribers.add(routeAware)) {
2434 routeAware.didPush();
2435 }
2436 }
2437
2438 /// Unsubscribe [routeAware].
2439 ///
2440 /// [routeAware] is no longer informed about changes to its route. If the given argument was
2441 /// subscribed to multiple types, this will unregister it (once) from each type.
2442 void unsubscribe(RouteAware routeAware) {
2443 final List<R> routes = _listeners.keys.toList();
2444 for (final R route in routes) {
2445 final Set<RouteAware>? subscribers = _listeners[route];
2446 if (subscribers != null) {
2447 subscribers.remove(routeAware);
2448 if (subscribers.isEmpty) {
2449 _listeners.remove(route);
2450 }
2451 }
2452 }
2453 }
2454
2455 @override
2456 void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
2457 if (route is R && previousRoute is R) {
2458 final List<RouteAware>? previousSubscribers = _listeners[previousRoute]?.toList();
2459
2460 if (previousSubscribers != null) {
2461 for (final RouteAware routeAware in previousSubscribers) {
2462 routeAware.didPopNext();
2463 }
2464 }
2465
2466 final List<RouteAware>? subscribers = _listeners[route]?.toList();
2467
2468 if (subscribers != null) {
2469 for (final RouteAware routeAware in subscribers) {
2470 routeAware.didPop();
2471 }
2472 }
2473 }
2474 }
2475
2476 @override
2477 void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
2478 if (route is R && previousRoute is R) {
2479 final Set<RouteAware>? previousSubscribers = _listeners[previousRoute];
2480
2481 if (previousSubscribers != null) {
2482 for (final RouteAware routeAware in previousSubscribers) {
2483 routeAware.didPushNext();
2484 }
2485 }
2486 }
2487 }
2488}
2489
2490/// An interface for objects that are aware of their current [Route].
2491///
2492/// This is used with [RouteObserver] to make a widget aware of changes to the
2493/// [Navigator]'s session history.
2494abstract mixin class RouteAware {
2495 /// Called when the top route has been popped off, and the current route
2496 /// shows up.
2497 void didPopNext() {}
2498
2499 /// Called when the current route has been pushed.
2500 void didPush() {}
2501
2502 /// Called when the current route has been popped off.
2503 void didPop() {}
2504
2505 /// Called when a new route has been pushed, and the current route is no
2506 /// longer visible.
2507 void didPushNext() {}
2508}
2509
2510/// A general dialog route which allows for customization of the dialog popup.
2511///
2512/// It is used internally by [showGeneralDialog] or can be directly pushed
2513/// onto the [Navigator] stack to enable state restoration. See
2514/// [showGeneralDialog] for a state restoration app example.
2515///
2516/// This function takes a `pageBuilder`, which typically builds a dialog.
2517/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
2518/// returned by the `builder` does not share a context with the location that
2519/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
2520/// custom [StatefulWidget] if the dialog needs to update dynamically.
2521///
2522/// The `barrierDismissible` argument is used to indicate whether tapping on the
2523/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
2524///
2525/// The `barrierColor` argument is used to specify the color of the modal
2526/// barrier that darkens everything below the dialog. If `null`, the default
2527/// color `Colors.black54` is used.
2528///
2529/// The `settings` argument define the settings for this route. See
2530/// [RouteSettings] for details.
2531///
2532/// {@template flutter.widgets.RawDialogRoute}
2533/// A [DisplayFeature] can split the screen into sub-screens. The closest one to
2534/// [anchorPoint] is used to render the content.
2535///
2536/// If no [anchorPoint] is provided, then [Directionality] is used:
2537///
2538/// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will
2539/// cause the content to appear in the top-left sub-screen.
2540/// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`,
2541/// which will cause the content to appear in the top-right sub-screen.
2542///
2543/// If no [anchorPoint] is provided, and there is no [Directionality] ancestor
2544/// widget in the tree, then the widget asserts during build in debug mode.
2545/// {@endtemplate}
2546///
2547/// See also:
2548///
2549/// * [DisplayFeatureSubScreen], which documents the specifics of how
2550/// [DisplayFeature]s can split the screen into sub-screens.
2551/// * [showGeneralDialog], which is a way to display a RawDialogRoute.
2552/// * [showDialog], which is a way to display a DialogRoute.
2553/// * [showCupertinoDialog], which displays an iOS-style dialog.
2554class RawDialogRoute<T> extends PopupRoute<T> {
2555 /// A general dialog route which allows for customization of the dialog popup.
2556 RawDialogRoute({
2557 required RoutePageBuilder pageBuilder,
2558 bool barrierDismissible = true,
2559 Color? barrierColor = const Color(0x80000000),
2560 String? barrierLabel,
2561 Duration transitionDuration = const Duration(milliseconds: 200),
2562 RouteTransitionsBuilder? transitionBuilder,
2563 super.settings,
2564 super.requestFocus,
2565 this.anchorPoint,
2566 super.traversalEdgeBehavior,
2567 super.directionalTraversalEdgeBehavior,
2568 this.fullscreenDialog = false,
2569 }) : _pageBuilder = pageBuilder,
2570 _barrierDismissible = barrierDismissible,
2571 _barrierLabel = barrierLabel,
2572 _barrierColor = barrierColor,
2573 _transitionDuration = transitionDuration,
2574 _transitionBuilder = transitionBuilder;
2575
2576 final RoutePageBuilder _pageBuilder;
2577
2578 @override
2579 bool get barrierDismissible => _barrierDismissible;
2580 final bool _barrierDismissible;
2581
2582 @override
2583 String? get barrierLabel => _barrierLabel;
2584 final String? _barrierLabel;
2585
2586 @override
2587 Color? get barrierColor => _barrierColor;
2588 final Color? _barrierColor;
2589
2590 @override
2591 Duration get transitionDuration => _transitionDuration;
2592 final Duration _transitionDuration;
2593
2594 final RouteTransitionsBuilder? _transitionBuilder;
2595
2596 /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint}
2597 final Offset? anchorPoint;
2598
2599 /// {@template flutter.widgets.RawDialogRoute.fullscreenDialog}
2600 /// Whether this route is a full-screen dialog.
2601 ///
2602 /// In Material and Cupertino, being fullscreen has the effects of making
2603 /// the app bars have a close button instead of a back button. On
2604 /// iOS, dialogs transitions animate differently and are also not closeable
2605 /// with the back swipe gesture.
2606 /// {@endtemplate}
2607 @override
2608 final bool fullscreenDialog;
2609
2610 @override
2611 Widget buildPage(
2612 BuildContext context,
2613 Animation<double> animation,
2614 Animation<double> secondaryAnimation,
2615 ) {
2616 return Semantics(
2617 scopesRoute: true,
2618 explicitChildNodes: true,
2619 child: DisplayFeatureSubScreen(
2620 anchorPoint: anchorPoint,
2621 child: _pageBuilder(context, animation, secondaryAnimation),
2622 ),
2623 );
2624 }
2625
2626 @override
2627 Widget buildTransitions(
2628 BuildContext context,
2629 Animation<double> animation,
2630 Animation<double> secondaryAnimation,
2631 Widget child,
2632 ) {
2633 if (_transitionBuilder == null) {
2634 // Some default transition.
2635 return FadeTransition(opacity: animation, child: child);
2636 }
2637 return _transitionBuilder(context, animation, secondaryAnimation, child);
2638 }
2639}
2640
2641/// Displays a dialog above the current contents of the app.
2642///
2643/// This function allows for customization of aspects of the dialog popup.
2644///
2645/// This function takes a `pageBuilder` which is used to build the primary
2646/// content of the route (typically a dialog widget). Content below the dialog
2647/// is dimmed with a [ModalBarrier]. The widget returned by the `pageBuilder`
2648/// does not share a context with the location that [showGeneralDialog] is
2649/// originally called from. Use a [StatefulBuilder] or a custom
2650/// [StatefulWidget] if the dialog needs to update dynamically.
2651///
2652/// The `context` argument is used to look up the [Navigator] for the
2653/// dialog. It is only used when the method is called. Its corresponding widget
2654/// can be safely removed from the tree before the dialog is closed.
2655///
2656/// The `useRootNavigator` argument is used to determine whether to push the
2657/// dialog to the [Navigator] furthest from or nearest to the given `context`.
2658/// By default, `useRootNavigator` is `true` and the dialog route created by
2659/// this method is pushed to the root navigator.
2660///
2661/// If the application has multiple [Navigator] objects, it may be necessary to
2662/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
2663/// dialog rather than just `Navigator.pop(context, result)`.
2664///
2665/// The `barrierDismissible` argument is used to determine whether this route
2666/// can be dismissed by tapping the modal barrier. This argument defaults
2667/// to false. If `barrierDismissible` is true, a non-null `barrierLabel` must be
2668/// provided.
2669///
2670/// The `barrierLabel` argument is the semantic label used for a dismissible
2671/// barrier. This argument defaults to `null`.
2672///
2673/// The `barrierColor` argument is the color used for the modal barrier. This
2674/// argument defaults to `Color(0x80000000)`.
2675///
2676/// The `transitionDuration` argument is used to determine how long it takes
2677/// for the route to arrive on or leave off the screen. This argument defaults
2678/// to 200 milliseconds.
2679///
2680/// The `transitionBuilder` argument is used to define how the route arrives on
2681/// and leaves off the screen. By default, the transition is a linear fade of
2682/// the page's contents.
2683///
2684/// The `routeSettings` will be used in the construction of the dialog's route.
2685/// See [RouteSettings] for more details.
2686///
2687/// {@macro flutter.material.dialog.requestFocus}
2688/// {@macro flutter.widgets.navigator.Route.requestFocus}
2689///
2690/// {@macro flutter.widgets.RawDialogRoute}
2691///
2692/// Returns a [Future] that resolves to the value (if any) that was passed to
2693/// [Navigator.pop] when the dialog was closed.
2694///
2695/// ### State Restoration in Dialogs
2696///
2697/// Using this method will not enable state restoration for the dialog. In order
2698/// to enable state restoration for a dialog, use [Navigator.restorablePush]
2699/// or [Navigator.restorablePushNamed] with [RawDialogRoute].
2700///
2701/// For more information about state restoration, see [RestorationManager].
2702///
2703/// {@tool sample}
2704/// This sample demonstrates how to create a restorable dialog. This is
2705/// accomplished by enabling state restoration by specifying
2706/// [WidgetsApp.restorationScopeId] and using [Navigator.restorablePush] to
2707/// push [RawDialogRoute] when the button is tapped.
2708///
2709/// {@macro flutter.widgets.RestorationManager}
2710///
2711/// ** See code in examples/api/lib/widgets/routes/show_general_dialog.0.dart **
2712/// {@end-tool}
2713///
2714/// See also:
2715///
2716/// * [DisplayFeatureSubScreen], which documents the specifics of how
2717/// [DisplayFeature]s can split the screen into sub-screens.
2718/// * [showDialog], which displays a Material-style dialog.
2719/// * [showCupertinoDialog], which displays an iOS-style dialog.
2720Future<T?> showGeneralDialog<T extends Object?>({
2721 required BuildContext context,
2722 required RoutePageBuilder pageBuilder,
2723 bool barrierDismissible = false,
2724 String? barrierLabel,
2725 Color barrierColor = const Color(0x80000000),
2726 Duration transitionDuration = const Duration(milliseconds: 200),
2727 RouteTransitionsBuilder? transitionBuilder,
2728 bool useRootNavigator = true,
2729 bool fullscreenDialog = false,
2730 RouteSettings? routeSettings,
2731 Offset? anchorPoint,
2732 bool? requestFocus,
2733}) {
2734 assert(!barrierDismissible || barrierLabel != null);
2735 return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(
2736 RawDialogRoute<T>(
2737 pageBuilder: pageBuilder,
2738 barrierDismissible: barrierDismissible,
2739 barrierLabel: barrierLabel,
2740 barrierColor: barrierColor,
2741 transitionDuration: transitionDuration,
2742 transitionBuilder: transitionBuilder,
2743 settings: routeSettings,
2744 anchorPoint: anchorPoint,
2745 requestFocus: requestFocus,
2746 fullscreenDialog: fullscreenDialog,
2747 ),
2748 );
2749}
2750
2751/// Signature for the function that builds a route's primary contents.
2752/// Used in [PageRouteBuilder] and [showGeneralDialog].
2753///
2754/// See [ModalRoute.buildPage] for complete definition of the parameters.
2755typedef RoutePageBuilder =
2756 Widget Function(
2757 BuildContext context,
2758 Animation<double> animation,
2759 Animation<double> secondaryAnimation,
2760 );
2761
2762/// Signature for the function that builds a route's transitions.
2763/// Used in [PageRouteBuilder] and [showGeneralDialog].
2764///
2765/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
2766typedef RouteTransitionsBuilder =
2767 Widget Function(
2768 BuildContext context,
2769 Animation<double> animation,
2770 Animation<double> secondaryAnimation,
2771 Widget child,
2772 );
2773
2774/// A callback type for informing that a navigation pop has been invoked,
2775/// whether or not it was handled successfully.
2776///
2777/// Accepts a didPop boolean indicating whether or not back navigation
2778/// succeeded.
2779///
2780/// The `result` contains the pop result.
2781typedef PopInvokedWithResultCallback<T> = void Function(bool didPop, T? result);
2782
2783/// Allows listening to and preventing pops.
2784///
2785/// Can be registered in [ModalRoute] to listen to pops with [onPopInvokedWithResult] or
2786/// to enable/disable them with [canPopNotifier].
2787///
2788/// See also:
2789///
2790/// * [PopScope], which provides similar functionality in a widget.
2791/// * [ModalRoute.registerPopEntry], which unregisters instances of this.
2792/// * [ModalRoute.unregisterPopEntry], which unregisters instances of this.
2793abstract class PopEntry<T> {
2794 /// {@macro flutter.widgets.PopScope.onPopInvokedWithResult}
2795 @Deprecated(
2796 'Use onPopInvokedWithResult instead. '
2797 'This feature was deprecated after v3.22.0-12.0.pre.',
2798 )
2799 void onPopInvoked(bool didPop) {}
2800
2801 /// {@macro flutter.widgets.PopScope.onPopInvokedWithResult}
2802 void onPopInvokedWithResult(bool didPop, T? result) => onPopInvoked(didPop);
2803
2804 /// {@macro flutter.widgets.PopScope.canPop}
2805 ValueListenable<bool> get canPopNotifier;
2806
2807 @override
2808 String toString() {
2809 return 'PopEntry canPop: ${canPopNotifier.value}, onPopInvoked: $onPopInvokedWithResult';
2810 }
2811}
2812