Past wonderful
π Flutter will know will series – start Overlay of Navigator
π Flutter will know will series — Unsung Heroes _Theatre
Overlay components are used to Overlay components. This component stacking effect is the page stacking effect of our APP, which can be opened page by page. In addition, we also introduced the unsung hero behind Overlay _Theatre. By using _Theatre, we can simply draw the content standing on the stage. We got closer and closer to the truth of Overlay page management.
This article, we introduce a front ββββ Route. Route appears in various places in our code, such as the page we open, in the MaterialRoute, our popover in the RawDialogRoute, and our bottom half Sheet in the _ModalBottomSheetRoute. Let’s take a look at Route, and you’ll have a new look at the page.
RouteWhat is the
Let’s look at the official definition of Route.
An abstraction forAn entry managed by a [Navigator] abstract entity Thisclass defines an abstract interface between the navigator and the routes
that are pushed on and popped off the navigator. Most routes havevisual
affordances.which they place in the navigators Overlay using oneor more
OverlayEntry objectsThis class defines some abstract interfaces that can be used tonavigator ε» poporpush. Most of therouteIt's all visual becauserouteWill be placed inOverlayIn the.Copy the code
From the description above, Route is an abstract interface class managed by the Navigator. This class defines some abstract interfaces that the Navigator can pop or push.
Because of overlays, most routes are visible.
The description doesn’t seem like much, but we look at the interfaces it encapsulates: member variables and member methods.
Member variables
Member variables are the flesh and blood of a class. As interface Route, there are only three member variables.
The property name | type | role |
---|---|---|
_settings | RouteSettings | Route related Settings, such as names, parameters, etc |
overlayEntries | List | Displays the OverlayEntry of the page |
navigator | NavigatorState | The Navigator that hosts the Route |
restorationScopeId | ValueListenable | Id of page data storage and recovery |
These variables are initialized at class declaration time, so they live and die with the Route page. The Navigator is a StatefulWidget that has no ability to draw itself, and its build method constructs subtrees called overlays.
When Route is pushed in, overlayEntries are added to the Overlay, so Route’s are displayed on the page. See the previous section on overlays for π Overlay guide to the start of the Flutter Overlay series – Navigator. So at this point, we know that our pages, our dialogs, are all going to be displayed in the Overlay.
Because Navigator is a StatefulWidget, its State is NavigatorState, which is a reference to Navigator, so Route holds NavigatorState, You can sense the NavigatorState declaration cycle.
RouteSettings is a routing configuration that defines names and parameters for routes. For example, when we want to open a page by name, we construct a Route through the RouteSettings, in the same way that we close the page stack to a Route by name. , etc.
These three attributes are the most important attributes of Route, and restorationScopeId is a new attribute that is responsible for page data recovery, which we will discuss later.
Let’s look at the interface methods defined.
Members of the method
Member variables are the flesh and blood of a class, so member methods are the skeleton of a class, defining the behavior of a Route. The Navigator implements administrative functions by calling these interfaces: initialize, open, close, change, judge, and so on. Here we introduce them one by one:
Initialize install
This method is the initialization method of the Route, which is called first when the Route is inserted into the Navigator (push).
OverlayEntries of Route are usually added to the Overlay, and different subclasses add their own content, such as animation effects, etc.
DidPush into the page
@protected @mustCallSuper TickerFuture didPush() { return TickerFuture.complete().. then<void>((void _) { if (navigator? .widget.requestFocus == true) { navigator! .focusScopeNode.requestFocus(); }}); }Copy the code
After Route is pushed, install is called to initialize, and then this method is called. The default behavior is to regain focus after the Route content is displayed on the page.
Add didAdd to the page
The default processing behavior is the same as didPush, and the timing of the call is also after Install. The only difference is that the didAdd scenario is a Route that does not require transitions, and the Route needs to be on the page immediately. For example, when the page data is restored, the page is displayed immediately, and the reconstructed Route is didAdd.
Switch pages to didReplace
The default is not implemented, and the call time is after install. The difference is that Route appears as a switch.
Whether to turn off the page’s willPop
This is where the Navigator’s maybePop is called to determine whether to close the Route.
Normally, Pop is not called when there is only one Route left in the stack, because it will appear black.
Launch the page for didPop
This method is called when a Pop Route request is issued.
The return value of this method is critical because it guarantees that the animation will not be executed. If the method returns true, the Navigator removes the route from the history list, but Dispose does not call.
DidComplete for Route completion
Computes the asynchronous result of pop
Become top level Route didPopNext
The nextRoute parameter is popped and the current Route becomes the topmost Route.
Route hierarchy changes in didChangeNext
Specify the nextRoute as nextRoute. This method is called whenever the next route of the route changes.
Route hierarchy change didChangePrevious
The Route preceding the Route is specified as previousRoute. This method is called whenever the Route preceding the Route changes.
The changedInternalState of the internal state change
This method is called when the internal state of the Route changes.
This method is called on exchanges like Willhandlepopexchanges, didPop, offstage and other internal values.
For example, ModalRoute uses this method to notify child nodes that information has changed.
ChangedExternalState of external state changes
When Navigator is rebuilt, that indicates that Route may need to be rebuilt as well. For example, when the MaterialApp is rebuilt.
This ensures that the Route relies on widgets that build the MaterialApp to be notified when state occurs.
Dispose released by Route
The Route will remove overlays and release other resources that no longer hold references to the Navigator
Route’s network
We now know that Route is an abstract class that Navagator uses to manage pages. An array of overlayEntry that is held internally and inserted into the Overlay. And defines a lot of abstract methods, the design of these methods is object-oriented design, as a Route to have these capabilities.
Let’s look at Route’s network.
Above is the system network of Route. Route and its subclasses hold multiple overlayentries, and each OverlayEntry will be displayed on the page. For example, the black mask of the popover is an OverlayEntry. The popover content displayed is again an OverlayEntry. Route also holds NavigatorState, which gives you access to NavigatorState properties and methods, such as focus, and so on.
In addition, routes are inherited, with each layer implementing specific functions. OverlayRoute implements the ability to insert OverlayEntry into an Overlay. TransitionRoute implements the animation switch function of Route, and we see that it holds the animation object and animation controller object. ModalRoute is relatively complete and does return interception, which is the usual WillPopScope component. Based on ModalRoute, there are two types: PopupRoute, popWindow type PopupRoute, PageRoute, Rout E is the most common Route in our development.
Let’s take a look at the respective functionality added by the different levels through initialization and release.
Route is linearly initialized
The Route initialization is the install method we talked about above. Let’s look at what is initialized and how each layer is initialized.
Route.install
Route is the most basic class that defines the specification.
void install() { }
Copy the code
As you can see, the method is declared in Route, but not implemented.
OverlayRoute.install
@override
void install() {
assert(_overlayEntries.isEmpty);
_overlayEntries.addAll(createOverlayEntries());
super.install();
}
@factory
ε―θΏδ»£<OverlayEntry> createOverlayEntries();
Copy the code
Step 1: OverlayRoute overlayEntries add all the overlayEntries of the Route, which may be one or more.
Note: createOverlayEntries are abstract methods, and non-abstract subclasses must tell the Framework what to display!
As we will see later, createOverlayEntries returns only those to be added or displayed.
TransitionRoute.install
@override
void install() {
_controller = createAnimationController(); / / first place_animation = createAnimation() .. addStatusListener(_handleStatusChanged);/ / the third place
super.install();/ / the first around
if(_animation! .isCompleted && overlayEntries.isNotEmpty) { overlayEntries.first.opaque = opaque; } } AnimationController createAnimationController() {final Duration duration = transitionDuration;
final Duration reverseDuration = reverseTransitionDuration;
return AnimationController( / / in the second place
duration: duration,
reverseDuration: reverseDuration,
debugLabel: debugLabel,
vsync: navigator!,
);
}
Copy the code
The first code creates the animation controller, which by default is an animation controller with animation duration transitionDuration
The default process created is createAnimationController, pay attention to in the second place code here.
We know that the animation needs a vsync parameter. Vsync is usually mixed with the TickerProviderStateMixin State, and NavigatorState is such a State, so vsync passes in navigator, This is why the Route holds a reference to the Navigator.
The third code creates an animation object, _animation, that drives the page transitions and adds a state listener to the animation.
Listen to:
void _handleStatusChanged(AnimationStatus status) {
switch (status) {
case AnimationStatus.completed:
if (overlayEntries.isNotEmpty)
overlayEntries.first.opaque = opaque;
break;
/ /... Omit code}}Copy the code
For example, animation completion, where opaque of the first overlayEntry in the overlayEntries array is set to opaque of the constructor.
Remember what opaque property does? If opaque is true, the overwritten content will not be drawn. If opaque is true, the bottom page will not be drawn. You can watch π Flutter will know will series — Unsung Heroes _Theatre.
Last but not least, notice # 4: the super content of the TransitionRoute is initialized before the animation itself is initialized, i.e. the animation is initialized before the overlayEntry content.
Thus: the TransitionRoute creates the animation object (the current animation progress) and the animation controller.
ModalRoute.install
@override
void install() {
super.install();
_animationProxy = ProxyAnimation(super.animation);
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
}
Copy the code
ModalRoute generates two agent animations.
Proxy: Accepts an Animation class as its parent and forwards only the state of that parent. The value of _animationProxy is the value of animation.
Here’s why two animations are generated: the first one needs to exit and the second one needs to enter.
Animationproxy: animates the push and pop animations of the current Route, and animates the animations that drive the previous Route, such as the pop animations that drive the previous Route.
Secondaryanimationproxy: The animation of the Route placed on top of the Route, which connects the Route itself to the entry and exit animations of the new Route.
This is the install method, OverlayRoute. Install also has a createOverlayEntries abstract method. Let’s look at the implementation of ModalRoute.
@override
ε―θΏδ»£<OverlayEntry> createOverlayEntries() sync* {
yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
yield _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}
Copy the code
A Dart syntax is used here: sync* and yield
Sync * and yield are a set of syntactic keywords for Dart. Sync * is placed on the method signature to indicate that the method returns an Iterable array object. The elements of the array are yield generated for each row.
So the array returned by the createOverlayEntries method contains two elements — modalBarrier and OverlayEntry.
MaintainState field: maintainState field, which indicates whether the Route status is required to be maintained. This is the Entry in the maintainState area described earlier.
We know that pages are superimposed, so if a page is not displayed and is covered by the top page, does it need to live in memory? That’s what this field means. If true, the Route is saved, and some Future results of the current Route, the previous Route overridden, can be computed normally, such as refreshing, etc. If set to false, it will be reclaimed when memory is tight.
The familiar MaterialPageRoute is true.
Now, let’s take a look at what was added.
Mask _modalBarrier
Widget _buildModalBarrier(BuildContext context) {
Widget barrier;
if(barrierColor ! =null&& barrierColor! .alpha ! =0 && !offstage) { // changedInternalState is called if barrierColor or offstage updates
finalAnimation<Color? > color = animation! .drive( ColorTween( begin: barrierColor! .withOpacity(0.0),
end: barrierColor, // changedInternalState is called if barrierColor updates
).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates
);
barrier = AnimatedModalBarrier( / / first place
color: color,
dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
barrierSemanticsDismissible: semanticsDismissible,
);
} else {
barrier = ModalBarrier( / / first place
dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
barrierSemanticsDismissible: semanticsDismissible,
);
}
/ /... Omit code
barrier = IgnorePointer(/ / in the second placeignoring: animation! .status == AnimationStatus.reverse ||// changedInternalState is called when animation.status updatesanimation! .status == AnimationStatus.dismissed,// dismissed is possible when doing a manual pop gesture
child: barrier,
);
return barrier;
}
Copy the code
BarrierColor is the mask color we often see in dialog boxes.
Barrierdistransmissible is whether we click on a mask to disappear or not.
ModalBarrier, animated or not, is the core of the Route. ModalBarrier is the first one in the Route.
ModalBarrier does three things: add a mask background, click the mask to go back to the page, and prevent events from penetrating.
Ignoring true, the Barrier does not respond to the gesture. The dialog box is displayed and the black mask surrounding the barrier responds to the gesture.
The main purpose of adding this layer is to prevent gestures from passing to the next layer of the page.
The actual content _buildModalScope
Widget _buildModalScope(BuildContext context) {
// To be sorted before the _modalBarrier.
return_modalScopeCache ?? = Semantics( sortKey:const OrdinalSortKey(0.0),
child: _ModalScope<T>(
key: _scopeKey,
route: this.// _ModalScope calls buildTransitions() and buildChild(), defined above)); }Copy the code
_buildModalScope builds _ModalScope and specifies a member variable key instead of a temporary variable key. This allows you to reuse elements, reducing repetitive builds. π Flutter must know must know series — Update reuse mechanism of Element
Let’s look at _ModalScope. _ModalScope is a StatefulWidget, and we can understand it as a StatefulWidget.
Look at the initState method first
@override
void initState() {
super.initState();
final List<Listenable> animations = <Listenable>[
if(widget.route.animation ! =null) widget.route.animation! .if(widget.route.secondaryAnimation ! =null) widget.route.secondaryAnimation!,
];
_listenable = Listenable.merge(animations); / / first place
if(widget.route.isCurrent && _shouldRequestFocus) { widget.route.navigator! .focusScopeNode.setFirstFocus(focusScopeNode);/ / in the second place}}Copy the code
Merge the Listenable function. Merge the Listenable function. Merge the Listenable function. This allows _listEnable to respond to both animation and secondaryAnimation changes.
Second: the focus scope is moved to the current scope
For the focusScopeNode, see this article: Talk about Focus, the unsung hero of Flutter
Let’s look at the build method, which is the highlight.
@override
Widget build(BuildContext context) {
return AnimatedBuilder( / / first place
animation: widget.route.restorationScopeId,
builder: (BuildContext context, Widget? child) {
return RestorationScope(
restorationId: widget.route.restorationScopeId.value,
child: child!,
);
},
child: _ModalScopeStatus(
route: widget.route,
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
canPop: widget.route.canPop, // _routeSetState is called if this updates
child: Offstage(
offstage: widget.route.offstage, // _routeSetState is called if this updates
child: PageStorage(
bucket: widget.route._storageBucket, // immutable
child: Builder(
builder: (BuildContext context) {
return Actions(
actions: <Type, Action<Intent>>{
DismissIntent: _DismissModalAction(context),
},
child: PrimaryScrollController(
controller: primaryScrollController,
child: FocusScope(
node: focusScopeNode, // immutable
child: FocusTrap(
focusScopeNode: focusScopeNode,
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _listenable, // immutable
builder: (BuildContext context, Widget? child) {
returnwidget.route.buildTransitions( context, widget.route.animation! , widget.route.secondaryAnimation! , AnimatedBuilder( animation: widget.route.navigator? .userGestureInProgressNotifier ?? ValueNotifier<bool> (false),
builder: (BuildContext context, Widget? child) {
final boolignoreEvents = _shouldIgnoreFocusRequest; focusScopeNode.canRequestFocus = ! ignoreEvents;returnIgnorePointer( ignoring: ignoreEvents, child: child, ); }, child: child, ), ); }, child: _page ?? = RepaintBoundary( key: widget.route._subtreeKey,// immutable
child: Builder(
builder: (BuildContext context) {
returnwidget.route.buildPage( context, widget.route.animation! , widget.route.secondaryAnimation! ,); }, ((), ((), ((), ((), ((), ((); }, (), (), (), (); }Copy the code
The component hierarchy is very deep, but the components are relatively simple, so let’s look at them layer by layer.
Layer 1: AnimatedBuilder at site 1. AnimatedBuilder is an animation component. The component that you want to animate is the child property, \_ModalScopeStatus. The animation that you add is the animation property, restorationScopeId of route. We introduced it in the member variables section. This layer adds animation to restorationScopeId without affecting the actual display.
Layer 2: _ModalScopeStatus, which is a normal InheritedWidget that passes down the state of ModalScope (who Route is, whether it’s a top-level Route, whether it can Pop).
The third layer: Offstage. Whether the current is not on “stage”, not on stage layout is not drawn. That is, Route is not drawn when the offstage property of Route is true. The default value is false. However, if the page Hero animation, the HeroController controller will use this value to control the effect of the later page. We just need to know that this value doesn’t matter to us.
Level 4: PageStorage, which provides a bucket for storing data for elements in a page. The child node can obtain the bucket through pagestorage.of. For example, ScrollPosition uses this feature to store the offset of a scroll.
@protected
voidsaveScrollOffset() { PageStorage.of(context.storageContext)? .writeState(context.storageContext, pixels); }Copy the code
The fifth layer: Actions, Actions is the new action ββββ intent component, Route DismissIntent corresponds to _DismissModalAction, The navigator.of (context).maybepop () behavior is performed. But this addition doesn’t have much impact on our mobile devices.
Level 6: PrimaryScrollController is the default ScrollController, which is why we still have a ScrollController available even if we don’t manually add a ScrollController to the ListView.
Level 7: FocusScope, which adds a focus domain to the page and provides a focus.
Layer 8: FocusTrap, which is a component for the Web and has little to do with our mobile terminal.
Level 9: RepaintBoundary: add a drawn boundary to the page, that is, nodes above this node do not need to be drawn.
When we talked about layout, we talked about the need for boundaries in layout. If the parent node depends on the size information of the child node, then when the child node needs to be laid out, the parent node will also be laid out. πFlutter must know must Know series – Render tree layout
Similarly, drawing requires boundaries. If a node needs to be drawn, it looks at the isRepaintBoundary value, and if it’s true, it just draws itself and doesn’t look up at the parent node. RepaintBoundary the isRepaintBoundary value of the component is true.
The first few layers are all preparation for a page, and the tenth layer below is where the real complexity comes in.
The tenth layer is still an AnimatedBuilder animation component that animates Route. AnimatedBuilder is the Builder effect that adds one parameter to the constructor’s child argument. The driver for animation is the parameter _listenable. Remember who _listenable is? It is the merger of animation and secondaryAnimation.
The basic model is as follows:
AnimatedBuilder(
animation: _listenable,
builder: (BuildContext context, Widget child) {
return B;
},
child: A,
)
Copy the code
We can assume that when the value of _listenable (that is, the value of the animation) changes, the page displays B. And the child argument to builder is A.
Now let’s see who A is.
RepaintBoundary(
key: widget.route._subtreeKey, // immutable
child: Builder(
builder: (BuildContext context) {
returnwidget.route.buildPage( context, widget.route.animation! , widget.route.secondaryAnimation! ,); },),)Copy the code
A is the Builder component, so A is the Widget built by Route’s buildPage method.
Similarly, the child of the parameter in B is the result of the buildPage method. Let’s see who B is.
widget.route.buildTransitions( context, widget.route.animation! , widget.route.secondaryAnimation! , AnimatedBuilder( animation: widget.route.navigator? .userGestureInProgressNotifier ?? ValueNotifier<bool> (false),
builder: (BuildContext context, Widget? child) {
final boolignoreEvents = _shouldIgnoreFocusRequest; focusScopeNode.canRequestFocus = ! ignoreEvents;return IgnorePointer(
ignoring: ignoreEvents,
child: child,
);
},
child: child,
),
)
Copy the code
B is the Widget for Route’s buildTransitions build.
AnimatedBuilder appears again in B. This animation does not affect the rendering of the page, but depends on the animation state of the route to determine whether the screen does not block gestures and whether to display builds.
So now we know that layer 10 uses the animation mechanism, and the component for the animation is the Route buildPage method, and the specific animation is the Route buildTransitions method.
For example, when the animation is 0, we let buildTransitions return child, and the page displays the Route’s buildPage. When animation is 0.5, we let buildTransitions return the Text Text component, which is what the page displays. When the animation is 1, we let buildTransitions return child, and the page will still display the Route’s buildPage. So with this feature, we can show panning, zooming and so on.
At this point, modalroute. install is initialized and two overlayentries are constructed. One is a mask, which handles colors, clicking back, and so on. One is _ModalScope, which has more than ten nodes: it realizes basic Modal Route information transmission, focus, drawing not drawing, data bucket, drawing boundary, animation and so on.
ModalRoute initialization, basically is the initialization of Route, function basic realization. Developers simply implement buildPage and buildTransitions without technical details, which could be a template pattern.
BuildPage is the content for the page display, and buildTransitions is the animated display of the content.
RawDialogRoute.install
ModalRoute above has done most of its work and defines the inheritance tasks of subclasses, overriding buildPage to determine what to display and overriding buildTransitions to determine transitions.
As the Route of the concrete dialog box, it only needs to implement these two methods. Let’s use animation as an example:
@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); }Copy the code
We see that the default animation effect for the dialog box is defined in buildTransitions — linear fade animation.
summary
We looked at the Route initialization process for each layer, from the top to the concrete reading. We know:
- The Route of the
OverlayEntry
Array, you’re going to add Overlay to the Navigator, so the page is superimposed. - Each layer has its own embodiment.
OverlayRoute
To complete theOverlayEntry
To add,TransitionRoute.install
Animation and animation controller are generated. ModalRoute.install
It is a combination of mask and animation effects.buildPage
Is what’s displayed,buildTransitions
Animation is the content displayed at any time, through the developer is the animation progress, we can achieve their own effects according to the animation progress.PageRoute
Is the parent of the page Route,RawDialogRoute
Is the Route of the dialog box that provides the defaultFadeTransition
The animation.- The page’s fence is
true
, so it does not layout and draw blocked pages.
Route linear release
Having looked at linear initialization, let’s look at linear freeing resources. Releasing resources is the Dispose method. Compared to the initialization, the release is much simpler, basically the request is released.
Route.dispose
void dispose() {
_navigator = null;
}
Copy the code
No more references to navigator to avoid memory leaks.
OverlayRoute.dispose
@override
void dispose() {
_overlayEntries.clear();
super.dispose();
}
Copy the code
Corresponding to the initialization of OverlayRoute, OverlayEntry is constructed with createOverlayEntries.
Therefore, OverlayEntry is removed from dispose.
TransitionRoute.dispose
@override
voiddispose() { _animation? .removeStatusListener(_handleStatusChanged);if(willDisposeAnimationController) { _controller? .dispose(); } _transitionCompleter.complete(_result);super.dispose();
}
Copy the code
Dispose dispose of the Controller corresponding to the initialization of the TransitionRoute, which constructs the animation Controller.
At this point, the resources held by the initialization have been released, and subsequent calls do not need to be made.
conclusion
So far, we have basically understood what Route is, and have a certain understanding of the hierarchical system and structure of Route. For us developers, we just need to know:
- Pages are fenced, so you don’t have to worry about drawing non-top-level pages
- in
buildPage
Display the content we want inbuildTransitions
According to the animation progress, to achieve their own effects.
Based on this section, we can take a closer look at Route push and POP flows later.