resolveState method

GlassNavPinnedState? resolveState()

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;
}