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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com