| // 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'; |
| |
| const Color _kTransparent = const 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 = new 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 new 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: |
| assert(!overlayEntries.first.opaque); |
| // 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 = new 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 = new 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 = new TrainHoppingAnimation(current, nextRoute._animation); |
| } |
| } else { |
| _secondaryAnimation.parent = nextRoute._animation; |
| } |
| } else { |
| _secondaryAnimation.parent = kAlwaysDismissedAnimation; |
| } |
| } |
| |
| /// Whether this route can perform a transition to the given route. |
| /// |
| /// Subclasses can override this method to restrict the set of routes they |
| /// need to coordinate transitions with. |
| bool canTransitionTo(TransitionRoute<dynamic> nextRoute) => true; |
| |
| /// Whether this route can perform a transition from the given route. |
| /// |
| /// Subclasses can override this method to restrict the set of routes they |
| /// need to coordinate transitions with. |
| 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 route that can 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. |
| /// |
| /// This class is typically used as a mixin. |
| abstract class LocalHistoryRoute<T> extends 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. |
| 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(new FlagProperty('isCurrent', value: isCurrent, ifTrue: 'active', ifFalse: 'inactive')); |
| description.add(new 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() => new _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 = new 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 new _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: new Offstage( |
| offstage: widget.route.offstage, // _routeSetState is called if this updates |
| child: new PageStorage( |
| bucket: widget.route._storageBucket, // immutable |
| child: new FocusScope( |
| node: widget.route.focusScopeNode, // immutable |
| child: new RepaintBoundary( |
| child: new AnimatedBuilder( |
| animation: _listenable, // immutable |
| builder: (BuildContext context, Widget child) { |
| return widget.route.buildTransitions( |
| context, |
| widget.route.animation, |
| widget.route.secondaryAnimation, |
| new IgnorePointer( |
| ignoring: widget.route.animation?.status == AnimationStatus.reverse, |
| child: child, |
| ), |
| ); |
| }, |
| child: _page ??= new RepaintBoundary( |
| key: widget.route._subtreeKey, // immutable |
| child: new 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). |
| 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 |
| /// new PageRouteBuilder( |
| /// pageBuilder: (BuildContext context, |
| /// Animation<double> animation, |
| /// Animation<double> secondaryAnimation, |
| /// Widget child, |
| /// ) { |
| /// return new Scaffold( |
| /// appBar: new AppBar(title: new Text('Hello')), |
| /// body: new Center( |
| /// child: new Text('Hello World'), |
| /// ), |
| /// ); |
| /// }, |
| /// transitionsBuilder: ( |
| /// BuildContext context, |
| /// Animation<double> animation, |
| /// Animation<double> secondaryAnimation, |
| /// Widget child, |
| /// ) { |
| /// return new SlideTransition( |
| /// position: new 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 new SlideTransition( |
| /// position: new AlignmentTween( |
| /// begin: const Offset(0.0, 1.0), |
| /// end: Offset.zero, |
| /// ).animate(animation), |
| /// child: new SlideTransition( |
| /// position: new 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 = new FocusScopeNode(); |
| |
| @override |
| void install(OverlayEntry insertionPoint) { |
| super.install(insertionPoint); |
| _animationProxy = new ProxyAnimation(super.animation); |
| _secondaryAnimationProxy = new 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; |
| |
| /// 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 new 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 new 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 = new GlobalKey<_ModalScopeState<T>>(); |
| final GlobalKey _subtreeKey = new GlobalKey(); |
| final PageStorageBucket _storageBucket = new PageStorageBucket(); |
| |
| // 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 = new ColorTween( |
| begin: _kTransparent, |
| end: barrierColor, // changedInternalState is called if this updates |
| ).animate(new CurvedAnimation( |
| parent: animation, |
| curve: Curves.ease, |
| )); |
| barrier = new AnimatedModalBarrier( |
| color: color, |
| dismissible: barrierDismissible, // changedInternalState is called if this updates |
| semanticsLabel: barrierLabel, // changedInternalState is called if this updates |
| ); |
| } else { |
| barrier = new ModalBarrier( |
| dismissible: barrierDismissible, // changedInternalState is called if this updates |
| semanticsLabel: barrierLabel, // changedInternalState is called if this updates |
| ); |
| } |
| return new 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 ??= new _ModalScope<T>( |
| key: _scopeKey, |
| route: this, |
| // _ModalScope calls buildTransitions() and buildChild(), defined above |
| ); |
| } |
| |
| @override |
| Iterable<OverlayEntry> createOverlayEntries() sync* { |
| yield _modalBarrier = new OverlayEntry(builder: _buildModalBarrier); |
| yield new 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. |
| /// |
| /// ## Sample code |
| /// |
| /// 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 = new RouteObserver<PageRoute>(); |
| /// void main() { |
| /// runApp(new MaterialApp( |
| /// home: new Container(), |
| /// navigatorObservers: [routeObserver], |
| /// )); |
| /// } |
| /// |
| /// class RouteAwareWidget extends StatefulWidget { |
| /// State<RouteAwareWidget> createState() => new 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) => new Container(); |
| /// |
| /// } |
| /// ``` |
| 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, () => new 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() { } |
| } |