Skip to content

feat: add releaseProjection and settleDuration - #83

Open
NicholasBoccuzzi wants to merge 1 commit into
software-mansion-labs:mainfrom
NicholasBoccuzzi:feat/release-projection-and-settle-duration
Open

feat: add releaseProjection and settleDuration#83
NicholasBoccuzzi wants to merge 1 commit into
software-mansion-labs:mainfrom
NicholasBoccuzzi:feat/release-projection-and-settle-duration

Conversation

@NicholasBoccuzzi

Copy link
Copy Markdown

Follows up on a Slack thread with @patrick — adds the two props we discussed for tuning how a drag release resolves and how the sheet travels to the detent it resolved to. The proposed commit-threshold prop is deliberately not here.

Both props default to current behavior, so no existing sheet changes until one is set.

releaseProjection

Seconds of release velocity projected onto the sheet's position before the nearest-detent search in bestSnapIndex / snapshotBestSnapIndex.

A release is otherwise resolved on where the gesture stopped, so a short but deliberate flick toward a detent can't reach it unless it also crossed the midpoint or exceeded the flick threshold. The closer two detents sit, the more that shows — a small, confident gesture reads as a non-event. Projecting the release forward lets its momentum count toward the detent it was aimed at.

<BottomSheet detents={[0, '50%', '90%']} releaseProjection={0.15} ... />

Two things kept deliberately narrow:

  • The flick branches still use the raw position. They mean "one detent along from where the sheet actually is," and projecting there could skip past one on a 3+ detent sheet. A fast release therefore behaves exactly as before, at any projection value.
  • It only moves the boundary between detents. No second rule is layered on top of the nearest-detent result: a release still resolves to one of the declared detents, and programmatic detents remain excluded from drag snapping.

settleDuration

How long the spring that carries the sheet to the resolved detent takes.

This also surfaced a pre-existing inconsistency worth flagging on its own: iOS hardcoded duration = 0.45, while Android used SpringForce.STIFFNESS_MEDIUM (1500), which is ω ≈ 38.7 and so settles in roughly 0.2s — the two platforms have not been settling alike.

Both springs are critically damped (CriticalSpring is ζ = 1 by construction; Android uses DAMPING_RATIO_NO_BOUNCY), and such a spring is settled within ~0.5% at ω·t ≈ 8 while SpringForce's stiffness is ω². So a duration maps exactly onto Android's stiffness as (8 / duration)² — the same relation iOS already uses for omega = 8.0 / duration, not an approximation.

I left both platform defaults in place so this PR doesn't change Android's existing feel, but setting settleDuration now gives both platforms the same settle, which the docs note.

Release velocity is still carried into the spring at any duration.

Notes

  • Validated as finite and non-negative in validateReleaseTuning, following validateIndex's style. A negative projection would resolve a release against the detent it was moving away from; a negative duration has no spring solution.
  • ModalBottomSheetProps extends BottomSheetProps and ModalBottomSheet spreads props, so both work there without extra plumbing.
  • New docs page release-and-settle.mdx, registered in the sidebar after detents-and-index.
  • tsc --noEmit and eslint src are clean. I have not run the Android unit/instrumented suites or built the example app — worth a CI pass, particularly on the @ReactProp overrides, which assume codegen emits Double for WithDefault<Double, 0>.

Where this came from

We're shipping a persistent map sheet whose top two detents sit insets.top + toolbarHeight apart (~120pt) while the next one down is most of the screen away (~700pt). Users reported that expanding took too much gesture. We trialled both changes as a local patch and measured on device: a slow ~34pt drag now resolves to the adjacent detent in either direction, where stock needed ~61pt up and ~365pt down.

Happy to adjust naming, defaults, or split this into two PRs if you'd prefer them landed separately.

Two props for tuning how a drag release resolves and how the sheet travels
to the detent it resolved to. Both default to current behavior.

`releaseProjection` is seconds of release velocity projected onto the
sheet's position before the nearest-detent search. A release is otherwise
resolved on where the gesture stopped, so a short but deliberate flick
cannot reach the next detent unless it also crossed the midpoint to it or
exceeded the flick threshold. Projecting the release forward lets that
momentum count. The flick branches deliberately keep using the raw
position: they mean "one detent along from where the sheet actually is",
and projecting there could skip past one.

`settleDuration` is how long the settle spring takes. iOS had this
hardcoded at 0.45s; Android used SpringForce.STIFFNESS_MEDIUM, which is
closer to 0.2s, so the two platforms did not settle alike. Since both
springs are critically damped, a duration maps to Android's stiffness as
(8 / duration)^2 — omega*t ~= 8 is the settled point, and SpringForce's
stiffness is omega^2. Left unset each platform keeps its own default, so
existing sheets are unchanged.

Both are validated as finite and non-negative, matching validateIndex.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant