releaseFromStateY function

VerticalRelease releaseFromStateY({
  1. required Rect currentRect,
  2. required Rect displayRect,
  3. required GestureBounds bounds,
  4. required double velocity,
  5. Rect? projectedRect,
})

Computes the VerticalRelease plan for the Y axis given gesture-end state.

projectedRect is the rect at its post-scale-settle dimensions — used to compute the viewport-fit center. Re-evaluated at gesture-end and decay-end so the rubber target tracks where the rect actually lands. See releaseFromStateX for full semantics.

Implementation

VerticalRelease releaseFromStateY({
  required Rect currentRect,
  required Rect displayRect,
  required GestureBounds bounds,
  required double velocity,
  Rect? projectedRect,
}) {
  final halfHeight = currentRect.height / 2;
  final pos = currentRect.center.dy;
  final dispTop = displayRect.top;
  final dispBottom = displayRect.bottom;
  final dispCenter = displayRect.center.dy;
  // Size-aware past bounds — see [releaseFromStateX] for the rationale.
  final double pastTopBound;
  final double pastBottomBound;
  if (currentRect.height > displayRect.height) {
    pastTopBound = dispBottom - halfHeight;
    pastBottomBound = dispTop + halfHeight;
  } else {
    pastTopBound = dispTop + halfHeight;
    pastBottomBound = dispBottom - halfHeight;
  }

  final topDc = bounds[.top]?.decay ?? _defaultDecay;
  final bottomDc = bounds[.bottom]?.decay ?? _defaultDecay;

  final fitWidth = projectedRect?.width ?? currentRect.width;
  final fitHeight = projectedRect?.height ?? currentRect.height;
  // Scale-aware center adjustment — see [releaseFromStateX] for the
  // rationale. Keeps the display-center point under display-center after
  // the scale settles, with cover-clamp applied as a safety.
  final heightRatio = currentRect.height == 0 ? 1.0 : fitHeight / currentRect.height;
  double scaleAwareCenter(double cy) =>
      displayRect.center.dy + (cy - displayRect.center.dy) * heightRatio;
  double fitAt(double cy) {
    return Rect.fromCenter(
      center: Offset(currentRect.center.dx, scaleAwareCenter(cy)),
      width: fitWidth,
      height: fitHeight,
    ).getLimitedCenterYInside(displayRect);
  }
  final fit = fitAt(pos);

  VerticalZone zoneOf(double p) {
    if (p < pastTopBound) return .pastTop;
    if (p > pastBottomBound) return .pastBottom;
    if (p <= dispCenter) return .top;
    return .bottom;
  }

  final startZone = zoneOf(pos);

  if (velocity.abs() <= _velocityFloor) {
    if ((pos - fit).abs() < 0.5) {
      return VerticalRelease(
        direction: .idle,
        startZone: startZone,
        endZone: startZone,
      );
    }
    final settleConfig = (pos > fit ? bottomDc : topDc).settle;
    return VerticalRelease(
      direction: .idle,
      startZone: startZone,
      endZone: startZone,
      settle: _rubberFling(startPos: pos, startVel: velocity, targetPos: fit, settle: settleConfig),
    );
  }

  Decay? decayAt(VerticalZone zone, bool ttb) {
    final (isTop, isPast) = switch (zone) {
      .pastTop => (true, true),
      .top => (true, false),
      .bottom => (false, false),
      .pastBottom => (false, true),
    };
    final dc = isTop ? topDc : bottomDc;
    final extending = (isTop && !ttb) || (!isTop && ttb);
    if (isPast) {
      return extending ? dc.extendingPastDisplay : dc.retractingPastDisplay;
    }
    return extending ? dc.extending : dc.retracting;
  }

  double? exitBoundaryAt(VerticalZone zone, bool ttb) {
    if (ttb) {
      switch (zone) {
        case .pastTop: return pastTopBound;
        case .top: return dispCenter;
        case .bottom: return pastBottomBound;
        case .pastBottom: return null;
      }
    } else {
      switch (zone) {
        case .pastBottom: return pastBottomBound;
        case .bottom: return dispCenter;
        case .top: return pastTopBound;
        case .pastTop: return null;
      }
    }
  }

  final phases = <AxisFling>[];
  var p = pos;
  var v = velocity;
  var endPos = pos;
  for (var i = 0; i < _maxPhases; i++) {
    if (v.abs() <= _velocityFloor) break;
    final ttb = v > 0;
    final zone = zoneOf(p);
    final pick = decayAt(zone, ttb);
    if (pick == null) break;

    final step = _runPhase(
      pos: p,
      vel: v,
      decay: pick,
      exitBoundary: exitBoundaryAt(zone, ttb),
    );
    phases.add(step.fling);
    endPos = step.endPos;
    v = step.endVel;
    if (step.stopped) break;
    p = step.endPos + v.sign * 0.01;
  }

  final endZone = zoneOf(endPos);
  final VerticalDir direction = velocity > 0 ? .ttb : .btt;

  final endFit = fitAt(endPos);
  AxisFling? settle;
  if (endZone case .pastTop) {
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: endFit, settle: topDc.settle);
  } else if (endZone case .pastBottom) {
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: endFit, settle: bottomDc.settle);
  } else if ((endPos - endFit).abs() >= 0.5) {
    final settleConfig = (endPos > endFit ? bottomDc : topDc).settle;
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: endFit, settle: settleConfig);
  }

  return VerticalRelease(
    direction: direction,
    startZone: startZone,
    endZone: endZone,
    decay: phases,
    settle: settle,
  );
}