resolveState method
The chrome to render right now, or null when nothing should be shown.
Implementation
GlassNavPinnedState? resolveState() {
if (!isActive) return null;
// A committed swipe plays from the frozen snapshot, not the registry: the
// outgoing route unregisters when its pop finishes, which happens before
// this animation does.
final exiting = _commitExitSnapshot;
if (exiting != null) {
return GlassNavPinnedState(
from: exiting.from,
to: exiting.to,
progress: _commitExitStart * (1.0 - _commitExit.value),
coverage: exiting.coverage,
settled: false,
// A committed swipe is a pop by definition.
popping: true,
topRoute: exiting.topRoute,
transition: exiting.transition,
);
}
final ordered = _orderedEntries;
if (ordered.isEmpty) return null;
final top = ordered.first;
// Nothing drawn above the `Navigator` can be underneath a route presented
// into it, so the shell stands down and the registrants take their chrome
// back for as long as one is up.
if (_isPresentedOver(top.key)) return null;
final below = ordered.length > 1 ? ordered[1] : null;
// Progress of the top route's own entrance: 1 at rest, 0 when it has just
// been pushed, and scrubbed by the interactive back-swipe during a pop.
// A completed route is at 1 whatever its controller last reported — a
// simulation stops inside a tolerance of its end, not on it.
final status = top.key.animation?.status ?? AnimationStatus.completed;
var progress = status == AnimationStatus.completed
? 1.0
: top.key.animation?.value ?? 1.0;
// A route's `animation` is a ProxyAnimation that reports itself complete
// at 1.0 until the navigator attaches the real controller in `didPush`,
// which lands *after* the route's first build — and that build is when its
// bar registers. Taken at face value it says a route that has not begun to
// move is fully entered, so the incoming chrome paints solid for one frame
// before dropping back to 0 and materializing: a visible flash.
//
// The route underneath is the witness. It is covered in lockstep with the
// top route's entrance, so at rest the two agree (1.0 and fully covered),
// and during a real transition they agree at every scrubbed value. Only
// the frame before the controller is attached has the top route claiming
// to be fully in while nothing below it has been covered at all.
if (below != null &&
progress == 1.0 &&
_coverageOf(below.key) == 0.0 &&
status == AnimationStatus.completed) {
progress = 0.0;
}
// Retreat only when something *unregistered* sits above the top route —
// a plain route or a modal sheet. `isCurrent` is what distinguishes that
// from a pop still unwinding this route's secondaryAnimation: anything
// registered above would have sorted above it instead, so a non-current
// top route is covered by something this shell doesn't manage.
final coverage = top.key.isCurrent ? 0.0 : _coverageOf(top.key);
// True for the whole of a swipe *and* the commit or rebound that follows
// it: Cupertino only calls `didStopUserGesture` from a status listener
// once that animation has finished.
final userGesture = top.key.navigator?.userGestureInProgress ?? false;
// A push or pop under no gesture plays on [_clock], started by
// [_onRouteStatus]. The run outlives the route if the route's own value
// got there first; the chrome is not settled until it has.
final running =
status == AnimationStatus.forward || status == AnimationStatus.reverse;
final clocked = identical(_clockRoute, top.key) &&
!userGesture &&
(running || _clockPending || _clock.isAnimating);
if (clocked) progress = _clockProgress();
// Whether the chrome may be tapped. Deliberately not derived from
// `progress`: a pop starts at 1.0 and a back-swipe sits there until the
// finger moves, so by value alone a transition that is very much running
// looks finished. The controller's status and the navigator's gesture flag
// are what actually tell the two apart, and `isCurrent` covers a route
// that something unregistered has been pushed over.
final settled = top.key.isCurrent &&
status == AnimationStatus.completed &&
!userGesture &&
!(clocked && (_clockPending || _clock.isAnimating));
// Whether the finger is still down with the pop not yet committed — which
// is narrower than [userGesture], and is what the chrome hold needs.
// Holding on the broader flag kept the chrome frozen for the whole commit
// animation and then snapped it at the end, with no transition played.
//
// The route is the witness: committing pops it immediately, so `isActive`
// goes false the moment the finger lifts on a commit, while a cancelled
// swipe leaves it active through the rebound — exactly when the chrome
// should still be holding.
final gesturing = userGesture && top.key.isActive;
// The frame the swipe commits: the finger has lifted and the route is
// already popped. Freeze what the chrome looked like and hand it to
// [_commitExit], which plays it out on its own clock.
//
// The held value has to be read before [_holdForGesture] runs, because
// releasing clears it — and the held value is exactly where the chrome
// must start from, not the live progress the drag happened to reach.
final committing = _gestureActive && !gesturing && !top.key.isActive;
final heldAtCommit = _gestureHeldProgress ?? progress;
// A pop plays the same forward choreography toward the other target, not
// the push in reverse — the host mirrors the clock and swaps the roles.
// The gesture flag matters as much as the status: a back-swipe holds the
// controller at whatever value the finger dictates without ever entering
// AnimationStatus.reverse.
final popping = !settled &&
(status == AnimationStatus.reverse ||
userGesture ||
(clocked && _clockStatus == AnimationStatus.reverse));
final state = GlassNavPinnedState(
from: below?.value ??
const GlassNavBarRegistration(
actions: <GlassBarItem>[],
showsBackButton: false,
),
to: top.value,
progress: committing
? heldAtCommit.clamp(0.0, 1.0)
: _holdForGesture(progress, gesturing).clamp(0.0, 1.0),
coverage: coverage.clamp(0.0, 1.0),
settled: settled,
popping: popping,
topRoute: top.key,
transition: widget.effectTransition,
);
if (committing) {
_gestureActive = false;
_gestureHeldProgress = null;
_commitExitStart = state.progress;
_commitExitSnapshot = state;
// Deferred: this runs inside [_tick]'s own ListenableBuilder, and
// starting a controller notifies its listeners synchronously — which
// would mark that builder dirty in the middle of its own build. The
// controller rests at 0 until then, so the frame in between holds the
// snapshot rather than showing a gap.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && _commitExitSnapshot != null) {
_commitExit.forward(from: 0.0);
}
});
}
return state;
}