diff --git a/patches/react-native-drawer-layout@4.2.3.patch b/patches/react-native-drawer-layout@4.2.3.patch index bb4b3381b1..62ff7b903d 100644 --- a/patches/react-native-drawer-layout@4.2.3.patch +++ b/patches/react-native-drawer-layout@4.2.3.patch @@ -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 => { diff --git a/patches/react-native-drawer-layout@4.2.3.patch.md b/patches/react-native-drawer-layout@4.2.3.patch.md new file mode 100644 index 0000000000..75876492f9 --- /dev/null +++ b/patches/react-native-drawer-layout@4.2.3.patch.md @@ -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. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5edf239650..2b0efaa100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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