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