Fix mobile sidebar swipe and drag failures (#2709)
* fix(app): prevent text selection during Android swipes Gesture interception did not cancel the native view where the touch began, leaving Android's pending text-selection action alive. * fix(app): restore iOS sidebar close swipes Fabric could fail to attach close handlers when their target lived inside retained but display-hidden panel content. Keep a registered native gesture host mounted outside the presentation wrapper so close swipes work over nested lists without activating rows. * fix(app): recover sidebar drags after cancellation Cancelled native drags could leave a lifted row and the shared outer list locked. Scope terminal cleanup to the owning gesture session and release the lock on cancellation or unmount. * test(app): clean up partial drag fixtures
This commit is contained in:
@@ -1,8 +1,360 @@
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||
index d3a4f1f..083bf19 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||
@@ -16,12 +16,14 @@ declare const _default: <T>(props: Omit<FlatListProps<T>, "keyExtractor" | "data
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: ((index: number) => void) | undefined;
|
||||
onDragEnd?: ((params: import("../types").DragEndParams<T>) => void) | undefined;
|
||||
+ onDragTerminate?: (() => void) | undefined;
|
||||
onPlaceholderIndexChange?: ((placeholderIndex: number) => void) | undefined;
|
||||
onRelease?: ((index: number) => void) | undefined;
|
||||
onScrollOffsetChange?: ((scrollOffset: number) => void) | undefined;
|
||||
renderItem: import("../types").RenderItem<T>;
|
||||
renderPlaceholder?: import("../types").RenderPlaceholder<T> | undefined;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[] | undefined;
|
||||
+ dragGestureHostPresented?: boolean | undefined;
|
||||
outerScrollOffset?: Animated.SharedValue<number> | undefined;
|
||||
onAnimValInit?: ((animVals: {
|
||||
activeCellOffset: import("react-native-reanimated").SharedValue<number>;
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||
index d3c6cbb..36e9645 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||
@@ -15,12 +15,14 @@ export declare const NestableDraggableFlatList: <T>(props: Omit<import("react-na
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: ((index: number) => void) | undefined;
|
||||
onDragEnd?: ((params: import("../types").DragEndParams<T>) => void) | undefined;
|
||||
+ onDragTerminate?: (() => void) | undefined;
|
||||
onPlaceholderIndexChange?: ((placeholderIndex: number) => void) | undefined;
|
||||
onRelease?: ((index: number) => void) | undefined;
|
||||
onScrollOffsetChange?: ((scrollOffset: number) => void) | undefined;
|
||||
renderItem: import("../types").RenderItem<T>;
|
||||
renderPlaceholder?: import("../types").RenderPlaceholder<T> | undefined;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[] | undefined;
|
||||
+ dragGestureHostPresented?: boolean | undefined;
|
||||
outerScrollOffset?: Animated.SharedValue<number> | undefined;
|
||||
onAnimValInit?: ((animVals: {
|
||||
activeCellOffset: import("react-native-reanimated").SharedValue<number>;
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||
index 2abb4a2..8ca967f 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||
@@ -23,12 +23,16 @@ export declare type DraggableFlatListProps<T> = Modify<FlatListProps<T>, {
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: (index: number) => void;
|
||||
onDragEnd?: (params: DragEndParams<T>) => void;
|
||||
+ /** @internal Used by the nestable wrapper to release its outer-scroll lock on cancellation. */
|
||||
+ onDragTerminate?: () => void;
|
||||
onPlaceholderIndexChange?: (placeholderIndex: number) => void;
|
||||
onRelease?: (index: number) => void;
|
||||
onScrollOffsetChange?: (scrollOffset: number) => void;
|
||||
renderItem: RenderItem<T>;
|
||||
renderPlaceholder?: RenderPlaceholder<T>;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||
+ /** @internal Forces handler reattachment when a retained native host is presented. */
|
||||
+ dragGestureHostPresented?: boolean;
|
||||
outerScrollOffset?: Animated.SharedValue<number>;
|
||||
onAnimValInit?: (animVals: ReturnType<typeof useAnimatedValues>) => void;
|
||||
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
|
||||
index 7c88afc..b630940 100644
|
||||
index 7c88afc..b2f103e 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
||||
+++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
||||
@@ -332,6 +332,13 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
+ runOnUI,
|
||||
useAnimatedReaction,
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue,
|
||||
@@ -92,6 +93,17 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
activeCellOffset.value = -1;
|
||||
setActiveKey(null);
|
||||
});
|
||||
+ const terminalHandled = useSharedValue(false);
|
||||
+ const gestureSessionId = useSharedValue(0);
|
||||
+ const gestureSessionActive = useSharedValue(false);
|
||||
+ const currentGestureSessionRef = useRef<{
|
||||
+ id: number;
|
||||
+ active: boolean;
|
||||
+ } | null>(null);
|
||||
+ const activeDragRef = useRef<{
|
||||
+ sessionId: number;
|
||||
+ index: number;
|
||||
+ } | null>(null);
|
||||
|
||||
const {
|
||||
dragHitSlop = DEFAULT_PROPS.dragHitSlop,
|
||||
@@ -144,19 +156,88 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
});
|
||||
}, [props.data, keyExtractor, keyToIndexRef]);
|
||||
|
||||
+ const onDragCancel = useStableCallback((sessionId: number, index: number) => {
|
||||
+ if (activeDragRef.current?.sessionId !== sessionId) return;
|
||||
+ activeDragRef.current = null;
|
||||
+ reset();
|
||||
+ props.onRelease?.(index);
|
||||
+ props.onDragTerminate?.();
|
||||
+ });
|
||||
+
|
||||
+ useEffect(() => {
|
||||
+ return () => {
|
||||
+ const activeDrag = activeDragRef.current;
|
||||
+ if (!activeDrag) return;
|
||||
+ activeDragRef.current = null;
|
||||
+ propsRef.current.onRelease?.(activeDrag.index);
|
||||
+ propsRef.current.onDragTerminate?.();
|
||||
+ };
|
||||
+ }, [propsRef]);
|
||||
+
|
||||
+ const onGestureBegin = useStableCallback((sessionId: number) => {
|
||||
+ currentGestureSessionRef.current = { id: sessionId, active: true };
|
||||
+ });
|
||||
+
|
||||
+ const onGestureFinalize = useStableCallback(
|
||||
+ (sessionId: number, committed: boolean) => {
|
||||
+ if (currentGestureSessionRef.current?.id === sessionId) {
|
||||
+ currentGestureSessionRef.current.active = false;
|
||||
+ }
|
||||
+ if (!committed && activeDragRef.current?.sessionId === sessionId) {
|
||||
+ onDragCancel(sessionId, activeDragRef.current.index);
|
||||
+ }
|
||||
+ }
|
||||
+ );
|
||||
+
|
||||
+ const onDragActivationRejected = useStableCallback((sessionId: number) => {
|
||||
+ if (activeDragRef.current?.sessionId === sessionId) {
|
||||
+ onDragCancel(sessionId, activeDragRef.current.index);
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ const activateDragOnUI = (
|
||||
+ sessionId: number,
|
||||
+ index: number,
|
||||
+ cellOffset: number,
|
||||
+ cellSize: number,
|
||||
+ hasCellMeasurements: boolean
|
||||
+ ) => {
|
||||
+ "worklet";
|
||||
+ if (
|
||||
+ gestureSessionId.value !== sessionId ||
|
||||
+ !gestureSessionActive.value ||
|
||||
+ terminalHandled.value
|
||||
+ ) {
|
||||
+ runOnJS(onDragActivationRejected)(sessionId);
|
||||
+ return;
|
||||
+ }
|
||||
+ if (hasCellMeasurements) {
|
||||
+ activeCellOffset.value = cellOffset;
|
||||
+ activeCellSize.value = cellSize;
|
||||
+ }
|
||||
+ spacerIndexAnim.value = index;
|
||||
+ activeIndexAnim.value = index;
|
||||
+ };
|
||||
+
|
||||
const drag = useStableCallback((activeKey: string) => {
|
||||
- if (disabled.value) return;
|
||||
+ const gestureSession = currentGestureSessionRef.current;
|
||||
+ if (disabled.value || !gestureSession?.active) return;
|
||||
const index = keyToIndexRef.current.get(activeKey);
|
||||
const cellData = cellDataRef.current.get(activeKey);
|
||||
- if (cellData) {
|
||||
- activeCellOffset.value = cellData.measurements.offset;
|
||||
- activeCellSize.value = cellData.measurements.size;
|
||||
- }
|
||||
|
||||
const { onDragBegin } = propsRef.current;
|
||||
if (index !== undefined) {
|
||||
- spacerIndexAnim.value = index;
|
||||
- activeIndexAnim.value = index;
|
||||
+ runOnUI(activateDragOnUI)(
|
||||
+ gestureSession.id,
|
||||
+ index,
|
||||
+ cellData?.measurements.offset ?? -1,
|
||||
+ cellData?.measurements.size ?? -1,
|
||||
+ Boolean(cellData)
|
||||
+ );
|
||||
+ activeDragRef.current = {
|
||||
+ sessionId: gestureSession.id,
|
||||
+ index,
|
||||
+ };
|
||||
setActiveKey(activeKey);
|
||||
onDragBegin?.(index);
|
||||
}
|
||||
@@ -218,8 +299,25 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
props.onRelease?.(index);
|
||||
});
|
||||
|
||||
+ const finalizeCancelledDrag = () => {
|
||||
+ "worklet";
|
||||
+ const activeIndex = activeIndexAnim.value;
|
||||
+ if (terminalHandled.value || activeIndex < 0) return;
|
||||
+
|
||||
+ terminalHandled.value = true;
|
||||
+ isTouchActiveNative.value = false;
|
||||
+ touchTranslate.value = 0;
|
||||
+ activeIndexAnim.value = -1;
|
||||
+ spacerIndexAnim.value = -1;
|
||||
+ activeCellSize.value = -1;
|
||||
+ activeCellOffset.value = -1;
|
||||
+ disabled.value = false;
|
||||
+ };
|
||||
+
|
||||
const onDragEnd = useStableCallback(
|
||||
- ({ from, to }: { from: number; to: number }) => {
|
||||
+ ({ from, to, sessionId }: { from: number; to: number; sessionId: number }) => {
|
||||
+ if (activeDragRef.current?.sessionId !== sessionId) return;
|
||||
+ activeDragRef.current = null;
|
||||
const { onDragEnd, data } = props;
|
||||
|
||||
const newData = [...data];
|
||||
@@ -238,26 +336,6 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
props.onPlaceholderIndexChange?.(index);
|
||||
});
|
||||
|
||||
- // Handle case where user ends drag without moving their finger.
|
||||
- useAnimatedReaction(
|
||||
- () => {
|
||||
- return isTouchActiveNative.value;
|
||||
- },
|
||||
- (cur, prev) => {
|
||||
- if (cur !== prev && !cur) {
|
||||
- const hasMoved = !!touchTranslate.value;
|
||||
- if (!hasMoved && activeIndexAnim.value >= 0 && !disabled.value) {
|
||||
- runOnJS(onRelease)(activeIndexAnim.value);
|
||||
- runOnJS(onDragEnd)({
|
||||
- from: activeIndexAnim.value,
|
||||
- to: spacerIndexAnim.value,
|
||||
- });
|
||||
- }
|
||||
- }
|
||||
- },
|
||||
- [isTouchActiveNative, onDragEnd, onRelease]
|
||||
- );
|
||||
-
|
||||
useAnimatedReaction(
|
||||
() => {
|
||||
return spacerIndexAnim.value;
|
||||
@@ -271,11 +349,19 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
);
|
||||
|
||||
const gestureDisabled = useSharedValue(false);
|
||||
+ const touchStartX = useSharedValue(0);
|
||||
+ const touchStartY = useSharedValue(0);
|
||||
+ const coordinatesDragWithExternalGesture = Boolean(props.simultaneousHandlers);
|
||||
|
||||
- const panGesture = Gesture.Pan()
|
||||
+ let panGesture = Gesture.Pan()
|
||||
.onBegin((evt) => {
|
||||
gestureDisabled.value = disabled.value;
|
||||
if (gestureDisabled.value) return;
|
||||
+ const sessionId = gestureSessionId.value + 1;
|
||||
+ gestureSessionId.value = sessionId;
|
||||
+ gestureSessionActive.value = true;
|
||||
+ terminalHandled.value = false;
|
||||
+ runOnJS(onGestureBegin)(sessionId);
|
||||
panGestureState.value = evt.state;
|
||||
})
|
||||
.onUpdate((evt) => {
|
||||
@@ -286,8 +372,15 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
: evt.translationY;
|
||||
touchTranslate.value = translation;
|
||||
})
|
||||
- .onEnd((evt) => {
|
||||
+ .onEnd((evt, success) => {
|
||||
if (gestureDisabled.value) return;
|
||||
+ if (
|
||||
+ !success ||
|
||||
+ terminalHandled.value ||
|
||||
+ !gestureSessionActive.value
|
||||
+ )
|
||||
+ return;
|
||||
+
|
||||
// Set touch val to current translate val
|
||||
isTouchActiveNative.value = false;
|
||||
const translation = horizontalAnim.value
|
||||
@@ -299,6 +392,7 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
|
||||
// Only call onDragEnd if actually dragging a cell
|
||||
if (activeIndexAnim.value === -1 || disabled.value) return;
|
||||
+ terminalHandled.value = true;
|
||||
disabled.value = true;
|
||||
runOnJS(onRelease)(activeIndexAnim.value);
|
||||
const springTo = placeholderOffset.value - activeCellOffset.value;
|
||||
@@ -309,14 +403,58 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
runOnJS(onDragEnd)({
|
||||
from: activeIndexAnim.value,
|
||||
to: spacerIndexAnim.value,
|
||||
+ sessionId: gestureSessionId.value,
|
||||
});
|
||||
disabled.value = false;
|
||||
}
|
||||
);
|
||||
})
|
||||
- .onTouchesDown(() => {
|
||||
+ .onFinalize((evt) => {
|
||||
+ panGestureState.value = evt.state;
|
||||
+ runOnJS(onContainerTouchEnd)();
|
||||
+ if (gestureDisabled.value) return;
|
||||
+
|
||||
+ const sessionId = gestureSessionId.value;
|
||||
+ const committed = terminalHandled.value;
|
||||
+ gestureSessionActive.value = false;
|
||||
+ if (!committed) finalizeCancelledDrag();
|
||||
+ runOnJS(onGestureFinalize)(sessionId, committed);
|
||||
+ })
|
||||
+ .onTouchesDown((event) => {
|
||||
+ const touch = event.changedTouches[0];
|
||||
+ if (touch) {
|
||||
+ touchStartX.value = touch.absoluteX;
|
||||
+ touchStartY.value = touch.absoluteY;
|
||||
+ }
|
||||
runOnJS(onContainerTouchStart)();
|
||||
})
|
||||
+ .onTouchesMove((event, stateManager) => {
|
||||
+ if (!coordinatesDragWithExternalGesture) return;
|
||||
+
|
||||
+ const touch = event.changedTouches[0];
|
||||
+ if (!touch || event.numberOfTouches !== 1) {
|
||||
+ stateManager.fail();
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const deltaX = touch.absoluteX - touchStartX.value;
|
||||
+ const deltaY = touch.absoluteY - touchStartY.value;
|
||||
+ const absDeltaX = Math.abs(deltaX);
|
||||
+ const absDeltaY = Math.abs(deltaY);
|
||||
+
|
||||
+ if (activeIndexAnim.value < 0) {
|
||||
+ if (absDeltaX > 6 || absDeltaY > 6) {
|
||||
+ stateManager.fail();
|
||||
+ }
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (absDeltaX > 10 && absDeltaX > absDeltaY) {
|
||||
+ stateManager.fail();
|
||||
+ } else if (absDeltaY >= activationDistanceProp && absDeltaY > absDeltaX) {
|
||||
+ stateManager.activate();
|
||||
+ }
|
||||
+ })
|
||||
.onTouchesUp(() => {
|
||||
// Turning this into a worklet causes timing issues. We want it to run
|
||||
// just after the finger lifts.
|
||||
@@ -324,7 +462,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
});
|
||||
|
||||
if (dragHitSlop) panGesture.hitSlop(dragHitSlop);
|
||||
- if (activationDistanceProp) {
|
||||
+ if (coordinatesDragWithExternalGesture) {
|
||||
+ panGesture = panGesture.manualActivation(true);
|
||||
+ } else if (activationDistanceProp) {
|
||||
const activeOffset = [-activationDistanceProp, activationDistanceProp];
|
||||
if (props.horizontal) {
|
||||
panGesture.activeOffsetX(activeOffset);
|
||||
@@ -332,6 +472,21 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
panGesture.activeOffsetY(activeOffset);
|
||||
}
|
||||
}
|
||||
@@ -13,11 +365,30 @@ index 7c88afc..b630940 100644
|
||||
+ : [props.simultaneousHandlers])
|
||||
+ );
|
||||
+ }
|
||||
|
||||
+
|
||||
+ // GestureDetector only updates handler config when a retained host reappears
|
||||
+ // with the same native tag. Changing the gesture count forces a native
|
||||
+ // reattachment without remounting the list subtree.
|
||||
+ const detectorGesture =
|
||||
+ props.dragGestureHostPresented === false
|
||||
+ ? Gesture.Simultaneous(panGesture, Gesture.Manual().enabled(false))
|
||||
+ : panGesture;
|
||||
|
||||
const onScroll = useStableCallback((scrollOffset: number) => {
|
||||
props.onScrollOffsetChange?.(scrollOffset);
|
||||
@@ -373,8 +528,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
horizontal={!!props.horizontal}
|
||||
layoutAnimationDisabled={layoutAnimationDisabled}
|
||||
>
|
||||
- <GestureDetector gesture={panGesture}>
|
||||
+ <GestureDetector gesture={detectorGesture}>
|
||||
<Animated.View
|
||||
+ collapsable={false}
|
||||
style={props.containerStyle}
|
||||
ref={containerRef}
|
||||
onLayout={onContainerLayout}
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||
index 1559352..99d7a9b 100644
|
||||
index 1559352..33eb5de 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||
+++ b/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||
@@ -1,5 +1,5 @@
|
||||
@@ -36,3 +407,43 @@ index 1559352..99d7a9b 100644
|
||||
|
||||
const onSuccess = (_x: number, y: number) => {
|
||||
listVerticalOffset.value = y;
|
||||
@@ -74,6 +74,10 @@ function NestableDraggableFlatListInner<T>(
|
||||
}
|
||||
);
|
||||
|
||||
+ const onDragTerminate = useStableCallback(() => {
|
||||
+ setOuterScrollEnabled(true);
|
||||
+ });
|
||||
+
|
||||
const onAnimValInit: DraggableFlatListProps<T>["onAnimValInit"] = useStableCallback(
|
||||
(params) => {
|
||||
setListHoverOffset(params.hoverOffset);
|
||||
@@ -95,6 +99,7 @@ function NestableDraggableFlatListInner<T>(
|
||||
outerScrollOffset={outerScrollOffset}
|
||||
onDragBegin={onDragBegin}
|
||||
onDragEnd={onDragEnd}
|
||||
+ onDragTerminate={onDragTerminate}
|
||||
onAnimValInit={onAnimValInit}
|
||||
/>
|
||||
);
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/src/types.ts b/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||
index d6755c8..1b244a6 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||
+++ b/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||
@@ -36,12 +36,16 @@ export type DraggableFlatListProps<T> = Modify<
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: (index: number) => void;
|
||||
onDragEnd?: (params: DragEndParams<T>) => void;
|
||||
+ /** @internal Used by the nestable wrapper to release its outer-scroll lock on cancellation. */
|
||||
+ onDragTerminate?: () => void;
|
||||
onPlaceholderIndexChange?: (placeholderIndex: number) => void;
|
||||
onRelease?: (index: number) => void;
|
||||
onScrollOffsetChange?: (scrollOffset: number) => void;
|
||||
renderItem: RenderItem<T>;
|
||||
renderPlaceholder?: RenderPlaceholder<T>;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||
+ /** @internal Forces handler reattachment when a retained native host is presented. */
|
||||
+ dragGestureHostPresented?: boolean;
|
||||
outerScrollOffset?: Animated.SharedValue<number>;
|
||||
onAnimValInit?: (animVals: ReturnType<typeof useAnimatedValues>) => void;
|
||||
itemEnteringAnimation?: AnimateProps<Animated.View>["entering"];
|
||||
|
||||
@@ -1,5 +1,171 @@
|
||||
diff --git a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
|
||||
index a532cf6..7b66222 100644
|
||||
--- a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
|
||||
+++ b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt
|
||||
@@ -2,6 +2,7 @@ package com.swmansion.gesturehandler.core
|
||||
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.PointF
|
||||
+import android.util.SparseArray
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -24,6 +25,10 @@ class GestureHandlerOrchestrator(
|
||||
private val awaitingHandlers = arrayListOf<GestureHandler>()
|
||||
private val preparedHandlers = arrayListOf<GestureHandler>()
|
||||
|
||||
+ // Used by `cancelTouchesInInterceptedViews`.
|
||||
+ private val viewsToCancel = arrayListOf<View>()
|
||||
+ private val pointerDownPoints = SparseArray<PointF>()
|
||||
+
|
||||
// In `onHandlerStateChange` method we iterate through `awaitingHandlers`, but calling `tryActivate` may modify this list.
|
||||
// To avoid `ConcurrentModificationException` we iterate through copy. There is one more problem though - if handler was
|
||||
// removed from `awaitingHandlers`, it was still present in copy of original list. This hashset helps us identify which handlers
|
||||
@@ -42,6 +47,7 @@ class GestureHandlerOrchestrator(
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
isHandlingTouch = true
|
||||
val action = event.actionMasked
|
||||
+ trackPointerDownPoints(event)
|
||||
if (action == MotionEvent.ACTION_DOWN ||
|
||||
action == MotionEvent.ACTION_POINTER_DOWN ||
|
||||
action == MotionEvent.ACTION_HOVER_MOVE
|
||||
@@ -562,6 +568,107 @@ class GestureHandlerOrchestrator(
|
||||
extractGestureHandlers(wrapperView, tempCoords, pointerId, event)
|
||||
}
|
||||
|
||||
+ private fun trackPointerDownPoints(event: MotionEvent) {
|
||||
+ val index = event.actionIndex
|
||||
+ when (event.actionMasked) {
|
||||
+ MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN ->
|
||||
+ pointerDownPoints.put(event.getPointerId(index), PointF(event.getX(index), event.getY(index)))
|
||||
+ MotionEvent.ACTION_POINTER_UP ->
|
||||
+ pointerDownPoints.remove(event.getPointerId(index))
|
||||
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->
|
||||
+ pointerDownPoints.clear()
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ fun cancelTouchesInInterceptedViews(event: MotionEvent) {
|
||||
+ viewsToCancel.clear()
|
||||
+ for (i in 0 until pointerDownPoints.size()) {
|
||||
+ val point = pointerDownPoints.valueAt(i)
|
||||
+ tempCoords[0] = point.x
|
||||
+ tempCoords[1] = point.y
|
||||
+ collectViewsAtPoint(wrapperView, tempCoords, viewsToCancel)
|
||||
+ }
|
||||
+
|
||||
+ if (viewsToCancel.isEmpty()) {
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ val activeHandlers = gestureHandlers.filter { it.isActive }
|
||||
+ val cancelEvent = MotionEvent.obtain(event).apply { action = MotionEvent.ACTION_CANCEL }
|
||||
+
|
||||
+ for (view in viewsToCancel) {
|
||||
+ if (view === wrapperView || isViewDrivenByActiveNativeGesture(view, activeHandlers)) {
|
||||
+ continue
|
||||
+ }
|
||||
+ view.onTouchEvent(cancelEvent)
|
||||
+ }
|
||||
+
|
||||
+ cancelEvent.recycle()
|
||||
+ viewsToCancel.clear()
|
||||
+ }
|
||||
+
|
||||
+ // Whether the view's touch is still owned by a NativeViewGestureHandler that survived arbitration.
|
||||
+ // Only those are fed through `onTouchEvent`, so only those break if cancelled. Other handlers are
|
||||
+ // orchestrator-driven and unaffected.
|
||||
+ private fun isViewDrivenByActiveNativeGesture(view: View, activeHandlers: List<GestureHandler>) =
|
||||
+ handlerRegistry.getHandlersForView(view)?.let { handlers ->
|
||||
+ synchronized(handlers) {
|
||||
+ handlers.any { nativeGestureSurvivesArbitration(it, activeHandlers) }
|
||||
+ }
|
||||
+ } ?: false
|
||||
+
|
||||
+ // A native handler survives arbitration if it is active, or it does not conflict with any active handler.
|
||||
+ private fun nativeGestureSurvivesArbitration(handler: GestureHandler, activeHandlers: List<GestureHandler>) =
|
||||
+ handler is NativeViewGestureHandler &&
|
||||
+ (handler.isActive || activeHandlers.none { shouldHandlerBeCancelledBy(handler, it) })
|
||||
+
|
||||
+ // Collects the view path under the point (topmost child first, like touch dispatch), leaf to root.
|
||||
+ private fun collectViewsAtPoint(view: View, coords: FloatArray, out: MutableList<View>): Boolean {
|
||||
+ val pointerEvents = viewConfigHelper.getPointerEventsConfigForView(view)
|
||||
+ if (pointerEvents == PointerEventsConfig.NONE) {
|
||||
+ return false
|
||||
+ }
|
||||
+
|
||||
+ var found = false
|
||||
+ if (view is ViewGroup && pointerEvents != PointerEventsConfig.BOX_ONLY) {
|
||||
+ for (i in view.childCount - 1 downTo 0) {
|
||||
+ val child = viewConfigHelper.getChildInDrawingOrderAtIndex(view, i)
|
||||
+ if (!canReceiveEvents(child)) {
|
||||
+ continue
|
||||
+ }
|
||||
+ val childPoint = tempPoint
|
||||
+ transformPointToChildViewCoords(coords[0], coords[1], view, child, childPoint)
|
||||
+ if (isClipping(child) && !isTransformedTouchPointInView(childPoint.x, childPoint.y, child)) {
|
||||
+ continue
|
||||
+ }
|
||||
+ val restoreX = coords[0]
|
||||
+ val restoreY = coords[1]
|
||||
+ coords[0] = childPoint.x
|
||||
+ coords[1] = childPoint.y
|
||||
+ found = collectViewsAtPoint(child, coords, out)
|
||||
+ coords[0] = restoreX
|
||||
+ coords[1] = restoreY
|
||||
+
|
||||
+ if (found) {
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // BOX_NONE views can't be the target themselves, only their children can.
|
||||
+ val selfIsTarget = pointerEvents != PointerEventsConfig.BOX_NONE &&
|
||||
+ isTransformedTouchPointInView(coords[0], coords[1], view)
|
||||
+
|
||||
+ if (found || selfIsTarget) {
|
||||
+ // `out` may already contain this view when several pointers share part of their path.
|
||||
+ if (!out.contains(view)) {
|
||||
+ out.add(view)
|
||||
+ }
|
||||
+ return true
|
||||
+ }
|
||||
+ return false
|
||||
+ }
|
||||
+
|
||||
private fun extractGestureHandlers(
|
||||
viewGroup: ViewGroup,
|
||||
coords: FloatArray,
|
||||
diff --git a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
|
||||
index d8f8fd6..ed101b9 100644
|
||||
--- a/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
|
||||
+++ b/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootHelper.kt
|
||||
@@ -19,6 +19,7 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
|
||||
private val jsGestureHandler: GestureHandler?
|
||||
val rootView: ViewGroup
|
||||
private var shouldIntercept = false
|
||||
+ private var wasIntercepting = false
|
||||
private var passingTouch = false
|
||||
|
||||
init {
|
||||
@@ -117,6 +118,14 @@ class RNGestureHandlerRootHelper(private val context: ReactContext, wrappedView:
|
||||
passingTouch = true
|
||||
orchestrator!!.onTouchEvent(ev)
|
||||
passingTouch = false
|
||||
+
|
||||
+ // On the transition into interception, cancel the native views the pointers landed on - the
|
||||
+ // framework's ACTION_CANCEL never reaches them since RNGH ignores `onInterceptTouchEvent`.
|
||||
+ if (shouldIntercept && !wasIntercepting) {
|
||||
+ orchestrator!!.cancelTouchesInInterceptedViews(ev)
|
||||
+ }
|
||||
+ wasIntercepting = shouldIntercept
|
||||
+
|
||||
return shouldIntercept
|
||||
}
|
||||
|
||||
diff --git a/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js b/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
||||
index 76eb808..0debe83 100644
|
||||
index 76eb808..7c40526 100644
|
||||
--- a/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
||||
+++ b/node_modules/react-native-gesture-handler/lib/commonjs/web/tools/PointerEventManager.js
|
||||
@@ -58,7 +58,14 @@ class PointerEventManager extends _EventManager.default {
|
||||
@@ -19,7 +185,7 @@ index 76eb808..0debe83 100644
|
||||
this.markAsOutOfBounds(adaptedEvent.pointerId);
|
||||
this.trackedPointers.delete(adaptedEvent.pointerId);
|
||||
diff --git a/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js b/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
||||
index 6ce3b8f..3976f3f 100644
|
||||
index 6ce3b8f..8fe28bc 100644
|
||||
--- a/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
||||
+++ b/node_modules/react-native-gesture-handler/lib/module/web/tools/PointerEventManager.js
|
||||
@@ -53,7 +53,14 @@ export default class PointerEventManager extends EventManager {
|
||||
|
||||
Reference in New Issue
Block a user