releaseFromStateX function
HorizontalRelease
releaseFromStateX({
- required Rect currentRect,
- required Rect displayRect,
- required GestureBounds bounds,
- required double velocity,
- 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,
);
}