| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/services.dart'; |
| |
| import 'actions.dart'; |
| import 'basic.dart'; |
| import 'display_feature_sub_screen.dart'; |
| import 'focus_manager.dart'; |
| import 'focus_scope.dart'; |
| import 'framework.dart'; |
| import 'modal_barrier.dart'; |
| import 'navigator.dart'; |
| import 'overlay.dart'; |
| import 'page_storage.dart'; |
| import 'primary_scroll_controller.dart'; |
| import 'restoration.dart'; |
| import 'scroll_controller.dart'; |
| import 'transitions.dart'; |
| |
| // Examples can assume: |
| // late NavigatorState navigator; |
| // late BuildContext context; |
| // Future<bool> askTheUserIfTheyAreSure() async { return true; } |
| // abstract class MyWidget extends StatefulWidget { const MyWidget({super.key}); } |
| // late dynamic _myState, newValue; |
| // late StateSetter setState; |
| |
| /// A route that displays widgets in the [Navigator]'s [Overlay]. |
| /// |
| /// See also: |
| /// |
| /// * [Route], which documents the meaning of the `T` generic type argument. |
| abstract class OverlayRoute<T> extends Route<T> { |
| /// Creates a route that knows how to interact with an [Overlay]. |
| OverlayRoute({ |
| super.settings, |
| }); |
| |
| /// Subclasses should override this getter to return the builders for the overlay. |
| @factory |
| Iterable<OverlayEntry> createOverlayEntries(); |
| |
| @override |
| List<OverlayEntry> get overlayEntries => _overlayEntries; |
| final List<OverlayEntry> _overlayEntries = <OverlayEntry>[]; |
| |
| @override |
| void install() { |
| assert(_overlayEntries.isEmpty); |
| _overlayEntries.addAll(createOverlayEntries()); |
| super.install(); |
| } |
| |
| /// Controls whether [didPop] calls [NavigatorState.finalizeRoute]. |
| /// |
| /// If true, this route removes its overlay entries during [didPop]. |
| /// Subclasses can override this getter if they want to delay finalization |
| /// (for example to animate the route's exit before removing it from the |
| /// overlay). |
| /// |
| /// Subclasses that return false from [finishedWhenPopped] are responsible for |
| /// calling [NavigatorState.finalizeRoute] themselves. |
| @protected |
| bool get finishedWhenPopped => true; |
| |
| @override |
| bool didPop(T? result) { |
| final bool returnValue = super.didPop(result); |
| assert(returnValue); |
| if (finishedWhenPopped) { |
| navigator!.finalizeRoute(this); |
| } |
| return returnValue; |
| } |
| |
| @override |
| void dispose() { |
| _overlayEntries.clear(); |
| super.dispose(); |
| } |
| } |
| |
| /// A route with entrance and exit transitions. |
| /// |
| /// See also: |
| /// |
| /// * [Route], which documents the meaning of the `T` generic type argument. |
| abstract class TransitionRoute<T> extends OverlayRoute<T> { |
| /// Creates a route that animates itself when it is pushed or popped. |
| TransitionRoute({ |
| super.settings, |
| }); |
| |
| /// This future completes only once the transition itself has finished, after |
| /// the overlay entries have been removed from the navigator's overlay. |
| /// |
| /// This future completes once the animation has been dismissed. That will be |
| /// after [popped], because [popped] typically completes before the animation |
| /// even starts, as soon as the route is popped. |
| Future<T?> get completed => _transitionCompleter.future; |
| final Completer<T?> _transitionCompleter = Completer<T?>(); |
| |
| /// Handle to the performance mode request. |
| /// |
| /// When the route is animating, the performance mode is requested. It is then |
| /// disposed when the animation ends. Requesting [DartPerformanceMode.latency] |
| /// indicates to the engine that the transition is latency sensitive and to delay |
| /// non-essential work while this handle is active. |
| PerformanceModeRequestHandle? _performanceModeRequestHandle; |
| |
| /// {@template flutter.widgets.TransitionRoute.transitionDuration} |
| /// The duration the transition going forwards. |
| /// |
| /// See also: |
| /// |
| /// * [reverseTransitionDuration], which controls the duration of the |
| /// transition when it is in reverse. |
| /// {@endtemplate} |
| Duration get transitionDuration; |
| |
| /// {@template flutter.widgets.TransitionRoute.reverseTransitionDuration} |
| /// The duration the transition going in reverse. |
| /// |
| /// By default, the reverse transition duration is set to the value of |
| /// the forwards [transitionDuration]. |
| /// {@endtemplate} |
| Duration get reverseTransitionDuration => transitionDuration; |
| |
| /// {@template flutter.widgets.TransitionRoute.opaque} |
| /// Whether the route obscures previous routes when the transition is complete. |
| /// |
| /// When an opaque route's entrance transition is complete, the routes behind |
| /// the opaque route will not be built to save resources. |
| /// {@endtemplate} |
| bool get opaque; |
| |
| /// {@template flutter.widgets.TransitionRoute.allowSnapshotting} |
| /// Whether the route transition will prefer to animate a snapshot of the |
| /// entering/exiting routes. |
| /// |
| /// When this value is true, certain route transitions (such as the Android |
| /// zoom page transition) will snapshot the entering and exiting routes. |
| /// These snapshots are then animated in place of the underlying widgets to |
| /// improve performance of the transition. |
| /// |
| /// Generally this means that animations that occur on the entering/exiting |
| /// route while the route animation plays may appear frozen - unless they |
| /// are a hero animation or something that is drawn in a separate overlay. |
| /// {@endtemplate} |
| bool get allowSnapshotting => true; |
| |
| // This ensures that if we got to the dismissed state while still current, |
| // we will still be disposed when we are eventually popped. |
| // |
| // This situation arises when dealing with the Cupertino dismiss gesture. |
| @override |
| bool get finishedWhenPopped => _controller!.status == AnimationStatus.dismissed && !_popFinalized; |
| |
| bool _popFinalized = false; |
| |
| /// The animation that drives the route's transition and the previous route's |
| /// forward transition. |
| Animation<double>? get animation => _animation; |
| Animation<double>? _animation; |
| |
| /// The animation controller that the route uses to drive the transitions. |
| /// |
| /// The animation itself is exposed by the [animation] property. |
| @protected |
| AnimationController? get controller => _controller; |
| AnimationController? _controller; |
| |
| /// The animation for the route being pushed on top of this route. This |
| /// animation lets this route coordinate with the entrance and exit transition |
| /// of route pushed on top of this route. |
| Animation<double>? get secondaryAnimation => _secondaryAnimation; |
| final ProxyAnimation _secondaryAnimation = ProxyAnimation(kAlwaysDismissedAnimation); |
| |
| /// Whether to takeover the [controller] created by [createAnimationController]. |
| /// |
| /// If true, this route will call [AnimationController.dispose] when the |
| /// controller is no longer needed. |
| /// If false, the controller should be disposed by whoever owned it. |
| /// |
| /// It defaults to `true`. |
| bool willDisposeAnimationController = true; |
| |
| /// Called to create the animation controller that will drive the transitions to |
| /// this route from the previous one, and back to the previous route from this |
| /// one. |
| /// |
| /// The returned controller will be disposed by [AnimationController.dispose] |
| /// if the [willDisposeAnimationController] is `true`. |
| AnimationController createAnimationController() { |
| assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); |
| final Duration duration = transitionDuration; |
| final Duration reverseDuration = reverseTransitionDuration; |
| assert(duration != null && duration >= Duration.zero); |
| return AnimationController( |
| duration: duration, |
| reverseDuration: reverseDuration, |
| debugLabel: debugLabel, |
| vsync: navigator!, |
| ); |
| } |
| |
| /// Called to create the animation that exposes the current progress of |
| /// the transition controlled by the animation controller created by |
| /// [createAnimationController()]. |
| Animation<double> createAnimation() { |
| assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); |
| assert(_controller != null); |
| return _controller!.view; |
| } |
| |
| T? _result; |
| |
| void _handleStatusChanged(AnimationStatus status) { |
| switch (status) { |
| case AnimationStatus.completed: |
| if (overlayEntries.isNotEmpty) { |
| overlayEntries.first.opaque = opaque; |
| } |
| _performanceModeRequestHandle?.dispose(); |
| _performanceModeRequestHandle = null; |
| break; |
| case AnimationStatus.forward: |
| case AnimationStatus.reverse: |
| if (overlayEntries.isNotEmpty) { |
| overlayEntries.first.opaque = false; |
| } |
| _performanceModeRequestHandle ??= |
| SchedulerBinding.instance |
| .requestPerformanceMode(ui.DartPerformanceMode.latency); |
| break; |
| case AnimationStatus.dismissed: |
| // We might still be an active route if a subclass is controlling the |
| // transition and hits the dismissed status. For example, the iOS |
| // back gesture drives this animation to the dismissed status before |
| // removing the route and disposing it. |
| if (!isActive) { |
| navigator!.finalizeRoute(this); |
| _popFinalized = true; |
| _performanceModeRequestHandle?.dispose(); |
| _performanceModeRequestHandle = null; |
| } |
| break; |
| } |
| } |
| |
| @override |
| void install() { |
| assert(!_transitionCompleter.isCompleted, 'Cannot install a $runtimeType after disposing it.'); |
| _controller = createAnimationController(); |
| assert(_controller != null, '$runtimeType.createAnimationController() returned null.'); |
| _animation = createAnimation() |
| ..addStatusListener(_handleStatusChanged); |
| assert(_animation != null, '$runtimeType.createAnimation() returned null.'); |
| super.install(); |
| if (_animation!.isCompleted && overlayEntries.isNotEmpty) { |
| overlayEntries.first.opaque = opaque; |
| } |
| } |
| |
| @override |
| TickerFuture didPush() { |
| assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().'); |
| assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); |
| super.didPush(); |
| return _controller!.forward(); |
| } |
| |
| @override |
| void didAdd() { |
| assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().'); |
| assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); |
| super.didAdd(); |
| _controller!.value = _controller!.upperBound; |
| } |
| |
| @override |
| void didReplace(Route<dynamic>? oldRoute) { |
| assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().'); |
| assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); |
| if (oldRoute is TransitionRoute) { |
| _controller!.value = oldRoute._controller!.value; |
| } |
| super.didReplace(oldRoute); |
| } |
| |
| @override |
| bool didPop(T? result) { |
| assert(_controller != null, '$runtimeType.didPop called before calling install() or after calling dispose().'); |
| assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); |
| _result = result; |
| _controller!.reverse(); |
| return super.didPop(result); |
| } |
| |
| @override |
| void didPopNext(Route<dynamic> nextRoute) { |
| assert(_controller != null, '$runtimeType.didPopNext called before calling install() or after calling dispose().'); |
| assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); |
| _updateSecondaryAnimation(nextRoute); |
| super.didPopNext(nextRoute); |
| } |
| |
| @override |
| void didChangeNext(Route<dynamic>? nextRoute) { |
| assert(_controller != null, '$runtimeType.didChangeNext called before calling install() or after calling dispose().'); |
| assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); |
| _updateSecondaryAnimation(nextRoute); |
| super.didChangeNext(nextRoute); |
| } |
| |
| // A callback method that disposes existing train hopping animation and |
| // removes its listener. |
| // |
| // This property is non-null if there is a train hopping in progress, and the |
| // caller must reset this property to null after it is called. |
| VoidCallback? _trainHoppingListenerRemover; |
| |
| void _updateSecondaryAnimation(Route<dynamic>? nextRoute) { |
| // There is an existing train hopping in progress. Unfortunately, we cannot |
| // dispose current train hopping animation until we replace it with a new |
| // animation. |
| final VoidCallback? previousTrainHoppingListenerRemover = _trainHoppingListenerRemover; |
| _trainHoppingListenerRemover = null; |
| |
| if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) { |
| final Animation<double>? current = _secondaryAnimation.parent; |
| if (current != null) { |
| final Animation<double> currentTrain = (current is TrainHoppingAnimation ? current.currentTrain : current)!; |
| final Animation<double> nextTrain = nextRoute._animation!; |
| if ( |
| currentTrain.value == nextTrain.value || |
| nextTrain.status == AnimationStatus.completed || |
| nextTrain.status == AnimationStatus.dismissed |
| ) { |
| _setSecondaryAnimation(nextTrain, nextRoute.completed); |
| } else { |
| // Two trains animate at different values. We have to do train hopping. |
| // There are three possibilities of train hopping: |
| // 1. We hop on the nextTrain when two trains meet in the middle using |
| // TrainHoppingAnimation. |
| // 2. There is no chance to hop on nextTrain because two trains never |
| // cross each other. We have to directly set the animation to |
| // nextTrain once the nextTrain stops animating. |
| // 3. A new _updateSecondaryAnimation is called before train hopping |
| // finishes. We leave a listener remover for the next call to |
| // properly clean up the existing train hopping. |
| TrainHoppingAnimation? newAnimation; |
| void jumpOnAnimationEnd(AnimationStatus status) { |
| switch (status) { |
| case AnimationStatus.completed: |
| case AnimationStatus.dismissed: |
| // The nextTrain has stopped animating without train hopping. |
| // Directly sets the secondary animation and disposes the |
| // TrainHoppingAnimation. |
| _setSecondaryAnimation(nextTrain, nextRoute.completed); |
| if (_trainHoppingListenerRemover != null) { |
| _trainHoppingListenerRemover!(); |
| _trainHoppingListenerRemover = null; |
| } |
| break; |
| case AnimationStatus.forward: |
| case AnimationStatus.reverse: |
| break; |
| } |
| } |
| _trainHoppingListenerRemover = () { |
| nextTrain.removeStatusListener(jumpOnAnimationEnd); |
| newAnimation?.dispose(); |
| }; |
| nextTrain.addStatusListener(jumpOnAnimationEnd); |
| newAnimation = TrainHoppingAnimation( |
| currentTrain, |
| nextTrain, |
| onSwitchedTrain: () { |
| assert(_secondaryAnimation.parent == newAnimation); |
| assert(newAnimation!.currentTrain == nextRoute._animation); |
| // We can hop on the nextTrain, so we don't need to listen to |
| // whether the nextTrain has stopped. |
| _setSecondaryAnimation(newAnimation!.currentTrain, nextRoute.completed); |
| if (_trainHoppingListenerRemover != null) { |
| _trainHoppingListenerRemover!(); |
| _trainHoppingListenerRemover = null; |
| } |
| }, |
| ); |
| _setSecondaryAnimation(newAnimation, nextRoute.completed); |
| } |
| } else { |
| _setSecondaryAnimation(nextRoute._animation, nextRoute.completed); |
| } |
| } else { |
| _setSecondaryAnimation(kAlwaysDismissedAnimation); |
| } |
| // Finally, we dispose any previous train hopping animation because it |
| // has been successfully updated at this point. |
| if (previousTrainHoppingListenerRemover != null) { |
| previousTrainHoppingListenerRemover(); |
| } |
| } |
| |
| void _setSecondaryAnimation(Animation<double>? animation, [Future<dynamic>? disposed]) { |
| _secondaryAnimation.parent = animation; |
| // Releases the reference to the next route's animation when that route |
| // is disposed. |
| disposed?.then((dynamic _) { |
| if (_secondaryAnimation.parent == animation) { |
| _secondaryAnimation.parent = kAlwaysDismissedAnimation; |
| if (animation is TrainHoppingAnimation) { |
| animation.dispose(); |
| } |
| } |
| }); |
| } |
| |
| /// Returns true if this route supports a transition animation that runs |
| /// when [nextRoute] is pushed on top of it or when [nextRoute] is popped |
| /// off of it. |
| /// |
| /// Subclasses can override this method to restrict the set of routes they |
| /// need to coordinate transitions with. |
| /// |
| /// If true, and `nextRoute.canTransitionFrom()` is true, then the |
| /// [ModalRoute.buildTransitions] `secondaryAnimation` will run from 0.0 - 1.0 |
| /// when [nextRoute] is pushed on top of this one. Similarly, if |
| /// the [nextRoute] is popped off of this route, the |
| /// `secondaryAnimation` will run from 1.0 - 0.0. |
| /// |
| /// If false, this route's [ModalRoute.buildTransitions] `secondaryAnimation` parameter |
| /// value will be [kAlwaysDismissedAnimation]. In other words, this route |
| /// will not animate when [nextRoute] is pushed on top of it or when |
| /// [nextRoute] is popped off of it. |
| /// |
| /// Returns true by default. |
| /// |
| /// See also: |
| /// |
| /// * [canTransitionFrom], which must be true for [nextRoute] for the |
| /// [ModalRoute.buildTransitions] `secondaryAnimation` to run. |
| bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true; |
| |
| /// Returns true if [previousRoute] should animate when this route |
| /// is pushed on top of it or when then this route is popped off of it. |
| /// |
| /// Subclasses can override this method to restrict the set of routes they |
| /// need to coordinate transitions with. |
| /// |
| /// If true, and `previousRoute.canTransitionTo()` is true, then the |
| /// previous route's [ModalRoute.buildTransitions] `secondaryAnimation` will |
| /// run from 0.0 - 1.0 when this route is pushed on top of |
| /// it. Similarly, if this route is popped off of [previousRoute] |
| /// the previous route's `secondaryAnimation` will run from 1.0 - 0.0. |
| /// |
| /// If false, then the previous route's [ModalRoute.buildTransitions] |
| /// `secondaryAnimation` value will be kAlwaysDismissedAnimation. In |
| /// other words [previousRoute] will not animate when this route is |
| /// pushed on top of it or when then this route is popped off of it. |
| /// |
| /// Returns true by default. |
| /// |
| /// See also: |
| /// |
| /// * [canTransitionTo], which must be true for [previousRoute] for its |
| /// [ModalRoute.buildTransitions] `secondaryAnimation` to run. |
| bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => true; |
| |
| @override |
| void dispose() { |
| assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.'); |
| _animation?.removeStatusListener(_handleStatusChanged); |
| _performanceModeRequestHandle?.dispose(); |
| _performanceModeRequestHandle = null; |
| if (willDisposeAnimationController) { |
| _controller?.dispose(); |
| } |
| _transitionCompleter.complete(_result); |
| super.dispose(); |
| } |
| |
| /// A short description of this route useful for debugging. |
| String get debugLabel => objectRuntimeType(this, 'TransitionRoute'); |
| |
| @override |
| String toString() => '${objectRuntimeType(this, 'TransitionRoute')}(animation: $_controller)'; |
| } |
| |
| /// An entry in the history of a [LocalHistoryRoute]. |
| class LocalHistoryEntry { |
| /// Creates an entry in the history of a [LocalHistoryRoute]. |
| /// |
| /// The [impliesAppBarDismissal] defaults to true if not provided. |
| LocalHistoryEntry({ this.onRemove, this.impliesAppBarDismissal = true }); |
| |
| /// Called when this entry is removed from the history of its associated [LocalHistoryRoute]. |
| final VoidCallback? onRemove; |
| |
| LocalHistoryRoute<dynamic>? _owner; |
| |
| /// Whether an [AppBar] in the route this entry belongs to should |
| /// automatically add a back button or close button. |
| /// |
| /// Defaults to true. |
| final bool impliesAppBarDismissal; |
| |
| /// Remove this entry from the history of its associated [LocalHistoryRoute]. |
| void remove() { |
| _owner?.removeLocalHistoryEntry(this); |
| assert(_owner == null); |
| } |
| |
| void _notifyRemoved() { |
| onRemove?.call(); |
| } |
| } |
| |
| /// A mixin used by routes to handle back navigations internally by popping a list. |
| /// |
| /// When a [Navigator] is instructed to pop, the current route is given an |
| /// opportunity to handle the pop internally. A [LocalHistoryRoute] handles the |
| /// pop internally if its list of local history entries is non-empty. Rather |
| /// than being removed as the current route, the most recent [LocalHistoryEntry] |
| /// is removed from the list and its [LocalHistoryEntry.onRemove] is called. |
| /// |
| /// See also: |
| /// |
| /// * [Route], which documents the meaning of the `T` generic type argument. |
| mixin LocalHistoryRoute<T> on Route<T> { |
| List<LocalHistoryEntry>? _localHistory; |
| int _entriesImpliesAppBarDismissal = 0; |
| /// Adds a local history entry to this route. |
| /// |
| /// When asked to pop, if this route has any local history entries, this route |
| /// will handle the pop internally by removing the most recently added local |
| /// history entry. |
| /// |
| /// The given local history entry must not already be part of another local |
| /// history route. |
| /// |
| /// {@tool snippet} |
| /// |
| /// The following example is an app with 2 pages: `HomePage` and `SecondPage`. |
| /// The `HomePage` can navigate to the `SecondPage`. |
| /// |
| /// The `SecondPage` uses a [LocalHistoryEntry] to implement local navigation |
| /// within that page. Pressing 'show rectangle' displays a red rectangle and |
| /// adds a local history entry. At that point, pressing the '< back' button |
| /// pops the latest route, which is the local history entry, and the red |
| /// rectangle disappears. Pressing the '< back' button a second time |
| /// once again pops the latest route, which is the `SecondPage`, itself. |
| /// Therefore, the second press navigates back to the `HomePage`. |
| /// |
| /// ```dart |
| /// class App extends StatelessWidget { |
| /// const App({super.key}); |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return MaterialApp( |
| /// initialRoute: '/', |
| /// routes: <String, WidgetBuilder>{ |
| /// '/': (BuildContext context) => const HomePage(), |
| /// '/second_page': (BuildContext context) => const SecondPage(), |
| /// }, |
| /// ); |
| /// } |
| /// } |
| /// |
| /// class HomePage extends StatefulWidget { |
| /// const HomePage({super.key}); |
| /// |
| /// @override |
| /// State<HomePage> createState() => _HomePageState(); |
| /// } |
| /// |
| /// class _HomePageState extends State<HomePage> { |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return Scaffold( |
| /// body: Center( |
| /// child: Column( |
| /// mainAxisSize: MainAxisSize.min, |
| /// children: <Widget>[ |
| /// const Text('HomePage'), |
| /// // Press this button to open the SecondPage. |
| /// ElevatedButton( |
| /// child: const Text('Second Page >'), |
| /// onPressed: () { |
| /// Navigator.pushNamed(context, '/second_page'); |
| /// }, |
| /// ), |
| /// ], |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// |
| /// class SecondPage extends StatefulWidget { |
| /// const SecondPage({super.key}); |
| /// |
| /// @override |
| /// State<SecondPage> createState() => _SecondPageState(); |
| /// } |
| /// |
| /// class _SecondPageState extends State<SecondPage> { |
| /// |
| /// bool _showRectangle = false; |
| /// |
| /// Future<void> _navigateLocallyToShowRectangle() async { |
| /// // This local history entry essentially represents the display of the red |
| /// // rectangle. When this local history entry is removed, we hide the red |
| /// // rectangle. |
| /// setState(() => _showRectangle = true); |
| /// ModalRoute.of(context)?.addLocalHistoryEntry( |
| /// LocalHistoryEntry( |
| /// onRemove: () { |
| /// // Hide the red rectangle. |
| /// setState(() => _showRectangle = false); |
| /// } |
| /// ) |
| /// ); |
| /// } |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// final Widget localNavContent = _showRectangle |
| /// ? Container( |
| /// width: 100.0, |
| /// height: 100.0, |
| /// color: Colors.red, |
| /// ) |
| /// : ElevatedButton( |
| /// onPressed: _navigateLocallyToShowRectangle, |
| /// child: const Text('Show Rectangle'), |
| /// ); |
| /// |
| /// return Scaffold( |
| /// body: Center( |
| /// child: Column( |
| /// mainAxisAlignment: MainAxisAlignment.center, |
| /// children: <Widget>[ |
| /// localNavContent, |
| /// ElevatedButton( |
| /// child: const Text('< Back'), |
| /// onPressed: () { |
| /// // Pop a route. If this is pressed while the red rectangle is |
| /// // visible then it will pop our local history entry, which |
| /// // will hide the red rectangle. Otherwise, the SecondPage will |
| /// // navigate back to the HomePage. |
| /// Navigator.of(context).pop(); |
| /// }, |
| /// ), |
| /// ], |
| /// ), |
| /// ), |
| /// ); |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| void addLocalHistoryEntry(LocalHistoryEntry entry) { |
| assert(entry._owner == null); |
| entry._owner = this; |
| _localHistory ??= <LocalHistoryEntry>[]; |
| final bool wasEmpty = _localHistory!.isEmpty; |
| _localHistory!.add(entry); |
| bool internalStateChanged = false; |
| if (entry.impliesAppBarDismissal) { |
| internalStateChanged = _entriesImpliesAppBarDismissal == 0; |
| _entriesImpliesAppBarDismissal += 1; |
| } |
| if (wasEmpty || internalStateChanged) { |
| changedInternalState(); |
| } |
| } |
| |
| /// Remove a local history entry from this route. |
| /// |
| /// The entry's [LocalHistoryEntry.onRemove] callback, if any, will be called |
| /// synchronously. |
| void removeLocalHistoryEntry(LocalHistoryEntry entry) { |
| assert(entry != null); |
| assert(entry._owner == this); |
| assert(_localHistory!.contains(entry)); |
| bool internalStateChanged = false; |
| if (_localHistory!.remove(entry) && entry.impliesAppBarDismissal) { |
| _entriesImpliesAppBarDismissal -= 1; |
| internalStateChanged = _entriesImpliesAppBarDismissal == 0; |
| } |
| entry._owner = null; |
| entry._notifyRemoved(); |
| if (_localHistory!.isEmpty || internalStateChanged) { |
| assert(_entriesImpliesAppBarDismissal == 0); |
| if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { |
| // The local history might be removed as a result of disposing inactive |
| // elements during finalizeTree. The state is locked at this moment, and |
| // we can only notify state has changed in the next frame. |
| SchedulerBinding.instance.addPostFrameCallback((Duration duration) { |
| changedInternalState(); |
| }); |
| } else { |
| changedInternalState(); |
| } |
| } |
| } |
| |
| @override |
| Future<RoutePopDisposition> willPop() async { |
| if (willHandlePopInternally) { |
| return RoutePopDisposition.pop; |
| } |
| return super.willPop(); |
| } |
| |
| @override |
| bool didPop(T? result) { |
| if (_localHistory != null && _localHistory!.isNotEmpty) { |
| final LocalHistoryEntry entry = _localHistory!.removeLast(); |
| assert(entry._owner == this); |
| entry._owner = null; |
| entry._notifyRemoved(); |
| bool internalStateChanged = false; |
| if (entry.impliesAppBarDismissal) { |
| _entriesImpliesAppBarDismissal -= 1; |
| internalStateChanged = _entriesImpliesAppBarDismissal == 0; |
| } |
| if (_localHistory!.isEmpty || internalStateChanged) { |
| changedInternalState(); |
| } |
| return false; |
| } |
| return super.didPop(result); |
| } |
| |
| @override |
| bool get willHandlePopInternally { |
| return _localHistory != null && _localHistory!.isNotEmpty; |
| } |
| } |
| |
| class _DismissModalAction extends DismissAction { |
| _DismissModalAction(this.context); |
| |
| final BuildContext context; |
| |
| @override |
| bool isEnabled(DismissIntent intent) { |
| final ModalRoute<dynamic> route = ModalRoute.of<dynamic>(context)!; |
| return route.barrierDismissible; |
| } |
| |
| @override |
| Object invoke(DismissIntent intent) { |
| return Navigator.of(context).maybePop(); |
| } |
| } |
| |
| class _ModalScopeStatus extends InheritedWidget { |
| const _ModalScopeStatus({ |
| required this.isCurrent, |
| required this.canPop, |
| required this.impliesAppBarDismissal, |
| required this.route, |
| required super.child, |
| }) : assert(isCurrent != null), |
| assert(canPop != null), |
| assert(route != null), |
| assert(child != null); |
| |
| final bool isCurrent; |
| final bool canPop; |
| final bool impliesAppBarDismissal; |
| final Route<dynamic> route; |
| |
| @override |
| bool updateShouldNotify(_ModalScopeStatus old) { |
| return isCurrent != old.isCurrent || |
| canPop != old.canPop || |
| impliesAppBarDismissal != old.impliesAppBarDismissal || |
| route != old.route; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder description) { |
| super.debugFillProperties(description); |
| description.add(FlagProperty('isCurrent', value: isCurrent, ifTrue: 'active', ifFalse: 'inactive')); |
| description.add(FlagProperty('canPop', value: canPop, ifTrue: 'can pop')); |
| description.add(FlagProperty('impliesAppBarDismissal', value: impliesAppBarDismissal, ifTrue: 'implies app bar dismissal')); |
| } |
| } |
| |
| class _ModalScope<T> extends StatefulWidget { |
| const _ModalScope({ |
| super.key, |
| required this.route, |
| }); |
| |
| final ModalRoute<T> route; |
| |
| @override |
| _ModalScopeState<T> createState() => _ModalScopeState<T>(); |
| } |
| |
| class _ModalScopeState<T> extends State<_ModalScope<T>> { |
| // We cache the result of calling the route's buildPage, and clear the cache |
| // whenever the dependencies change. This implements the contract described in |
| // the documentation for buildPage, namely that it gets called once, unless |
| // something like a ModalRoute.of() dependency triggers an update. |
| Widget? _page; |
| |
| // This is the combination of the two animations for the route. |
| late Listenable _listenable; |
| |
| /// The node this scope will use for its root [FocusScope] widget. |
| final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope'); |
| final ScrollController primaryScrollController = ScrollController(); |
| |
| @override |
| void initState() { |
| super.initState(); |
| final List<Listenable> animations = <Listenable>[ |
| if (widget.route.animation != null) widget.route.animation!, |
| if (widget.route.secondaryAnimation != null) widget.route.secondaryAnimation!, |
| ]; |
| _listenable = Listenable.merge(animations); |
| if (widget.route.isCurrent && _shouldRequestFocus) { |
| widget.route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode); |
| } |
| } |
| |
| @override |
| void didUpdateWidget(_ModalScope<T> oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| assert(widget.route == oldWidget.route); |
| if (widget.route.isCurrent && _shouldRequestFocus) { |
| widget.route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode); |
| } |
| } |
| |
| @override |
| void didChangeDependencies() { |
| super.didChangeDependencies(); |
| _page = null; |
| } |
| |
| void _forceRebuildPage() { |
| setState(() { |
| _page = null; |
| }); |
| } |
| |
| @override |
| void dispose() { |
| focusScopeNode.dispose(); |
| super.dispose(); |
| } |
| |
| bool get _shouldIgnoreFocusRequest { |
| return widget.route.animation?.status == AnimationStatus.reverse || |
| (widget.route.navigator?.userGestureInProgress ?? false); |
| } |
| |
| bool get _shouldRequestFocus { |
| return widget.route.navigator!.widget.requestFocus; |
| } |
| |
| // This should be called to wrap any changes to route.isCurrent, route.canPop, |
| // and route.offstage. |
| void _routeSetState(VoidCallback fn) { |
| if (widget.route.isCurrent && !_shouldIgnoreFocusRequest && _shouldRequestFocus) { |
| widget.route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode); |
| } |
| setState(fn); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return AnimatedBuilder( |
| animation: widget.route.restorationScopeId, |
| builder: (BuildContext context, Widget? child) { |
| assert(child != null); |
| return RestorationScope( |
| restorationId: widget.route.restorationScopeId.value, |
| child: child!, |
| ); |
| }, |
| child: _ModalScopeStatus( |
| route: widget.route, |
| isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates |
| canPop: widget.route.canPop, // _routeSetState is called if this updates |
| impliesAppBarDismissal: widget.route.impliesAppBarDismissal, |
| child: Offstage( |
| offstage: widget.route.offstage, // _routeSetState is called if this updates |
| child: PageStorage( |
| bucket: widget.route._storageBucket, // immutable |
| child: Builder( |
| builder: (BuildContext context) { |
| return Actions( |
| actions: <Type, Action<Intent>>{ |
| DismissIntent: _DismissModalAction(context), |
| }, |
| child: PrimaryScrollController( |
| controller: primaryScrollController, |
| child: FocusScope( |
| node: focusScopeNode, // immutable |
| child: RepaintBoundary( |
| child: AnimatedBuilder( |
| animation: _listenable, // immutable |
| builder: (BuildContext context, Widget? child) { |
| return widget.route.buildTransitions( |
| context, |
| widget.route.animation!, |
| widget.route.secondaryAnimation!, |
| // This additional AnimatedBuilder is include because if the |
| // value of the userGestureInProgressNotifier changes, it's |
| // only necessary to rebuild the IgnorePointer widget and set |
| // the focus node's ability to focus. |
| AnimatedBuilder( |
| animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false), |
| builder: (BuildContext context, Widget? child) { |
| final bool ignoreEvents = _shouldIgnoreFocusRequest; |
| focusScopeNode.canRequestFocus = !ignoreEvents; |
| return IgnorePointer( |
| ignoring: ignoreEvents, |
| child: child, |
| ); |
| }, |
| child: child, |
| ), |
| ); |
| }, |
| child: _page ??= RepaintBoundary( |
| key: widget.route._subtreeKey, // immutable |
| child: Builder( |
| builder: (BuildContext context) { |
| return widget.route.buildPage( |
| context, |
| widget.route.animation!, |
| widget.route.secondaryAnimation!, |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| } |
| |
| /// A route that blocks interaction with previous routes. |
| /// |
| /// [ModalRoute]s cover the entire [Navigator]. They are not necessarily |
| /// [opaque], however; for example, a pop-up menu uses a [ModalRoute] but only |
| /// shows the menu in a small box overlapping the previous route. |
| /// |
| /// The `T` type argument is the return value of the route. If there is no |
| /// return value, consider using `void` as the return value. |
| /// |
| /// See also: |
| /// |
| /// * [Route], which further documents the meaning of the `T` generic type argument. |
| abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> { |
| /// Creates a route that blocks interaction with previous routes. |
| ModalRoute({ |
| super.settings, |
| this.filter, |
| }); |
| |
| /// The filter to add to the barrier. |
| /// |
| /// If given, this filter will be applied to the modal barrier using |
| /// [BackdropFilter]. This allows blur effects, for example. |
| final ui.ImageFilter? filter; |
| |
| // The API for general users of this class |
| |
| /// Returns the modal route most closely associated with the given context. |
| /// |
| /// Returns null if the given context is not associated with a modal route. |
| /// |
| /// {@tool snippet} |
| /// |
| /// Typical usage is as follows: |
| /// |
| /// ```dart |
| /// ModalRoute<int>? route = ModalRoute.of<int>(context); |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// The given [BuildContext] will be rebuilt if the state of the route changes |
| /// while it is visible (specifically, if [isCurrent] or [canPop] change value). |
| @optionalTypeArgs |
| static ModalRoute<T>? of<T extends Object?>(BuildContext context) { |
| final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>(); |
| return widget?.route as ModalRoute<T>?; |
| } |
| |
| /// Schedule a call to [buildTransitions]. |
| /// |
| /// Whenever you need to change internal state for a [ModalRoute] object, make |
| /// the change in a function that you pass to [setState], as in: |
| /// |
| /// ```dart |
| /// setState(() { _myState = newValue; }); |
| /// ``` |
| /// |
| /// If you just change the state directly without calling [setState], then the |
| /// route will not be scheduled for rebuilding, meaning that its rendering |
| /// will not be updated. |
| @protected |
| void setState(VoidCallback fn) { |
| if (_scopeKey.currentState != null) { |
| _scopeKey.currentState!._routeSetState(fn); |
| } else { |
| // The route isn't currently visible, so we don't have to call its setState |
| // method, but we do still need to call the fn callback, otherwise the state |
| // in the route won't be updated! |
| fn(); |
| } |
| } |
| |
| /// Returns a predicate that's true if the route has the specified name and if |
| /// popping the route will not yield the same route, i.e. if the route's |
| /// [willHandlePopInternally] property is false. |
| /// |
| /// This function is typically used with [Navigator.popUntil()]. |
| static RoutePredicate withName(String name) { |
| return (Route<dynamic> route) { |
| return !route.willHandlePopInternally |
| && route is ModalRoute |
| && route.settings.name == name; |
| }; |
| } |
| |
| // The API for subclasses to override - used by _ModalScope |
| |
| /// Override this method to build the primary content of this route. |
| /// |
| /// The arguments have the following meanings: |
| /// |
| /// * `context`: The context in which the route is being built. |
| /// * [animation]: The animation for this route's transition. When entering, |
| /// the animation runs forward from 0.0 to 1.0. When exiting, this animation |
| /// runs backwards from 1.0 to 0.0. |
| /// * [secondaryAnimation]: The animation for the route being pushed on top of |
| /// this route. This animation lets this route coordinate with the entrance |
| /// and exit transition of routes pushed on top of this route. |
| /// |
| /// This method is only called when the route is first built, and rarely |
| /// thereafter. In particular, it is not automatically called again when the |
| /// route's state changes unless it uses [ModalRoute.of]. For a builder that |
| /// is called every time the route's state changes, consider |
| /// [buildTransitions]. For widgets that change their behavior when the |
| /// route's state changes, consider [ModalRoute.of] to obtain a reference to |
| /// the route; this will cause the widget to be rebuilt each time the route |
| /// changes state. |
| /// |
| /// In general, [buildPage] should be used to build the page contents, and |
| /// [buildTransitions] for the widgets that change as the page is brought in |
| /// and out of view. Avoid using [buildTransitions] for content that never |
| /// changes; building such content once from [buildPage] is more efficient. |
| Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation); |
| |
| /// Override this method to wrap the [child] with one or more transition |
| /// widgets that define how the route arrives on and leaves the screen. |
| /// |
| /// By default, the child (which contains the widget returned by [buildPage]) |
| /// is not wrapped in any transition widgets. |
| /// |
| /// The [buildTransitions] method, in contrast to [buildPage], is called each |
| /// time the [Route]'s state changes while it is visible (e.g. if the value of |
| /// [canPop] changes on the active route). |
| /// |
| /// The [buildTransitions] method is typically used to define transitions |
| /// that animate the new topmost route's comings and goings. When the |
| /// [Navigator] pushes a route on the top of its stack, the new route's |
| /// primary [animation] runs from 0.0 to 1.0. When the Navigator pops the |
| /// topmost route, e.g. because the use pressed the back button, the |
| /// primary animation runs from 1.0 to 0.0. |
| /// |
| /// {@tool snippet} |
| /// The following example uses the primary animation to drive a |
| /// [SlideTransition] that translates the top of the new route vertically |
| /// from the bottom of the screen when it is pushed on the Navigator's |
| /// stack. When the route is popped the SlideTransition translates the |
| /// route from the top of the screen back to the bottom. |
| /// |
| /// We've used [PageRouteBuilder] to demonstrate the [buildTransitions] method |
| /// here. The body of an override of the [buildTransitions] method would be |
| /// defined in the same way. |
| /// |
| /// ```dart |
| /// PageRouteBuilder<void>( |
| /// pageBuilder: (BuildContext context, |
| /// Animation<double> animation, |
| /// Animation<double> secondaryAnimation, |
| /// ) { |
| /// return Scaffold( |
| /// appBar: AppBar(title: const Text('Hello')), |
| /// body: const Center( |
| /// child: Text('Hello World'), |
| /// ), |
| /// ); |
| /// }, |
| /// transitionsBuilder: ( |
| /// BuildContext context, |
| /// Animation<double> animation, |
| /// Animation<double> secondaryAnimation, |
| /// Widget child, |
| /// ) { |
| /// return SlideTransition( |
| /// position: Tween<Offset>( |
| /// begin: const Offset(0.0, 1.0), |
| /// end: Offset.zero, |
| /// ).animate(animation), |
| /// child: child, // child is the value returned by pageBuilder |
| /// ); |
| /// }, |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// When the [Navigator] pushes a route on the top of its stack, the |
| /// [secondaryAnimation] can be used to define how the route that was on |
| /// the top of the stack leaves the screen. Similarly when the topmost route |
| /// is popped, the secondaryAnimation can be used to define how the route |
| /// below it reappears on the screen. When the Navigator pushes a new route |
| /// on the top of its stack, the old topmost route's secondaryAnimation |
| /// runs from 0.0 to 1.0. When the Navigator pops the topmost route, the |
| /// secondaryAnimation for the route below it runs from 1.0 to 0.0. |
| /// |
| /// {@tool snippet} |
| /// The example below adds a transition that's driven by the |
| /// [secondaryAnimation]. When this route disappears because a new route has |
| /// been pushed on top of it, it translates in the opposite direction of |
| /// the new route. Likewise when the route is exposed because the topmost |
| /// route has been popped off. |
| /// |
| /// ```dart |
| /// PageRouteBuilder<void>( |
| /// pageBuilder: (BuildContext context, |
| /// Animation<double> animation, |
| /// Animation<double> secondaryAnimation, |
| /// ) { |
| /// return Scaffold( |
| /// appBar: AppBar(title: const Text('Hello')), |
| /// body: const Center( |
| /// child: Text('Hello World'), |
| /// ), |
| /// ); |
| /// }, |
| /// transitionsBuilder: ( |
| /// BuildContext context, |
| /// Animation<double> animation, |
| /// Animation<double> secondaryAnimation, |
| /// Widget child, |
| /// ) { |
| /// return SlideTransition( |
| /// position: Tween<Offset>( |
| /// begin: const Offset(0.0, 1.0), |
| /// end: Offset.zero, |
| /// ).animate(animation), |
| /// child: SlideTransition( |
| /// position: Tween<Offset>( |
| /// begin: Offset.zero, |
| /// end: const Offset(0.0, 1.0), |
| /// ).animate(secondaryAnimation), |
| /// child: child, |
| /// ), |
| /// ); |
| /// }, |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// In practice the `secondaryAnimation` is used pretty rarely. |
| /// |
| /// The arguments to this method are as follows: |
| /// |
| /// * `context`: The context in which the route is being built. |
| /// * [animation]: When the [Navigator] pushes a route on the top of its stack, |
| /// the new route's primary [animation] runs from 0.0 to 1.0. When the [Navigator] |
| /// pops the topmost route this animation runs from 1.0 to 0.0. |
| /// * [secondaryAnimation]: When the Navigator pushes a new route |
| /// on the top of its stack, the old topmost route's [secondaryAnimation] |
| /// runs from 0.0 to 1.0. When the [Navigator] pops the topmost route, the |
| /// [secondaryAnimation] for the route below it runs from 1.0 to 0.0. |
| /// * `child`, the page contents, as returned by [buildPage]. |
| /// |
| /// See also: |
| /// |
| /// * [buildPage], which is used to describe the actual contents of the page, |
| /// and whose result is passed to the `child` argument of this method. |
| Widget buildTransitions( |
| BuildContext context, |
| Animation<double> animation, |
| Animation<double> secondaryAnimation, |
| Widget child, |
| ) { |
| return child; |
| } |
| |
| @override |
| void install() { |
| super.install(); |
| _animationProxy = ProxyAnimation(super.animation); |
| _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation); |
| } |
| |
| @override |
| TickerFuture didPush() { |
| if (_scopeKey.currentState != null && navigator!.widget.requestFocus) { |
| navigator!.focusNode.enclosingScope?.setFirstFocus(_scopeKey.currentState!.focusScopeNode); |
| } |
| return super.didPush(); |
| } |
| |
| @override |
| void didAdd() { |
| if (_scopeKey.currentState != null && navigator!.widget.requestFocus) { |
| navigator!.focusNode.enclosingScope?.setFirstFocus(_scopeKey.currentState!.focusScopeNode); |
| } |
| super.didAdd(); |
| } |
| |
| // The API for subclasses to override - used by this class |
| |
| /// {@template flutter.widgets.ModalRoute.barrierDismissible} |
| /// Whether you can dismiss this route by tapping the modal barrier. |
| /// |
| /// The modal barrier is the scrim that is rendered behind each route, which |
| /// generally prevents the user from interacting with the route below the |
| /// current route, and normally partially obscures such routes. |
| /// |
| /// For example, when a dialog is on the screen, the page below the dialog is |
| /// usually darkened by the modal barrier. |
| /// |
| /// If [barrierDismissible] is true, then tapping this barrier, pressing |
| /// the escape key on the keyboard, or calling route popping functions |
| /// such as [Navigator.pop] will cause the current route to be popped |
| /// with null as the value. |
| /// |
| /// If [barrierDismissible] is false, then tapping the barrier has no effect. |
| /// |
| /// If this getter would ever start returning a different value, |
| /// either [changedInternalState] or [changedExternalState] should |
| /// be invoked so that the change can take effect. |
| /// |
| /// It is safe to use `navigator.context` to look up inherited |
| /// widgets here, because the [Navigator] calls |
| /// [changedExternalState] whenever its dependencies change, and |
| /// [changedExternalState] causes the modal barrier to rebuild. |
| /// |
| /// See also: |
| /// |
| /// * [Navigator.pop], which is used to dismiss the route. |
| /// * [barrierColor], which controls the color of the scrim for this route. |
| /// * [ModalBarrier], the widget that implements this feature. |
| /// {@endtemplate} |
| bool get barrierDismissible; |
| |
| /// Whether the semantics of the modal barrier are included in the |
| /// semantics tree. |
| /// |
| /// The modal barrier is the scrim that is rendered behind each route, which |
| /// generally prevents the user from interacting with the route below the |
| /// current route, and normally partially obscures such routes. |
| /// |
| /// If [semanticsDismissible] is true, then modal barrier semantics are |
| /// included in the semantics tree. |
| /// |
| /// If [semanticsDismissible] is false, then modal barrier semantics are |
| /// excluded from the semantics tree and tapping on the modal barrier |
| /// has no effect. |
| /// |
| /// If this getter would ever start returning a different value, |
| /// either [changedInternalState] or [changedExternalState] should |
| /// be invoked so that the change can take effect. |
| /// |
| /// It is safe to use `navigator.context` to look up inherited |
| /// widgets here, because the [Navigator] calls |
| /// [changedExternalState] whenever its dependencies change, and |
| /// [changedExternalState] causes the modal barrier to rebuild. |
| bool get semanticsDismissible => true; |
| |
| /// {@template flutter.widgets.ModalRoute.barrierColor} |
| /// The color to use for the modal barrier. If this is null, the barrier will |
| /// be transparent. |
| /// |
| /// The modal barrier is the scrim that is rendered behind each route, which |
| /// generally prevents the user from interacting with the route below the |
| /// current route, and normally partially obscures such routes. |
| /// |
| /// For example, when a dialog is on the screen, the page below the dialog is |
| /// usually darkened by the modal barrier. |
| /// |
| /// The color is ignored, and the barrier made invisible, when |
| /// [ModalRoute.offstage] is true. |
| /// |
| /// While the route is animating into position, the color is animated from |
| /// transparent to the specified color. |
| /// {@endtemplate} |
| /// |
| /// If this getter would ever start returning a different color, one |
| /// of the [changedInternalState] or [changedExternalState] methods |
| /// should be invoked so that the change can take effect. |
| /// |
| /// It is safe to use `navigator.context` to look up inherited |
| /// widgets here, because the [Navigator] calls |
| /// [changedExternalState] whenever its dependencies change, and |
| /// [changedExternalState] causes the modal barrier to rebuild. |
| /// |
| /// {@tool snippet} |
| /// |
| /// For example, to make the barrier color use the theme's |
| /// background color, one could say: |
| /// |
| /// ```dart |
| /// Color get barrierColor => Theme.of(navigator.context).colorScheme.background; |
| /// ``` |
| /// |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [barrierDismissible], which controls the behavior of the barrier when |
| /// tapped. |
| /// * [ModalBarrier], the widget that implements this feature. |
| Color? get barrierColor; |
| |
| /// {@template flutter.widgets.ModalRoute.barrierLabel} |
| /// The semantic label used for a dismissible barrier. |
| /// |
| /// If the barrier is dismissible, this label will be read out if |
| /// accessibility tools (like VoiceOver on iOS) focus on the barrier. |
| /// |
| /// The modal barrier is the scrim that is rendered behind each route, which |
| /// generally prevents the user from interacting with the route below the |
| /// current route, and normally partially obscures such routes. |
| /// |
| /// For example, when a dialog is on the screen, the page below the dialog is |
| /// usually darkened by the modal barrier. |
| /// {@endtemplate} |
| /// |
| /// If this getter would ever start returning a different label, |
| /// either [changedInternalState] or [changedExternalState] should |
| /// be invoked so that the change can take effect. |
| /// |
| /// It is safe to use `navigator.context` to look up inherited |
| /// widgets here, because the [Navigator] calls |
| /// [changedExternalState] whenever its dependencies change, and |
| /// [changedExternalState] causes the modal barrier to rebuild. |
| /// |
| /// See also: |
| /// |
| /// * [barrierDismissible], which controls the behavior of the barrier when |
| /// tapped. |
| /// * [ModalBarrier], the widget that implements this feature. |
| String? get barrierLabel; |
| |
| /// The curve that is used for animating the modal barrier in and out. |
| /// |
| /// The modal barrier is the scrim that is rendered behind each route, which |
| /// generally prevents the user from interacting with the route below the |
| /// current route, and normally partially obscures such routes. |
| /// |
| /// For example, when a dialog is on the screen, the page below the dialog is |
| /// usually darkened by the modal barrier. |
| /// |
| /// While the route is animating into position, the color is animated from |
| /// transparent to the specified [barrierColor]. |
| /// |
| /// If this getter would ever start returning a different curve, |
| /// either [changedInternalState] or [changedExternalState] should |
| /// be invoked so that the change can take effect. |
| /// |
| /// It is safe to use `navigator.context` to look up inherited |
| /// widgets here, because the [Navigator] calls |
| /// [changedExternalState] whenever its dependencies change, and |
| /// [changedExternalState] causes the modal barrier to rebuild. |
| /// |
| /// It defaults to [Curves.ease]. |
| /// |
| /// See also: |
| /// |
| /// * [barrierColor], which determines the color that the modal transitions |
| /// to. |
| /// * [Curves] for a collection of common curves. |
| /// * [AnimatedModalBarrier], the widget that implements this feature. |
| Curve get barrierCurve => Curves.ease; |
| |
| /// {@template flutter.widgets.ModalRoute.maintainState} |
| /// Whether the route should remain in memory when it is inactive. |
| /// |
| /// If this is true, then the route is maintained, so that any futures it is |
| /// holding from the next route will properly resolve when the next route |
| /// pops. If this is not necessary, this can be set to false to allow the |
| /// framework to entirely discard the route's widget hierarchy when it is not |
| /// visible. |
| /// {@endtemplate} |
| /// |
| /// If this getter would ever start returning a different value, the |
| /// [changedInternalState] should be invoked so that the change can take |
| /// effect. |
| bool get maintainState; |
| |
| |
| // The API for _ModalScope and HeroController |
| |
| /// Whether this route is currently offstage. |
| /// |
| /// On the first frame of a route's entrance transition, the route is built |
| /// [Offstage] using an animation progress of 1.0. The route is invisible and |
| /// non-interactive, but each widget has its final size and position. This |
| /// mechanism lets the [HeroController] determine the final local of any hero |
| /// widgets being animated as part of the transition. |
| /// |
| /// The modal barrier, if any, is not rendered if [offstage] is true (see |
| /// [barrierColor]). |
| /// |
| /// Whenever this changes value, [changedInternalState] is called. |
| bool get offstage => _offstage; |
| bool _offstage = false; |
| set offstage(bool value) { |
| if (_offstage == value) { |
| return; |
| } |
| setState(() { |
| _offstage = value; |
| }); |
| _animationProxy!.parent = _offstage ? kAlwaysCompleteAnimation : super.animation; |
| _secondaryAnimationProxy!.parent = _offstage ? kAlwaysDismissedAnimation : super.secondaryAnimation; |
| changedInternalState(); |
| } |
| |
| /// The build context for the subtree containing the primary content of this route. |
| BuildContext? get subtreeContext => _subtreeKey.currentContext; |
| |
| @override |
| Animation<double>? get animation => _animationProxy; |
| ProxyAnimation? _animationProxy; |
| |
| @override |
| Animation<double>? get secondaryAnimation => _secondaryAnimationProxy; |
| ProxyAnimation? _secondaryAnimationProxy; |
| |
| final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[]; |
| |
| /// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with |
| /// [addScopedWillPopCallback] returns either false or null. If they all |
| /// return true, the base [Route.willPop]'s result will be returned. The |
| /// callbacks will be called in the order they were added, and will only be |
| /// called if all previous callbacks returned true. |
| /// |
| /// Typically this method is not overridden because applications usually |
| /// don't create modal routes directly, they use higher level primitives |
| /// like [showDialog]. The scoped [WillPopCallback] list makes it possible |
| /// for ModalRoute descendants to collectively define the value of [willPop]. |
| /// |
| /// See also: |
| /// |
| /// * [Form], which provides an `onWillPop` callback that uses this mechanism. |
| /// * [addScopedWillPopCallback], which adds a callback to the list this |
| /// method checks. |
| /// * [removeScopedWillPopCallback], which removes a callback from the list |
| /// this method checks. |
| @override |
| Future<RoutePopDisposition> willPop() async { |
| final _ModalScopeState<T>? scope = _scopeKey.currentState; |
| assert(scope != null); |
| for (final WillPopCallback callback in List<WillPopCallback>.of(_willPopCallbacks)) { |
| if (await callback() != true) { |
| return RoutePopDisposition.doNotPop; |
| } |
| } |
| return super.willPop(); |
| } |
| |
| /// Enables this route to veto attempts by the user to dismiss it. |
| /// |
| /// {@tool snippet} |
| /// This callback is typically added using a [WillPopScope] widget. That |
| /// widget finds the enclosing [ModalRoute] and uses this function to register |
| /// this callback: |
| /// |
| /// ```dart |
| /// Widget build(BuildContext context) { |
| /// return WillPopScope( |
| /// onWillPop: () async { |
| /// // ask the user if they are sure |
| /// return true; |
| /// }, |
| /// child: Container(), |
| /// ); |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// This callback runs asynchronously and it's possible that it will be called |
| /// after its route has been disposed. The callback should check [State.mounted] |
| /// before doing anything. |
| /// |
| /// A typical application of this callback would be to warn the user about |
| /// unsaved [Form] data if the user attempts to back out of the form. In that |
| /// case, use the [Form.onWillPop] property to register the callback. |
| /// |
| /// {@tool snippet} |
| /// To register a callback manually, look up the enclosing [ModalRoute] in a |
| /// [State.didChangeDependencies] callback: |
| /// |
| /// ```dart |
| /// abstract class _MyWidgetState extends State<MyWidget> { |
| /// ModalRoute<dynamic>? _route; |
| /// |
| /// // ... |
| /// |
| /// @override |
| /// void didChangeDependencies() { |
| /// super.didChangeDependencies(); |
| /// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure); |
| /// _route = ModalRoute.of(context); |
| /// _route?.addScopedWillPopCallback(askTheUserIfTheyAreSure); |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// {@tool snippet} |
| /// If you register a callback manually, be sure to remove the callback with |
| /// [removeScopedWillPopCallback] by the time the widget has been disposed. A |
| /// stateful widget can do this in its dispose method (continuing the previous |
| /// example): |
| /// |
| /// ```dart |
| /// abstract class _MyWidgetState2 extends State<MyWidget> { |
| /// ModalRoute<dynamic>? _route; |
| /// |
| /// // ... |
| /// |
| /// @override |
| /// void dispose() { |
| /// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure); |
| /// _route = null; |
| /// super.dispose(); |
| /// } |
| /// } |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [WillPopScope], which manages the registration and unregistration |
| /// process automatically. |
| /// * [Form], which provides an `onWillPop` callback that uses this mechanism. |
| /// * [willPop], which runs the callbacks added with this method. |
| /// * [removeScopedWillPopCallback], which removes a callback from the list |
| /// that [willPop] checks. |
| void addScopedWillPopCallback(WillPopCallback callback) { |
| assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.'); |
| _willPopCallbacks.add(callback); |
| } |
| |
| /// Remove one of the callbacks run by [willPop]. |
| /// |
| /// See also: |
| /// |
| /// * [Form], which provides an `onWillPop` callback that uses this mechanism. |
| /// * [addScopedWillPopCallback], which adds callback to the list |
| /// checked by [willPop]. |
| void removeScopedWillPopCallback(WillPopCallback callback) { |
| assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.'); |
| _willPopCallbacks.remove(callback); |
| } |
| |
| /// True if one or more [WillPopCallback] callbacks exist. |
| /// |
| /// This method is used to disable the horizontal swipe pop gesture supported |
| /// by [MaterialPageRoute] for [TargetPlatform.iOS] and |
| /// [TargetPlatform.macOS]. If a pop might be vetoed, then the back gesture is |
| /// disabled. |
| /// |
| /// The [buildTransitions] method will not be called again if this changes, |
| /// since it can change during the build as descendants of the route add or |
| /// remove callbacks. |
| /// |
| /// See also: |
| /// |
| /// * [addScopedWillPopCallback], which adds a callback. |
| /// * [removeScopedWillPopCallback], which removes a callback. |
| /// * [willHandlePopInternally], which reports on another reason why |
| /// a pop might be vetoed. |
| @protected |
| bool get hasScopedWillPopCallback { |
| return _willPopCallbacks.isNotEmpty; |
| } |
| |
| @override |
| void didChangePrevious(Route<dynamic>? previousRoute) { |
| super.didChangePrevious(previousRoute); |
| changedInternalState(); |
| } |
| |
| @override |
| void changedInternalState() { |
| super.changedInternalState(); |
| setState(() { /* internal state already changed */ }); |
| _modalBarrier.markNeedsBuild(); |
| _modalScope.maintainState = maintainState; |
| } |
| |
| @override |
| void changedExternalState() { |
| super.changedExternalState(); |
| _modalBarrier.markNeedsBuild(); |
| if (_scopeKey.currentState != null) { |
| _scopeKey.currentState!._forceRebuildPage(); |
| } |
| } |
| |
| /// Whether this route can be popped. |
| /// |
| /// A route can be popped if there is at least one active route below it, or |
| /// if [willHandlePopInternally] returns true. |
| /// |
| /// When this changes, if the route is visible, the route will |
| /// rebuild, and any widgets that used [ModalRoute.of] will be |
| /// notified. |
| bool get canPop => hasActiveRouteBelow || willHandlePopInternally; |
| |
| /// Whether an [AppBar] in the route should automatically add a back button or |
| /// close button. |
| /// |
| /// This getter returns true if there is at least one active route below it, |
| /// or there is at least one [LocalHistoryEntry] with [impliesAppBarDismissal] |
| /// set to true |
| bool get impliesAppBarDismissal => hasActiveRouteBelow || _entriesImpliesAppBarDismissal > 0; |
| |
| // Internals |
| |
| final GlobalKey<_ModalScopeState<T>> _scopeKey = GlobalKey<_ModalScopeState<T>>(); |
| final GlobalKey _subtreeKey = GlobalKey(); |
| final PageStorageBucket _storageBucket = PageStorageBucket(); |
| |
| // one of the builders |
| late OverlayEntry _modalBarrier; |
| Widget _buildModalBarrier(BuildContext context) { |
| Widget barrier; |
| if (barrierColor != null && barrierColor!.alpha != 0 && !offstage) { // changedInternalState is called if barrierColor or offstage updates |
| assert(barrierColor != barrierColor!.withOpacity(0.0)); |
| final Animation<Color?> color = animation!.drive( |
| ColorTween( |
| begin: barrierColor!.withOpacity(0.0), |
| end: barrierColor, // changedInternalState is called if barrierColor updates |
| ).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates |
| ); |
| barrier = AnimatedModalBarrier( |
| color: color, |
| dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates |
| semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates |
| barrierSemanticsDismissible: semanticsDismissible, |
| ); |
| } else { |
| barrier = ModalBarrier( |
| dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates |
| semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates |
| barrierSemanticsDismissible: semanticsDismissible, |
| ); |
| } |
| if (filter != null) { |
| barrier = BackdropFilter( |
| filter: filter!, |
| child: barrier, |
| ); |
| } |
| barrier = IgnorePointer( |
| ignoring: animation!.status == AnimationStatus.reverse || // changedInternalState is called when animation.status updates |
| animation!.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture |
| child: barrier, |
| ); |
| if (semanticsDismissible && barrierDismissible) { |
| // To be sorted after the _modalScope. |
| barrier = Semantics( |
| sortKey: const OrdinalSortKey(1.0), |
| child: barrier, |
| ); |
| } |
| return barrier; |
| } |
| |
| // We cache the part of the modal scope that doesn't change from frame to |
| // frame so that we minimize the amount of building that happens. |
| Widget? _modalScopeCache; |
| |
| // one of the builders |
| Widget _buildModalScope(BuildContext context) { |
| // To be sorted before the _modalBarrier. |
| return _modalScopeCache ??= Semantics( |
| sortKey: const OrdinalSortKey(0.0), |
| child: _ModalScope<T>( |
| key: _scopeKey, |
| route: this, |
| // _ModalScope calls buildTransitions() and buildChild(), defined above |
| ), |
| ); |
| } |
| |
| late OverlayEntry _modalScope; |
| |
| @override |
| Iterable<OverlayEntry> createOverlayEntries() { |
| return <OverlayEntry>[ |
| _modalBarrier = OverlayEntry(builder: _buildModalBarrier), |
| _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState), |
| ]; |
| } |
| |
| @override |
| String toString() => '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)'; |
| } |
| |
| /// A modal route that overlays a widget over the current route. |
| /// |
| /// {@macro flutter.widgets.ModalRoute.barrierDismissible} |
| /// |
| /// {@tool dartpad} |
| /// This example shows how to create a dialog box that is dismissible. |
| /// |
| /// ** See code in examples/api/lib/widgets/routes/popup_route.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [ModalRoute], which is the base class for this class. |
| /// * [Navigator.pop], which is used to dismiss the route. |
| abstract class PopupRoute<T> extends ModalRoute<T> { |
| /// Initializes the [PopupRoute]. |
| PopupRoute({ |
| super.settings, |
| super.filter, |
| }); |
| |
| @override |
| bool get opaque => false; |
| |
| @override |
| bool get maintainState => true; |
| |
| @override |
| bool get allowSnapshotting => false; |
| } |
| |
| /// A [Navigator] observer that notifies [RouteAware]s of changes to the |
| /// state of their [Route]. |
| /// |
| /// [RouteObserver] informs subscribers whenever a route of type `R` is pushed |
| /// on top of their own route of type `R` or popped from it. This is for example |
| /// useful to keep track of page transitions, e.g. a `RouteObserver<PageRoute>` |
| /// will inform subscribed [RouteAware]s whenever the user navigates away from |
| /// the current page route to another page route. |
| /// |
| /// To be informed about route changes of any type, consider instantiating a |
| /// `RouteObserver<Route>`. |
| /// |
| /// ## Type arguments |
| /// |
| /// When using more aggressive |
| /// [lints](http://dart-lang.github.io/linter/lints/), in particular lints such |
| /// as `always_specify_types`, the Dart analyzer will require that certain types |
| /// be given with their type arguments. Since the [Route] class and its |
| /// subclasses have a type argument, this includes the arguments passed to this |
| /// class. Consider using `dynamic` to specify the entire class of routes rather |
| /// than only specific subtypes. For example, to watch for all [ModalRoute] |
| /// variants, the `RouteObserver<ModalRoute<dynamic>>` type may be used. |
| /// |
| /// {@tool snippet} |
| /// |
| /// To make a [StatefulWidget] aware of its current [Route] state, implement |
| /// [RouteAware] in its [State] and subscribe it to a [RouteObserver]: |
| /// |
| /// ```dart |
| /// // Register the RouteObserver as a navigation observer. |
| /// final RouteObserver<ModalRoute<void>> routeObserver = RouteObserver<ModalRoute<void>>(); |
| /// |
| /// void main() { |
| /// runApp(MaterialApp( |
| /// home: Container(), |
| /// navigatorObservers: <RouteObserver<ModalRoute<void>>>[ routeObserver ], |
| /// )); |
| /// } |
| /// |
| /// class RouteAwareWidget extends StatefulWidget { |
| /// const RouteAwareWidget({super.key}); |
| /// |
| /// @override |
| /// State<RouteAwareWidget> createState() => RouteAwareWidgetState(); |
| /// } |
| /// |
| /// // Implement RouteAware in a widget's state and subscribe it to the RouteObserver. |
| /// class RouteAwareWidgetState extends State<RouteAwareWidget> with RouteAware { |
| /// |
| /// @override |
| /// void didChangeDependencies() { |
| /// super.didChangeDependencies(); |
| /// routeObserver.subscribe(this, ModalRoute.of(context)!); |
| /// } |
| /// |
| /// @override |
| /// void dispose() { |
| /// routeObserver.unsubscribe(this); |
| /// super.dispose(); |
| /// } |
| /// |
| /// @override |
| /// void didPush() { |
| /// // Route was pushed onto navigator and is now topmost route. |
| /// } |
| /// |
| /// @override |
| /// void didPopNext() { |
| /// // Covering route was popped off the navigator. |
| /// } |
| /// |
| /// @override |
| /// Widget build(BuildContext context) => Container(); |
| /// |
| /// } |
| /// ``` |
| /// {@end-tool} |
| class RouteObserver<R extends Route<dynamic>> extends NavigatorObserver { |
| final Map<R, Set<RouteAware>> _listeners = <R, Set<RouteAware>>{}; |
| |
| /// Whether this observer is managing changes for the specified route. |
| /// |
| /// If asserts are disabled, this method will throw an exception. |
| @visibleForTesting |
| bool debugObservingRoute(R route) { |
| late bool contained; |
| assert(() { |
| contained = _listeners.containsKey(route); |
| return true; |
| }()); |
| return contained; |
| } |
| |
| /// Subscribe [routeAware] to be informed about changes to [route]. |
| /// |
| /// Going forward, [routeAware] will be informed about qualifying changes |
| /// to [route], e.g. when [route] is covered by another route or when [route] |
| /// is popped off the [Navigator] stack. |
| void subscribe(RouteAware routeAware, R route) { |
| assert(routeAware != null); |
| assert(route != null); |
| final Set<RouteAware> subscribers = _listeners.putIfAbsent(route, () => <RouteAware>{}); |
| if (subscribers.add(routeAware)) { |
| routeAware.didPush(); |
| } |
| } |
| |
| /// Unsubscribe [routeAware]. |
| /// |
| /// [routeAware] is no longer informed about changes to its route. If the given argument was |
| /// subscribed to multiple types, this will unregister it (once) from each type. |
| void unsubscribe(RouteAware routeAware) { |
| assert(routeAware != null); |
| final List<R> routes = _listeners.keys.toList(); |
| for (final R route in routes) { |
| final Set<RouteAware>? subscribers = _listeners[route]; |
| if (subscribers != null) { |
| subscribers.remove(routeAware); |
| if (subscribers.isEmpty) { |
| _listeners.remove(route); |
| } |
| } |
| } |
| } |
| |
| @override |
| void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) { |
| if (route is R && previousRoute is R) { |
| final List<RouteAware>? previousSubscribers = _listeners[previousRoute]?.toList(); |
| |
| if (previousSubscribers != null) { |
| for (final RouteAware routeAware in previousSubscribers) { |
| routeAware.didPopNext(); |
| } |
| } |
| |
| final List<RouteAware>? subscribers = _listeners[route]?.toList(); |
| |
| if (subscribers != null) { |
| for (final RouteAware routeAware in subscribers) { |
| routeAware.didPop(); |
| } |
| } |
| } |
| } |
| |
| @override |
| void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { |
| if (route is R && previousRoute is R) { |
| final Set<RouteAware>? previousSubscribers = _listeners[previousRoute]; |
| |
| if (previousSubscribers != null) { |
| for (final RouteAware routeAware in previousSubscribers) { |
| routeAware.didPushNext(); |
| } |
| } |
| } |
| } |
| } |
| |
| /// An interface for objects that are aware of their current [Route]. |
| /// |
| /// This is used with [RouteObserver] to make a widget aware of changes to the |
| /// [Navigator]'s session history. |
| abstract class RouteAware { |
| /// Called when the top route has been popped off, and the current route |
| /// shows up. |
| void didPopNext() { } |
| |
| /// Called when the current route has been pushed. |
| void didPush() { } |
| |
| /// Called when the current route has been popped off. |
| void didPop() { } |
| |
| /// Called when a new route has been pushed, and the current route is no |
| /// longer visible. |
| void didPushNext() { } |
| } |
| |
| /// A general dialog route which allows for customization of the dialog popup. |
| /// |
| /// It is used internally by [showGeneralDialog] or can be directly pushed |
| /// onto the [Navigator] stack to enable state restoration. See |
| /// [showGeneralDialog] for a state restoration app example. |
| /// |
| /// This function takes a `pageBuilder`, which typically builds a dialog. |
| /// Content below the dialog is dimmed with a [ModalBarrier]. The widget |
| /// returned by the `builder` does not share a context with the location that |
| /// `showDialog` is originally called from. Use a [StatefulBuilder] or a |
| /// custom [StatefulWidget] if the dialog needs to update dynamically. |
| /// |
| /// The `barrierDismissible` argument is used to indicate whether tapping on the |
| /// barrier will dismiss the dialog. It is `true` by default and cannot be `null`. |
| /// |
| /// The `barrierColor` argument is used to specify the color of the modal |
| /// barrier that darkens everything below the dialog. If `null`, the default |
| /// color `Colors.black54` is used. |
| /// |
| /// The `settings` argument define the settings for this route. See |
| /// [RouteSettings] for details. |
| /// |
| /// {@template flutter.widgets.RawDialogRoute} |
| /// A [DisplayFeature] can split the screen into sub-screens. The closest one to |
| /// [anchorPoint] is used to render the content. |
| /// |
| /// If no [anchorPoint] is provided, then [Directionality] is used: |
| /// |
| /// * for [TextDirection.ltr], [anchorPoint] is `Offset.zero`, which will |
| /// cause the content to appear in the top-left sub-screen. |
| /// * for [TextDirection.rtl], [anchorPoint] is `Offset(double.maxFinite, 0)`, |
| /// which will cause the content to appear in the top-right sub-screen. |
| /// |
| /// If no [anchorPoint] is provided, and there is no [Directionality] ancestor |
| /// widget in the tree, then the widget asserts during build in debug mode. |
| /// {@endtemplate} |
| /// |
| /// See also: |
| /// |
| /// * [DisplayFeatureSubScreen], which documents the specifics of how |
| /// [DisplayFeature]s can split the screen into sub-screens. |
| /// * [showGeneralDialog], which is a way to display a RawDialogRoute. |
| /// * [showDialog], which is a way to display a DialogRoute. |
| /// * [showCupertinoDialog], which displays an iOS-style dialog. |
| class RawDialogRoute<T> extends PopupRoute<T> { |
| /// A general dialog route which allows for customization of the dialog popup. |
| RawDialogRoute({ |
| required RoutePageBuilder pageBuilder, |
| bool barrierDismissible = true, |
| Color? barrierColor = const Color(0x80000000), |
| String? barrierLabel, |
| Duration transitionDuration = const Duration(milliseconds: 200), |
| RouteTransitionsBuilder? transitionBuilder, |
| super.settings, |
| this.anchorPoint, |
| }) : assert(barrierDismissible != null), |
| _pageBuilder = pageBuilder, |
| _barrierDismissible = barrierDismissible, |
| _barrierLabel = barrierLabel, |
| _barrierColor = barrierColor, |
| _transitionDuration = transitionDuration, |
| _transitionBuilder = transitionBuilder; |
| |
| final RoutePageBuilder _pageBuilder; |
| |
| @override |
| bool get barrierDismissible => _barrierDismissible; |
| final bool _barrierDismissible; |
| |
| @override |
| String? get barrierLabel => _barrierLabel; |
| final String? _barrierLabel; |
| |
| @override |
| Color? get barrierColor => _barrierColor; |
| final Color? _barrierColor; |
| |
| @override |
| Duration get transitionDuration => _transitionDuration; |
| final Duration _transitionDuration; |
| |
| final RouteTransitionsBuilder? _transitionBuilder; |
| |
| /// {@macro flutter.widgets.DisplayFeatureSubScreen.anchorPoint} |
| final Offset? anchorPoint; |
| |
| @override |
| Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { |
| return Semantics( |
| scopesRoute: true, |
| explicitChildNodes: true, |
| child: DisplayFeatureSubScreen( |
| anchorPoint: anchorPoint, |
| child: _pageBuilder(context, animation, secondaryAnimation), |
| ), |
| ); |
| } |
| |
| @override |
| Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { |
| if (_transitionBuilder == null) { |
| // Some default transition. |
| return FadeTransition( |
| opacity: CurvedAnimation( |
| parent: animation, |
| curve: Curves.linear, |
| ), |
| child: child, |
| ); |
| } |
| return _transitionBuilder!(context, animation, secondaryAnimation, child); |
| } |
| } |
| |
| /// Displays a dialog above the current contents of the app. |
| /// |
| /// This function allows for customization of aspects of the dialog popup. |
| /// |
| /// This function takes a `pageBuilder` which is used to build the primary |
| /// content of the route (typically a dialog widget). Content below the dialog |
| /// is dimmed with a [ModalBarrier]. The widget returned by the `pageBuilder` |
| /// does not share a context with the location that [showGeneralDialog] is |
| /// originally called from. Use a [StatefulBuilder] or a custom |
| /// [StatefulWidget] if the dialog needs to update dynamically. The |
| /// `pageBuilder` argument can not be null. |
| /// |
| /// The `context` argument is used to look up the [Navigator] for the |
| /// dialog. It is only used when the method is called. Its corresponding widget |
| /// can be safely removed from the tree before the dialog is closed. |
| /// |
| /// The `useRootNavigator` argument is used to determine whether to push the |
| /// dialog to the [Navigator] furthest from or nearest to the given `context`. |
| /// By default, `useRootNavigator` is `true` and the dialog route created by |
| /// this method is pushed to the root navigator. |
| /// |
| /// If the application has multiple [Navigator] objects, it may be necessary to |
| /// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the |
| /// dialog rather than just `Navigator.pop(context, result)`. |
| /// |
| /// The `barrierDismissible` argument is used to determine whether this route |
| /// can be dismissed by tapping the modal barrier. This argument defaults |
| /// to false. If `barrierDismissible` is true, a non-null `barrierLabel` must be |
| /// provided. |
| /// |
| /// The `barrierLabel` argument is the semantic label used for a dismissible |
| /// barrier. This argument defaults to `null`. |
| /// |
| /// The `barrierColor` argument is the color used for the modal barrier. This |
| /// argument defaults to `Color(0x80000000)`. |
| /// |
| /// The `transitionDuration` argument is used to determine how long it takes |
| /// for the route to arrive on or leave off the screen. This argument defaults |
| /// to 200 milliseconds. |
| /// |
| /// The `transitionBuilder` argument is used to define how the route arrives on |
| /// and leaves off the screen. By default, the transition is a linear fade of |
| /// the page's contents. |
| /// |
| /// The `routeSettings` will be used in the construction of the dialog's route. |
| /// See [RouteSettings] for more details. |
| /// |
| /// {@macro flutter.widgets.RawDialogRoute} |
| /// |
| /// Returns a [Future] that resolves to the value (if any) that was passed to |
| /// [Navigator.pop] when the dialog was closed. |
| /// |
| /// ### State Restoration in Dialogs |
| /// |
| /// Using this method will not enable state restoration for the dialog. In order |
| /// to enable state restoration for a dialog, use [Navigator.restorablePush] |
| /// or [Navigator.restorablePushNamed] with [RawDialogRoute]. |
| /// |
| /// For more information about state restoration, see [RestorationManager]. |
| /// |
| /// {@tool sample} |
| /// This sample demonstrates how to create a restorable dialog. This is |
| /// accomplished by enabling state restoration by specifying |
| /// [WidgetsApp.restorationScopeId] and using [Navigator.restorablePush] to |
| /// push [RawDialogRoute] when the button is tapped. |
| /// |
| /// {@macro flutter.widgets.RestorationManager} |
| /// |
| /// ** See code in examples/api/lib/widgets/routes/show_general_dialog.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [DisplayFeatureSubScreen], which documents the specifics of how |
| /// [DisplayFeature]s can split the screen into sub-screens. |
| /// * [showDialog], which displays a Material-style dialog. |
| /// * [showCupertinoDialog], which displays an iOS-style dialog. |
| Future<T?> showGeneralDialog<T extends Object?>({ |
| required BuildContext context, |
| required RoutePageBuilder pageBuilder, |
| bool barrierDismissible = false, |
| String? barrierLabel, |
| Color barrierColor = const Color(0x80000000), |
| Duration transitionDuration = const Duration(milliseconds: 200), |
| RouteTransitionsBuilder? transitionBuilder, |
| bool useRootNavigator = true, |
| RouteSettings? routeSettings, |
| Offset? anchorPoint, |
| }) { |
| assert(pageBuilder != null); |
| assert(useRootNavigator != null); |
| assert(!barrierDismissible || barrierLabel != null); |
| return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(RawDialogRoute<T>( |
| pageBuilder: pageBuilder, |
| barrierDismissible: barrierDismissible, |
| barrierLabel: barrierLabel, |
| barrierColor: barrierColor, |
| transitionDuration: transitionDuration, |
| transitionBuilder: transitionBuilder, |
| settings: routeSettings, |
| anchorPoint: anchorPoint, |
| )); |
| } |
| |
| /// Signature for the function that builds a route's primary contents. |
| /// Used in [PageRouteBuilder] and [showGeneralDialog]. |
| /// |
| /// See [ModalRoute.buildPage] for complete definition of the parameters. |
| typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation); |
| |
| /// Signature for the function that builds a route's transitions. |
| /// Used in [PageRouteBuilder] and [showGeneralDialog]. |
| /// |
| /// See [ModalRoute.buildTransitions] for complete definition of the parameters. |
| typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child); |