// Copyright 2015 The Chromium 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 'package:flutter/foundation.dart'; import 'basic.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 'transitions.dart'; // Examples can assume: // dynamic routeObserver; const Color _kTransparent = Color(0x00000000); /// A route that displays widgets in the [Navigator]'s [Overlay]. abstract class OverlayRoute<T> extends Route<T> { /// Creates a route that knows how to interact with an [Overlay]. OverlayRoute({ RouteSettings settings, }) : super(settings: settings); /// Subclasses should override this getter to return the builders for the overlay. Iterable<OverlayEntry> createOverlayEntries(); /// The entries this route has placed in the overlay. @override List<OverlayEntry> get overlayEntries => _overlayEntries; final List<OverlayEntry> _overlayEntries = <OverlayEntry>[]; @override void install(OverlayEntry insertionPoint) { assert(_overlayEntries.isEmpty); _overlayEntries.addAll(createOverlayEntries()); navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint); super.install(insertionPoint); } /// 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() { for (OverlayEntry entry in _overlayEntries) entry.remove(); _overlayEntries.clear(); super.dispose(); } } /// A route with entrance and exit transitions. abstract class TransitionRoute<T> extends OverlayRoute<T> { /// Creates a route that animates itself when it is pushed or popped. TransitionRoute({ RouteSettings settings, }) : super(settings: settings); // TODO(ianh): once https://github.com/dart-lang/sdk/issues/31543 is fixed, // this should be removed. TransitionRoute._settings(RouteSettings settings) : super(settings: 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>(); /// The duration the transition lasts. Duration get transitionDuration; /// 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. bool get opaque; @override bool get finishedWhenPopped => _controller.status == AnimationStatus.dismissed; /// 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; /// 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. AnimationController createAnimationController() { assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); final Duration duration = transitionDuration; assert(duration != null && duration >= Duration.zero); return AnimationController( duration: duration, 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; break; case AnimationStatus.forward: case AnimationStatus.reverse: if (overlayEntries.isNotEmpty) overlayEntries.first.opaque = false; break; case AnimationStatus.dismissed: // We might still be the current route if a subclass is controlling the // the transition and hits the dismissed status. For example, the iOS // back gesture drives this animation to the dismissed status before // popping the navigator. if (!isCurrent) { navigator.finalizeRoute(this); assert(overlayEntries.isEmpty); } break; } changedInternalState(); } /// 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. Animation<double> get secondaryAnimation => _secondaryAnimation; final ProxyAnimation _secondaryAnimation = ProxyAnimation(kAlwaysDismissedAnimation); @override void install(OverlayEntry insertionPoint) { assert(!_transitionCompleter.isCompleted, 'Cannot install a $runtimeType after disposing it.'); _controller = createAnimationController(); assert(_controller != null, '$runtimeType.createAnimationController() returned null.'); _animation = createAnimation(); assert(_animation != null, '$runtimeType.createAnimation() returned null.'); super.install(insertionPoint); } @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.'); _animation.addStatusListener(_handleStatusChanged); return _controller.forward(); } @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; _animation.addStatusListener(_handleStatusChanged); 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); } void _updateSecondaryAnimation(Route<dynamic> nextRoute) { if (nextRoute is TransitionRoute<dynamic> && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) { final Animation<double> current = _secondaryAnimation.parent; if (current != null) { if (current is TrainHoppingAnimation) { TrainHoppingAnimation newAnimation; newAnimation = TrainHoppingAnimation( current.currentTrain, nextRoute._animation, onSwitchedTrain: () { assert(_secondaryAnimation.parent == newAnimation); assert(newAnimation.currentTrain == nextRoute._animation); _secondaryAnimation.parent = newAnimation.currentTrain; newAnimation.dispose(); } ); _secondaryAnimation.parent = newAnimation; current.dispose(); } else { _secondaryAnimation.parent = TrainHoppingAnimation(current, nextRoute._animation); } } else { _secondaryAnimation.parent = nextRoute._animation; } } else { _secondaryAnimation.parent = kAlwaysDismissedAnimation; } } /// 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 /// [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 [buildTransitions] `secondaryAnimation` parameter /// value will be [kAlwaysDismissedAnimation]. In other words, this route /// will not animate when 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 /// [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 [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 [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 /// [buildTransitions] `secondaryAnimation` to run. bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => true; @override void dispose() { assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.'); _controller?.dispose(); _transitionCompleter.complete(_result); super.dispose(); } /// A short description of this route useful for debugging. String get debugLabel => '$runtimeType'; @override String toString() => '$runtimeType(animation: $_controller)'; } /// An entry in the history of a [LocalHistoryRoute]. class LocalHistoryEntry { /// Creates an entry in the history of a [LocalHistoryRoute]. LocalHistoryEntry({ this.onRemove }); /// Called when this entry is removed from the history of its associated [LocalHistoryRoute]. final VoidCallback onRemove; LocalHistoryRoute<dynamic> _owner; /// Remove this entry from the history of its associated [LocalHistoryRoute]. void remove() { _owner.removeLocalHistoryEntry(this); assert(_owner == null); } void _notifyRemoved() { if (onRemove != null) onRemove(); } } /// 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. mixin LocalHistoryRoute<T> on Route<T> { List<LocalHistoryEntry> _localHistory; /// 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 sample} /// /// 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 { /// @override /// Widget build(BuildContext context) { /// return MaterialApp( /// initialRoute: '/', /// routes: { /// '/': (BuildContext context) => HomePage(), /// '/second_page': (BuildContext context) => SecondPage(), /// }, /// ); /// } /// } /// /// class HomePage extends StatefulWidget { /// HomePage(); /// /// @override /// _HomePageState createState() => _HomePageState(); /// } /// /// class _HomePageState extends State<HomePage> { /// @override /// Widget build(BuildContext context) { /// return Scaffold( /// body: Center( /// child: Column( /// mainAxisSize: MainAxisSize.min, /// children: <Widget>[ /// Text('HomePage'), /// // Press this button to open the SecondPage. /// RaisedButton( /// child: Text('Second Page >'), /// onPressed: () { /// Navigator.pushNamed(context, '/second_page'); /// }, /// ), /// ], /// ), /// ), /// ); /// } /// } /// /// class SecondPage extends StatefulWidget { /// @override /// _SecondPageState createState() => _SecondPageState(); /// } /// /// class _SecondPageState extends State<SecondPage> { /// /// bool _showRectangle = false; /// /// 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 localNavContent = _showRectangle /// ? Container( /// width: 100.0, /// height: 100.0, /// color: Colors.red, /// ) /// : RaisedButton( /// child: Text('Show Rectangle'), /// onPressed: _navigateLocallyToShowRectangle, /// ); /// /// return Scaffold( /// body: Center( /// child: Column( /// mainAxisAlignment: MainAxisAlignment.center, /// children: <Widget>[ /// localNavContent, /// RaisedButton( /// child: Text('< Back'), /// onPressed: () { /// // Pop a route. If this is pressed while the red rectangle is /// // visible then it will 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); if (wasEmpty) 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)); _localHistory.remove(entry); entry._owner = null; entry._notifyRemoved(); if (_localHistory.isEmpty) changedInternalState(); } @override Future<RoutePopDisposition> willPop() async { if (willHandlePopInternally) return RoutePopDisposition.pop; return await 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(); if (_localHistory.isEmpty) changedInternalState(); return false; } return super.didPop(result); } @override bool get willHandlePopInternally { return _localHistory != null && _localHistory.isNotEmpty; } } class _ModalScopeStatus extends InheritedWidget { const _ModalScopeStatus({ Key key, @required this.isCurrent, @required this.canPop, @required this.route, @required Widget child }) : assert(isCurrent != null), assert(canPop != null), assert(route != null), assert(child != null), super(key: key, child: child); final bool isCurrent; final bool canPop; final Route<dynamic> route; @override bool updateShouldNotify(_ModalScopeStatus old) { return isCurrent != old.isCurrent || canPop != old.canPop || 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')); } } class _ModalScope<T> extends StatefulWidget { const _ModalScope({ Key key, this.route, }) : super(key: key); 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. Listenable _listenable; @override void initState() { super.initState(); final List<Listenable> animations = <Listenable>[]; if (widget.route.animation != null) animations.add(widget.route.animation); if (widget.route.secondaryAnimation != null) animations.add(widget.route.secondaryAnimation); _listenable = Listenable.merge(animations); } @override void didUpdateWidget(_ModalScope<T> oldWidget) { super.didUpdateWidget(oldWidget); assert(widget.route == oldWidget.route); } @override void didChangeDependencies() { super.didChangeDependencies(); _page = null; } void _forceRebuildPage() { setState(() { _page = null; }); } // This should be called to wrap any changes to route.isCurrent, route.canPop, // and route.offstage. void _routeSetState(VoidCallback fn) { setState(fn); } @override Widget build(BuildContext context) { return _ModalScopeStatus( route: widget.route, isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates canPop: widget.route.canPop, // _routeSetState is called if this updates child: Offstage( offstage: widget.route.offstage, // _routeSetState is called if this updates child: PageStorage( bucket: widget.route._storageBucket, // immutable child: FocusScope( node: widget.route.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, IgnorePointer( ignoring: widget.route.animation?.status == AnimationStatus.reverse, 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. abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> { /// Creates a route that blocks interaction with previous routes. ModalRoute({ RouteSettings settings, }) : super._settings(settings); // 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. /// /// Typical usage is as follows: /// /// ```dart /// ModalRoute route = ModalRoute.of(context); /// ``` /// /// The given [BuildContext] will be rebuilt if the state of the route changes /// (specifically, if [isCurrent] or [canPop] change value). @optionalTypeArgs static ModalRoute<T> of<T extends Object>(BuildContext context) { final _ModalScopeStatus widget = context.inheritFromWidgetOfExactType(_ModalScopeStatus); return widget?.route; } /// 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 (e.g. the value of [canPop]). /// /// 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. /// /// 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. /// /// ```dart /// PageRouteBuilder( /// pageBuilder: (BuildContext context, /// Animation<double> animation, /// Animation<double> secondaryAnimation, /// Widget child, /// ) { /// return Scaffold( /// appBar: AppBar(title: Text('Hello')), /// body: 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 /// ); /// }, /// ); /// ``` /// /// 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. /// /// 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. /// /// 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 /// transitionsBuilder: ( /// BuildContext context, /// Animation<double> animation, /// Animation<double> secondaryAnimation, /// Widget child, /// ) { /// return SlideTransition( /// position: AlignmentTween( /// begin: const Offset(0.0, 1.0), /// end: Offset.zero, /// ).animate(animation), /// child: SlideTransition( /// position: TweenOffset( /// begin: Offset.zero, /// end: const Offset(0.0, 1.0), /// ).animate(secondaryAnimation), /// child: child, /// ), /// ); /// } /// ``` /// /// 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; } /// The node this route will use for its root [FocusScope] widget. final FocusScopeNode focusScopeNode = FocusScopeNode(); @override void install(OverlayEntry insertionPoint) { super.install(insertionPoint); _animationProxy = ProxyAnimation(super.animation); _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation); } @override TickerFuture didPush() { navigator.focusScopeNode.setFirstFocus(focusScopeNode); return super.didPush(); } @override void dispose() { focusScopeNode.detach(); super.dispose(); } // The API for subclasses to override - used by this class /// 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 will cause the /// current route to be popped (see [Navigator.pop]) 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 color, /// [changedInternalState] should be invoked so that the change can take /// effect. /// /// See also: /// /// * [barrierColor], which controls the color of the scrim for this route. /// * [ModalBarrier], the widget that implements this feature. 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. bool get semanticsDismissible => true; /// 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 [offstage] is /// true. /// /// While the route is animating into position, the color is animated from /// transparent to the specified color. /// /// If this getter would ever start returning a different color, /// [changedInternalState] should be invoked so that the change can take /// effect. /// /// See also: /// /// * [barrierDismissible], which controls the behavior of the barrier when /// tapped. /// * [ModalBarrier], the widget that implements this feature. Color get barrierColor; /// 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. /// /// If this getter would ever start returning a different color, /// [changedInternalState] should be invoked so that the change can take /// effect. /// /// See also: /// /// * [barrierDismissible], which controls the behavior of the barrier when /// tapped. /// * [ModalBarrier], the widget that implements this feature. String get barrierLabel; /// 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. /// /// The value of this getter should not change during the lifetime of the /// object. It is used by [createOverlayEntries], which is called by /// [install] near the beginning of the route lifecycle. 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]). 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; } /// 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 the value of the first callback added with /// [addScopedWillPopCallback] that returns false. If they all return true, /// returns the inherited method's result (see [Route.willPop]). /// /// 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 (WillPopCallback callback in List<WillPopCallback>.from(_willPopCallbacks)) { if (!await callback()) return RoutePopDisposition.doNotPop; } return await super.willPop(); } /// Enables this route to veto attempts by the user to dismiss it. /// /// 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: askTheUserIfTheyAreSure, /// child: ..., /// ); /// } /// ``` /// /// 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. /// /// To register a callback manually, look up the enclosing [ModalRoute] in a /// [State.didChangeDependencies] callback: /// /// ```dart /// ModalRoute<dynamic> _route; /// /// @override /// void didChangeDependencies() { /// super.didChangeDependencies(); /// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure); /// _route = ModalRoute.of(context); /// _route?.addScopedWillPopCallback(askTheUserIfTheyAreSure); /// } /// ``` /// /// 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 /// @override /// void dispose() { /// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure); /// _route = null; /// super.dispose(); /// } /// ``` /// /// 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]. /// 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(); } @override void changedExternalState() { super.changedExternalState(); if (_scopeKey.currentState != null) _scopeKey.currentState._forceRebuildPage(); } /// Whether this route can be popped. /// /// When this changes, the route will rebuild, and any widgets that used /// [ModalRoute.of] will be notified. bool get canPop => !isFirst || willHandlePopInternally; // Internals final GlobalKey<_ModalScopeState<T>> _scopeKey = GlobalKey<_ModalScopeState<T>>(); final GlobalKey _subtreeKey = GlobalKey(); final PageStorageBucket _storageBucket = PageStorageBucket(); static final Animatable<double> _easeCurveTween = CurveTween(curve: Curves.ease); // one of the builders OverlayEntry _modalBarrier; Widget _buildModalBarrier(BuildContext context) { Widget barrier; if (barrierColor != null && !offstage) { // changedInternalState is called if these update assert(barrierColor != _kTransparent); final Animation<Color> color = animation.drive( ColorTween( begin: _kTransparent, end: barrierColor, // changedInternalState is called if this updates ).chain(_easeCurveTween), ); barrier = AnimatedModalBarrier( color: color, dismissible: barrierDismissible, // changedInternalState is called if this updates semanticsLabel: barrierLabel, // changedInternalState is called if this updates barrierSemanticsDismissible: semanticsDismissible, ); } else { barrier = ModalBarrier( dismissible: barrierDismissible, // changedInternalState is called if this updates semanticsLabel: barrierLabel, // changedInternalState is called if this updates barrierSemanticsDismissible: semanticsDismissible, ); } return IgnorePointer( ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture child: 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) { return _modalScopeCache ??= _ModalScope<T>( key: _scopeKey, route: this, // _ModalScope calls buildTransitions() and buildChild(), defined above ); } @override Iterable<OverlayEntry> createOverlayEntries() sync* { yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier); yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState); } @override String toString() => '$runtimeType($settings, animation: $_animation)'; } /// A modal route that overlays a widget over the current route. abstract class PopupRoute<T> extends ModalRoute<T> { /// Initializes the [PopupRoute]. PopupRoute({ RouteSettings settings, }) : super(settings: settings); @override bool get opaque => false; @override bool get maintainState => true; } /// 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 [PageRoute] /// variants, the `RouteObserver<PageRoute<dynamic>>` type may be used. /// /// {@tool sample} /// /// 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<PageRoute> routeObserver = RouteObserver<PageRoute>(); /// void main() { /// runApp(MaterialApp( /// home: Container(), /// navigatorObservers: [routeObserver], /// )); /// } /// /// class RouteAwareWidget extends StatefulWidget { /// 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>>{}; /// 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, () => Set<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); for (R route in _listeners.keys) { final Set<RouteAware> subscribers = _listeners[route]; subscribers?.remove(routeAware); } } @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 (RouteAware routeAware in previousSubscribers) { routeAware.didPopNext(); } } final List<RouteAware> subscribers = _listeners[route]?.toList(); if (subscribers != null) { for (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 (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() { } } class _DialogRoute<T> extends PopupRoute<T> { _DialogRoute({ @required RoutePageBuilder pageBuilder, bool barrierDismissible = true, String barrierLabel, Color barrierColor = const Color(0x80000000), Duration transitionDuration = const Duration(milliseconds: 200), RouteTransitionsBuilder transitionBuilder, RouteSettings settings, }) : assert(barrierDismissible != null), _pageBuilder = pageBuilder, _barrierDismissible = barrierDismissible, _barrierLabel = barrierLabel, _barrierColor = barrierColor, _transitionDuration = transitionDuration, _transitionBuilder = transitionBuilder, super(settings: settings); 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; @override Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return Semantics( child: _pageBuilder(context, animation, secondaryAnimation), scopesRoute: true, explicitChildNodes: true, ); } @override Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { if (_transitionBuilder == null) { return FadeTransition( opacity: CurvedAnimation( parent: animation, curve: Curves.linear, ), child: child); } // Some default transition 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 `barrierDismissible` argument is used to determine whether this route /// can be dismissed by tapping the modal barrier. This argument defaults /// to true. 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 "Dismiss". /// /// 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. /// /// Returns a [Future] that resolves to the value (if any) that was passed to /// [Navigator.pop] when the dialog was closed. /// /// 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)`. /// /// See also: /// /// * [showDialog], which displays a Material-style dialog. /// * [showCupertinoDialog], which displays an iOS-style dialog. Future<T> showGeneralDialog<T>({ @required BuildContext context, @required RoutePageBuilder pageBuilder, bool barrierDismissible, String barrierLabel, Color barrierColor, Duration transitionDuration, RouteTransitionsBuilder transitionBuilder, }) { assert(pageBuilder != null); assert(!barrierDismissible || barrierLabel != null); return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>( pageBuilder: pageBuilder, barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, barrierColor: barrierColor, transitionDuration: transitionDuration, transitionBuilder: transitionBuilder, )); } /// 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);