fix(app): avoid draggable-list gesture livelock

Manual activation now runs once while the recognizer is waiting; reactivating or failing an already-active recognizer could leave the gesture pipeline stuck.
This commit is contained in:
Mohamed Boudra
2026-08-17 21:15:17 +02:00
parent 851dcb8d4a
commit f55f6d0599
@@ -58,7 +58,7 @@ index 2abb4a2..8ca967f 100644
onAnimValInit?: (animVals: ReturnType<typeof useAnimatedValues>) => void; onAnimValInit?: (animVals: ReturnType<typeof useAnimatedValues>) => void;
itemEnteringAnimation?: AnimateProps<Animated.View>["entering"]; itemEnteringAnimation?: AnimateProps<Animated.View>["entering"];
diff --git a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx diff --git a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
index 7c88afc..b2f103e 100644 index 7c88afc..6d5719c 100644
--- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx --- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
+++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx +++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
@@ -19,6 +19,7 @@ import { @@ -19,6 +19,7 @@ import {
@@ -237,17 +237,19 @@ index 7c88afc..b2f103e 100644
useAnimatedReaction( useAnimatedReaction(
() => { () => {
return spacerIndexAnim.value; return spacerIndexAnim.value;
@@ -271,11 +349,19 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) { @@ -271,11 +349,21 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
); );
const gestureDisabled = useSharedValue(false); const gestureDisabled = useSharedValue(false);
+ const touchStartX = useSharedValue(0); + const touchStartX = useSharedValue(0);
+ const touchStartY = useSharedValue(0); + const touchStartY = useSharedValue(0);
+ const manualPanActivated = useSharedValue(false);
+ const coordinatesDragWithExternalGesture = Boolean(props.simultaneousHandlers); + const coordinatesDragWithExternalGesture = Boolean(props.simultaneousHandlers);
- const panGesture = Gesture.Pan() - const panGesture = Gesture.Pan()
+ let panGesture = Gesture.Pan() + let panGesture = Gesture.Pan()
.onBegin((evt) => { .onBegin((evt) => {
+ manualPanActivated.value = false;
gestureDisabled.value = disabled.value; gestureDisabled.value = disabled.value;
if (gestureDisabled.value) return; if (gestureDisabled.value) return;
+ const sessionId = gestureSessionId.value + 1; + const sessionId = gestureSessionId.value + 1;
@@ -258,7 +260,7 @@ index 7c88afc..b2f103e 100644
panGestureState.value = evt.state; panGestureState.value = evt.state;
}) })
.onUpdate((evt) => { .onUpdate((evt) => {
@@ -286,8 +372,15 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) { @@ -286,8 +374,15 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
: evt.translationY; : evt.translationY;
touchTranslate.value = translation; touchTranslate.value = translation;
}) })
@@ -275,7 +277,7 @@ index 7c88afc..b2f103e 100644
// Set touch val to current translate val // Set touch val to current translate val
isTouchActiveNative.value = false; isTouchActiveNative.value = false;
const translation = horizontalAnim.value const translation = horizontalAnim.value
@@ -299,6 +392,7 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) { @@ -299,6 +394,7 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
// Only call onDragEnd if actually dragging a cell // Only call onDragEnd if actually dragging a cell
if (activeIndexAnim.value === -1 || disabled.value) return; if (activeIndexAnim.value === -1 || disabled.value) return;
@@ -283,7 +285,7 @@ index 7c88afc..b2f103e 100644
disabled.value = true; disabled.value = true;
runOnJS(onRelease)(activeIndexAnim.value); runOnJS(onRelease)(activeIndexAnim.value);
const springTo = placeholderOffset.value - activeCellOffset.value; const springTo = placeholderOffset.value - activeCellOffset.value;
@@ -309,14 +403,58 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) { @@ -309,14 +405,64 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
runOnJS(onDragEnd)({ runOnJS(onDragEnd)({
from: activeIndexAnim.value, from: activeIndexAnim.value,
to: spacerIndexAnim.value, to: spacerIndexAnim.value,
@@ -295,6 +297,7 @@ index 7c88afc..b2f103e 100644
}) })
- .onTouchesDown(() => { - .onTouchesDown(() => {
+ .onFinalize((evt) => { + .onFinalize((evt) => {
+ manualPanActivated.value = false;
+ panGestureState.value = evt.state; + panGestureState.value = evt.state;
+ runOnJS(onContainerTouchEnd)(); + runOnJS(onContainerTouchEnd)();
+ if (gestureDisabled.value) return; + if (gestureDisabled.value) return;
@@ -316,6 +319,10 @@ index 7c88afc..b2f103e 100644
+ .onTouchesMove((event, stateManager) => { + .onTouchesMove((event, stateManager) => {
+ if (!coordinatesDragWithExternalGesture) return; + if (!coordinatesDragWithExternalGesture) return;
+ +
+ // Manual activation arbitrates only while the pan is waiting. Failing or
+ // reactivating an active recognizer can leave RNGH/Reanimated livelocked.
+ if (manualPanActivated.value) return;
+
+ const touch = event.changedTouches[0]; + const touch = event.changedTouches[0];
+ if (!touch || event.numberOfTouches !== 1) { + if (!touch || event.numberOfTouches !== 1) {
+ stateManager.fail(); + stateManager.fail();
@@ -337,13 +344,14 @@ index 7c88afc..b2f103e 100644
+ if (absDeltaX > 10 && absDeltaX > absDeltaY) { + if (absDeltaX > 10 && absDeltaX > absDeltaY) {
+ stateManager.fail(); + stateManager.fail();
+ } else if (absDeltaY >= activationDistanceProp && absDeltaY > absDeltaX) { + } else if (absDeltaY >= activationDistanceProp && absDeltaY > absDeltaX) {
+ manualPanActivated.value = true;
+ stateManager.activate(); + stateManager.activate();
+ } + }
+ }) + })
.onTouchesUp(() => { .onTouchesUp(() => {
// Turning this into a worklet causes timing issues. We want it to run // Turning this into a worklet causes timing issues. We want it to run
// just after the finger lifts. // just after the finger lifts.
@@ -324,7 +462,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) { @@ -324,7 +470,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
}); });
if (dragHitSlop) panGesture.hitSlop(dragHitSlop); if (dragHitSlop) panGesture.hitSlop(dragHitSlop);
@@ -354,7 +362,7 @@ index 7c88afc..b2f103e 100644
const activeOffset = [-activationDistanceProp, activationDistanceProp]; const activeOffset = [-activationDistanceProp, activationDistanceProp];
if (props.horizontal) { if (props.horizontal) {
panGesture.activeOffsetX(activeOffset); panGesture.activeOffsetX(activeOffset);
@@ -332,6 +472,21 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) { @@ -332,6 +480,21 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
panGesture.activeOffsetY(activeOffset); panGesture.activeOffsetY(activeOffset);
} }
} }
@@ -376,7 +384,7 @@ index 7c88afc..b2f103e 100644
const onScroll = useStableCallback((scrollOffset: number) => { const onScroll = useStableCallback((scrollOffset: number) => {
props.onScrollOffsetChange?.(scrollOffset); props.onScrollOffsetChange?.(scrollOffset);
@@ -373,8 +528,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) { @@ -373,8 +536,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
horizontal={!!props.horizontal} horizontal={!!props.horizontal}
layoutAnimationDisabled={layoutAnimationDisabled} layoutAnimationDisabled={layoutAnimationDisabled}
> >