releaseFromStateX function

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

Computes the HorizontalRelease plan for the X axis given gesture-end state.

projectedRect is the rect at its post-scale-settle dimensions — used to compute the viewport-fit center for the rect when it eventually rests. Re-evaluated against the gesture-end position (idle case) and the decay-end position (velocity case) so the rubber target tracks where the rect actually lands. When omitted, defaults to currentRect's dims.

Implementation

HorizontalRelease releaseFromStateX({
  required Rect currentRect,
  required Rect displayRect,
  required GestureBounds bounds,
  required double velocity,
  Rect? projectedRect,
}) {
  final halfWidth = currentRect.width / 2;
  final pos = currentRect.center.dx;
  final dispLeft = displayRect.left;
  final dispRight = displayRect.right;
  final dispCenter = displayRect.center.dx;
  // Past zones — size-aware to mirror friction's [directionStateForX].
  // Large rect (covers display): past zone starts when the *near edge*
  // crosses display, i.e., we begin uncovering on that side.
  // Small rect (fits within display): past zone starts when the *far edge*
  // exits display, i.e., the rect itself starts going outside.
  final double pastLeftBound;
  final double pastRightBound;
  if (currentRect.width > displayRect.width) {
    pastLeftBound = dispRight - halfWidth;
    pastRightBound = dispLeft + halfWidth;
  } else {
    pastLeftBound = dispLeft + halfWidth;
    pastRightBound = dispRight - halfWidth;
  }

  final leftDc = bounds[.left]?.decay ?? _defaultDecay;
  final rightDc = bounds[.right]?.decay ?? _defaultDecay;

  final fitWidth = projectedRect?.width ?? currentRect.width;
  final fitHeight = projectedRect?.height ?? currentRect.height;
  // Scale-aware center adjustment: as the rect shrinks from
  // currentRect.width to fitWidth (during scale settle), the point currently
  // at displayRect.center should stay at displayRect.center. That means the
  // rect's new center is the proportional position between displayRect.center
  // and the current center, with the width-ratio as the proportion. Then
  // the cover-clamp (`getLimitedCenterXInside`) is applied as a safety so
  // the rect never exposes a display edge.
  final widthRatio = currentRect.width == 0 ? 1.0 : fitWidth / currentRect.width;
  // Same scale-aware adjustment used by both decay phases and the settle
  // target — keeps the display-center point stable as the rect shrinks.
  double scaleAwareCenter(double cx) =>
      displayRect.center.dx + (cx - displayRect.center.dx) * widthRatio;
  double fitAt(double cx) {
    return Rect.fromCenter(
      center: Offset(scaleAwareCenter(cx), currentRect.center.dy),
      width: fitWidth,
      height: fitHeight,
    ).getLimitedCenterXInside(displayRect);
  }
  final fit = fitAt(pos);

  HorizontalZone zoneOf(double p) {
    if (p < pastLeftBound) return .pastLeft;
    if (p > pastRightBound) return .pastRight;
    if (p <= dispCenter) return .left;
    return .right;
  }

  final startZone = zoneOf(pos);

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

  Decay? decayAt(HorizontalZone zone, bool ltr) {
    final (isLeft, isPast) = switch (zone) {
      .pastLeft => (true, true),
      .left => (true, false),
      .right => (false, false),
      .pastRight => (false, true),
    };
    final dc = isLeft ? leftDc : rightDc;
    final extending = (isLeft && !ltr) || (!isLeft && ltr);
    if (isPast) {
      return extending ? dc.extendingPastDisplay : dc.retractingPastDisplay;
    }
    return extending ? dc.extending : dc.retracting;
  }

  double? exitBoundaryAt(HorizontalZone zone, bool ltr) {
    if (ltr) {
      switch (zone) {
        case .pastLeft: return pastLeftBound;
        case .left: return dispCenter;
        case .right: return pastRightBound;
        case .pastRight: return null;
      }
    } else {
      switch (zone) {
        case .pastRight: return pastRightBound;
        case .right: return dispCenter;
        case .left: return pastLeftBound;
        case .pastLeft: 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 ltr = v > 0;
    final zone = zoneOf(p);
    final pick = decayAt(zone, ltr);
    if (pick == null) break;

    final step = _runPhase(
      pos: p,
      vel: v,
      decay: pick,
      exitBoundary: exitBoundaryAt(zone, ltr),
    );
    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 HorizontalDir direction = velocity > 0 ? .ltr : .rtl;

  // Recompute fit at the decay-end position — for zoomed rects, the
  // viewport-correct center depends on where the rect actually lands.
  final endFit = fitAt(endPos);
  AxisFling? settle;
  if (endZone case .pastLeft) {
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: endFit, settle: leftDc.settle);
  } else if (endZone case .pastRight) {
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: endFit, settle: rightDc.settle);
  } else if ((endPos - endFit).abs() >= 0.5) {
    final settleConfig = (endPos > endFit ? rightDc : leftDc).settle;
    settle = _rubberFling(startPos: endPos, startVel: v, targetPos: endFit, settle: settleConfig);
  }

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