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