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 => {
@@ -0,0 +1,68 @@
# react-native-drawer-layout@4.2.3.patch
Backport of the current upstream `toggleDrawer` from
[`react-native-drawer-layout`](https://github.com/react-navigation/react-navigation/blob/main/packages/react-native-drawer-layout/src/views/Drawer.native.tsx).
None of it is released on the 4.x line (4.2.9 is latest and still lacks all of
it); upstream only ships it on the `5.0.0-alpha` line, which needs Gesture
Handler 3 and therefore react-native 0.82+. The patch can be dropped when we
move to drawer-layout 5.x.
The patch has three parts. The first landed in #8952; the other two were added
when the New Architecture switch (#10980) brought Reanimated 4 with it.
## 1. Deduplicate `toggleDrawer` calls (`animatingTo`)
`toggleDrawer` fires twice for a single swipe: once when the gesture ends, and
again from the effect that watches the `open` prop, because ending the gesture
calls `onOpen`/`onClose` which updates our shell state. The second call
restarts the spring from the current position with no velocity, which reads as
a stutter mid-animation.
`animatingTo` records which direction is in flight and the second call returns
early. Fixes the drawer jitter investigated in #8947 and #8949.
## 2. Ignore release velocity that opposes the spring direction
Reanimated 4 rewrote spring termination. With `overshootClamping: true` (which
this library always passes), the animation now ends as soon as the position
leaves the corridor between its start and its target:
```js
const leftBound = startValue >= 0 ? toValue : toValue + startValue
const rightBound = leftBound + Math.abs(startValue)
if (current < leftBound || current > rightBound) {
return true // terminating
}
```
Reanimated 3 only treated *crossing* the target as overshoot, so a spring that
first moved away from its target (which is what an initial velocity pointing
the other way produces) was left to swing back on its own.
The consequence on Reanimated 4 is that any release whose velocity opposes the
direction the drawer settles in ends the spring on its first frame, so the
drawer teleports instead of animating. Both cases are easy to hit: 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 further past its edge before releasing.
Zeroing the opposing velocity keeps the animation inside the corridor. Real
flings, whose velocity agrees with the target, are passed through untouched.
## 3. `useLayoutEffect` for the `open` prop
Upstream moved the effect that calls `toggleDrawer(open)` off `useEffect`, so
the follow-up call described in part 1 happens in the same commit rather than a
frame later. Part 1 dedupes it either way, but the dedupe window is only as
long as the spring runs, so not depending on that is worth the one-line change.
## Notes
`restDisplacementThreshold` and `restSpeedThreshold` are left in the spring
config, but they are inert on Reanimated 4 - it settles on a relative
`energyThreshold` instead, and those two options survive only as no-op layout
animation builder methods. The drawer animation is roughly 200ms shorter than
it was on Reanimated 3 as a result. The spring itself is deliberately left at
this library's `stiffness: 1000, damping: 500, mass: 3`; upstream has since
retuned it to an underdamped `500/40/1`, which would change how the drawer
feels and is a separate decision.
+3 -3
View File
@@ -227,7 +227,7 @@ patchedDependencies:
expo-updates@29.0.17: 04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487
react-native-compressor@1.13.0: 58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925
react-native-date-picker@5.0.13: 92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd
react-native-drawer-layout@4.2.3: 74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130
react-native-drawer-layout@4.2.3: 73a2391c30d0178794590bd13139f10983c465d3a3e648004d494cd91c5f89c6
react-native-gesture-handler: 39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b
react-native-keyboard-controller@1.21.8: ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271
react-native-pager-view@6.8.0: c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc
@@ -630,7 +630,7 @@ importers:
version: 0.1.6(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=0a4b515f22d157816048e3181dd18fb83b1679a90fca50f715c55483aabdced4)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=0a4b515f22d157816048e3181dd18fb83b1679a90fca50f715c55483aabdced4)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=0a4b515f22d157816048e3181dd18fb83b1679a90fca50f715c55483aabdced4)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-drawer-layout:
specifier: ^4.2.3
version: 4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(e24fb32bf68bffe2eac7bf9484330e37)
version: 4.2.3(patch_hash=73a2391c30d0178794590bd13139f10983c465d3a3e648004d494cd91c5f89c6)(e24fb32bf68bffe2eac7bf9484330e37)
react-native-edge-to-edge:
specifier: ^1.8.1
version: 1.8.1(react-native@0.81.5(patch_hash=0a4b515f22d157816048e3181dd18fb83b1679a90fca50f715c55483aabdced4)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -17689,7 +17689,7 @@ snapshots:
'@babel/runtime': 7.29.2
dotenv: 16.6.1
react-native-drawer-layout@4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(e24fb32bf68bffe2eac7bf9484330e37):
react-native-drawer-layout@4.2.3(patch_hash=73a2391c30d0178794590bd13139f10983c465d3a3e648004d494cd91c5f89c6)(e24fb32bf68bffe2eac7bf9484330e37):
dependencies:
color: 4.2.3
react: 19.1.0