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