dismissedRect static method
The frame a sheet occupies mid-swipe, in global coordinates — the morph's destination when a swipe is what closed the sheet.
restingRect is carried down by the damped fall and shrunk about the
pivot.
scaleAnchor supplies that pivot: the grabbed point, expressed in the
resting card's frame (the live finger position minus the raw fall).
Carried down with the fall, it keeps the grabbed content exactly under
the finger below the damping knee — the direct-manipulation feel of the
native gesture. Omit it and the sheet shrinks about its own centre.
horizontalOffset only translates, never feeding the scale — measured
off iOS 26, where a 143 pt sideways sweep changed the card's scale by
just 0.029, all attributable to incidental vertical drift.
The card's bottom edge cannot lift into view under this pivot while dismissScaleGain stays below 1.0 per card height, because the fall always outruns the shrink; the closing clamp guards that invariant against a future retune.
Implementation
static Rect dismissedRect({
required Rect restingRect,
required double travel,
required double screenHeight,
double horizontalOffset = 0.0,
Offset? scaleAnchor,
}) {
final cardFraction =
screenHeight <= 0.0 ? 0.0 : restingRect.height / screenHeight;
final fall = Offset(
0.0,
dampedDismissTravel(travel, cardFraction: cardFraction) * screenHeight,
);
final fallen = restingRect.shift(fall);
final scale = dismissScale(travel, cardFraction: cardFraction);
final pivot = scaleAnchor == null
? fallen.center
: scaleAnchor.translate(0.0, fall.dy);
final scaled = Rect.fromLTRB(
pivot.dx + (fallen.left - pivot.dx) * scale,
pivot.dy + (fallen.top - pivot.dy) * scale,
pivot.dx + (fallen.right - pivot.dx) * scale,
pivot.dy + (fallen.bottom - pivot.dy) * scale,
);
// Invariant guard — see the doc comment. Never active at the shipped gain.
final lift = restingRect.bottom - scaled.bottom;
final grounded = lift > 0.0 ? scaled.shift(Offset(0.0, lift)) : scaled;
return grounded.shift(Offset(horizontalOffset, 0.0));
}