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:
@@ -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.
|
||||
Generated
+3
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user