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