Fix drawer animation snapping on Reanimated 4

Reanimated 4 (New Architecture, #10980) rewrote spring termination: with
`overshootClamping`, which drawer-layout always passes, an animation now ends
as soon as the position leaves the corridor between its start and its target.
Reanimated 3 only treated crossing the target as overshoot. So on Reanimated 4
any release whose velocity opposes the direction the drawer settles in ends the
spring on its first frame, and the drawer teleports instead of animating. Two
easy ways in: an aborted drag under `swipeMinDistance`/`swipeMinVelocity`,
where `nextOpen` keeps its current value while the finger was moving the other
way, and dragging the already-open drawer past its edge before releasing.

Refresh the patch against upstream `main`, which fixes this by zeroing velocity
that opposes the spring direction, and take the two other changes to the same
function while we are here: an early return when the drawer is already at the
target, and `useLayoutEffect` for the `open` prop so the follow-up
`toggleDrawer` call does not depend on the spring still running to be deduped.

Regenerated with `pnpm patch` / `pnpm patch-commit`. Also adds the patch doc
this one was missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011DRYvgEawpfehopEibJXQB
This commit is contained in:
Claude
2026-07-25 08:14:27 +00:00
parent afd267bd2b
commit 810d167426
3 changed files with 99 additions and 8 deletions
+28 -5
View File
@@ -1,8 +1,8 @@
diff --git a/lib/module/views/Drawer.native.js b/lib/module/views/Drawer.native.js
index 38470a658878a63919186257041098747ec55473..b9d25798c9be229efeb676166f89bfbc72615793 100644
index 38470a658878a63919186257041098747ec55473..1836d661ca0f8fa3e886da06e72eb004cd8f585e 100644
--- a/lib/module/views/Drawer.native.js
+++ b/lib/module/views/Drawer.native.js
@@ -124,15 +124,21 @@ export function Drawer({
@@ -124,17 +124,33 @@ export function Drawer({
}
onTransitionEnd?.(!open);
});
@@ -10,21 +10,34 @@ index 38470a658878a63919186257041098747ec55473..b9d25798c9be229efeb676166f89bfbc
const toggleDrawer = React.useCallback((open, velocity) => {
'worklet';
const translateX = getDrawerTranslationX(open);
+
+ if (translationX.value === translateX) {
+ return;
+ }
+
+ if (animatingTo.value === (open ? 'open' : 'close')) {
+ return;
+ }
+
const translateX = getDrawerTranslationX(open);
if (velocity === undefined) {
runOnJS(onAnimationStart)(open);
}
touchStartX.value = 0;
touchX.value = 0;
+
+ // Ignore velocity that opposes the spring direction.
+ // Otherwise overshootClamping makes the spring snap instantly.
+ const isSameDirection = velocity !== undefined && (translateX > translationX.value && velocity > 0 || translateX < translationX.value && velocity < 0);
+ const effectiveVelocity = isSameDirection ? velocity : 0;
+ animatingTo.value = open ? 'open' : 'close';
translationX.value = withSpring(translateX, {
velocity,
- velocity,
+ velocity: effectiveVelocity,
stiffness: 1000,
@@ -142,7 +148,10 @@ export function Drawer({
damping: 500,
mass: 3,
@@ -142,14 +158,19 @@ export function Drawer({
restDisplacementThreshold: 0.01,
restSpeedThreshold: 0.01,
reduceMotion: ReduceMotion.Never
@@ -36,3 +49,13 @@ index 38470a658878a63919186257041098747ec55473..b9d25798c9be229efeb676166f89bfbc
if (open) {
runOnJS(onOpen)();
} else {
runOnJS(onClose)();
}
}, [getDrawerTranslationX, onAnimationEnd, onAnimationStart, onClose, onOpen, touchStartX, touchX, translationX]);
- React.useEffect(() => toggleDrawer(open), [open, toggleDrawer]);
+ React.useLayoutEffect(() => {
+ toggleDrawer(open);
+ }, [open, toggleDrawer]);
const startX = useSharedValue(0);
const pan = React.useMemo(() => {
let panGesture = Gesture?.Pan().onBegin(event => {