Repository navigation
Commit f232258
authored
[Android] Don't let an awaiting parent handler cancel the child it is waiting for (#4476)
## Description
Fixes #3326
Replaces `state == ACTIVE` check with `handler.isActive` to correctly
account for handlers that have technically met activation criteria, but
are awaiting for the failure of another handler.
## Test plan
<details>
<summary>Tested on updated repro from issue</summary>
```jsx
import React, { useState } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import type {
LegacyPanGesture,
NativeGesture,
PanGesture,
} from 'react-native-gesture-handler';
import {
Gesture,
GestureDetector,
useNativeGesture,
usePanGesture,
} from 'react-native-gesture-handler';
import type { PagerViewOnPageSelectedEvent } from 'react-native-pager-view';
import PagerView from 'react-native-pager-view';
import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
// Reproduction of #3326
// Expected: dragging the yellow ScrollView never begins/activates the drawer pan.
type AnyPanGesture = PanGesture | LegacyPanGesture;
type AnyNativeGesture = NativeGesture | ReturnType<typeof Gesture.Native>;
type SetStatus = React.Dispatch<React.SetStateAction<string>>;
type Mode =
| 'requireToFail-parent'
| 'block-parent'
| 'block-child'
| 'v2'
| 'noPager'
| 'noPaging'
| 'nestedPan';
const MODES: Mode[] = [
'requireToFail-parent',
'block-parent',
'block-child',
'v2',
'noPager',
'noPaging',
'nestedPan',
];
export default function EmptyExample() {
const [mode, setMode] = useState<Mode>('requireToFail-parent');
return (
<View style={styles.container}>
<View style={styles.modes}>
{MODES.map((m) => (
<Pressable
key={m}
testID={`mode-${m}`}
onPress={() => setMode(m)}
style={[styles.modeButton, m === mode && styles.modeButtonActive]}>
<Text style={styles.modeText}>{m}</Text>
</Pressable>
))}
</View>
{mode === 'requireToFail-parent' && <RequireToFailInParent key={mode} />}
{mode === 'block-parent' && <BlockInParent key={mode} />}
{mode === 'block-child' && <BlockInChild key={mode} />}
{mode === 'v2' && <V2BlockInChild key={mode} />}
{mode === 'noPager' && <BlockInChild key={mode} pager={false} />}
{mode === 'noPaging' && <BlockInChild key={mode} paging={false} />}
{mode === 'nestedPan' && <NestedPanInScrollView key={mode} />}
</View>
);
}
function useDrawerPan(
innerNative: NativeGesture | undefined,
swipeEnabled: boolean,
setStatus: SetStatus
) {
const val = useSharedValue(0);
const pan = usePanGesture({
requireToFail: innerNative,
activeOffsetX: swipeEnabled ? 5 : undefined,
failOffsetX: swipeEnabled ? -1 : [0, 0],
failOffsetY: swipeEnabled ? undefined : [0, 0],
runOnJS: true,
onBegin: () => {
setStatus('pan: begin');
val.set(0);
},
onActivate: () => {
setStatus((s) => `${s} > ACTIVE`);
},
onUpdate: (e) => {
val.set(e.translationX);
},
onDeactivate: () => {
val.set(0);
},
onFinalize: (e) => {
setStatus((s) => `${s} > finalized (canceled: ${e.canceled})`);
val.set(0);
},
});
const style = useAnimatedStyle(() => ({
flex: 1,
transform: [{ translateX: val.value }],
}));
return { pan, style };
}
function RequireToFailInParent() {
const [status, setStatus] = useState('pan: idle');
const [swipeEnabled, setSwipeEnabled] = useState(true);
const innerNative = useNativeGesture({});
const { pan, style } = useDrawerPan(innerNative, swipeEnabled, setStatus);
const pagerNative = useNativeGesture({ requireToFail: pan });
return (
<Drawer pan={pan} style={style} status={status}>
<Pager
native={pagerNative}
setSwipeEnabled={setSwipeEnabled}
renderPage={(index) =>
index === 0 ? (
<InnerScrollView innerNative={innerNative} />
) : (
<Text style={styles.status}>Page {index + 1}</Text>
)
}
/>
</Drawer>
);
}
function BlockInParent() {
const [status, setStatus] = useState('pan: idle');
const [swipeEnabled, setSwipeEnabled] = useState(true);
const { pan, style } = useDrawerPan(undefined, swipeEnabled, setStatus);
const innerNative = useNativeGesture({ block: pan });
const pagerNative = useNativeGesture({ requireToFail: pan });
return (
<Drawer pan={pan} style={style} status={status}>
<Pager
native={pagerNative}
setSwipeEnabled={setSwipeEnabled}
renderPage={(index) =>
index === 0 ? (
<InnerScrollView innerNative={innerNative} />
) : (
<Text style={styles.status}>Page {index + 1}</Text>
)
}
/>
</Drawer>
);
}
function BlockInChild({
pager = true,
paging = true,
}: {
pager?: boolean;
paging?: boolean;
}) {
const [status, setStatus] = useState('pan: idle');
const [swipeEnabled, setSwipeEnabled] = useState(true);
const { pan, style } = useDrawerPan(undefined, swipeEnabled, setStatus);
const pagerNative = useNativeGesture({ requireToFail: pan });
return (
<Drawer pan={pan} style={style} status={status}>
<Pager
native={pager ? pagerNative : undefined}
setSwipeEnabled={setSwipeEnabled}
renderPage={() => (
<InnerScrollViewBlockingPan pan={pan} paging={paging} />
)}
/>
</Drawer>
);
}
// Scenario from PR #3095: a pan nested in a ScrollView must not activate while the ScrollView scrolls.
function NestedPanInScrollView() {
const [status, setStatus] = useState('pan: idle');
const [scrollY, setScrollY] = useState(0);
const native = useNativeGesture({});
const pan = usePanGesture({
runOnJS: true,
onBegin: () => setStatus('pan: begin'),
onActivate: () => setStatus((s) => `${s} > ACTIVE`),
onFinalize: (e) =>
setStatus((s) => `${s} > finalized (canceled: ${e.canceled})`),
});
return (
<View style={styles.container}>
<Text testID="status" style={styles.status}>
{status}
</Text>
<Text testID="scrollY" style={styles.status}>
scrollY: {Math.round(scrollY)}
</Text>
<GestureDetector gesture={native}>
<ScrollView
testID="outer-scroll"
style={styles.pager}
scrollEventThrottle={16}
onScroll={(e) => setScrollY(e.nativeEvent.contentOffset.y)}>
<View style={styles.spacer} />
<GestureDetector gesture={pan}>
<View testID="nested-box" style={styles.nestedBox} />
</GestureDetector>
<View style={styles.spacer} />
<View style={styles.spacer} />
<View style={styles.spacer} />
</ScrollView>
</GestureDetector>
</View>
);
}
function V2BlockInChild() {
const [status, setStatus] = useState('pan: idle');
const [swipeEnabled, setSwipeEnabled] = useState(true);
const val = useSharedValue(0);
let pan = Gesture.Pan()
.runOnJS(true)
.onBegin(() => {
setStatus('pan: begin');
val.set(0);
})
.onStart(() => {
setStatus((s) => `${s} > ACTIVE`);
})
.onUpdate((e) => {
val.set(e.translationX);
})
.onEnd(() => {
val.set(0);
})
.onFinalize((_e, success) => {
setStatus((s) => `${s} > finalized (canceled: ${!success})`);
val.set(0);
});
pan = swipeEnabled
? pan.failOffsetX(-1).activeOffsetX(5)
: pan.failOffsetX([0, 0]).failOffsetY([0, 0]);
const style = useAnimatedStyle(() => ({
flex: 1,
transform: [{ translateX: val.value }],
}));
const pagerNative = Gesture.Native().requireExternalGestureToFail(pan);
return (
<Drawer pan={pan} style={style} status={status}>
<Pager
native={pagerNative}
setSwipeEnabled={setSwipeEnabled}
renderPage={() => <V2InnerScrollView pan={pan} />}
/>
</Drawer>
);
}
function V2InnerScrollView({ pan }: { pan: LegacyPanGesture }) {
const innerNative = Gesture.Native().blocksExternalGesture(pan);
return <InnerScrollView innerNative={innerNative} />;
}
function Drawer({
pan,
style,
status,
children,
}: {
pan: AnyPanGesture;
style: ReturnType<typeof useAnimatedStyle>;
status: string;
children: React.ReactNode;
}) {
return (
<GestureDetector gesture={pan as PanGesture}>
<Animated.View style={style}>
<Text testID="status" style={styles.status}>
{status}
</Text>
{children}
</Animated.View>
</GestureDetector>
);
}
function Pager({
renderPage,
native,
setSwipeEnabled,
}: {
renderPage: (index: number) => React.ReactNode;
native: AnyNativeGesture | undefined;
setSwipeEnabled: (enabled: boolean) => void;
}) {
const [page, setPage] = useState(0);
if (!native) {
return <View style={styles.pager}>{renderPage(0)}</View>;
}
return (
<GestureDetector gesture={native as NativeGesture}>
<PagerView
overdrag
initialPage={0}
style={styles.pager}
onPageSelected={(e: PagerViewOnPageSelectedEvent) => {
setSwipeEnabled(e.nativeEvent.position === 0);
setPage(e.nativeEvent.position);
}}>
<View key="1">
<Text testID="page" style={styles.status}>
page: {page}
</Text>
{renderPage(0)}
</View>
<View key="2">{renderPage(1)}</View>
<View key="3">{renderPage(2)}</View>
</PagerView>
</GestureDetector>
);
}
function InnerScrollViewBlockingPan({
pan,
paging,
}: {
pan: PanGesture;
paging: boolean;
}) {
const innerNative = useNativeGesture({ block: pan });
return <InnerScrollView innerNative={innerNative} paging={paging} />;
}
function InnerScrollView({
innerNative,
paging = true,
}: {
innerNative: AnyNativeGesture;
paging?: boolean;
}) {
const [scrollX, setScrollX] = useState(0);
return (
<View style={styles.scrollContainer}>
<Text testID="scrollX" style={styles.status}>
scrollX: {Math.round(scrollX)}
</Text>
<GestureDetector gesture={innerNative as NativeGesture}>
<ScrollView
horizontal
pagingEnabled={paging}
testID="inner-scroll"
onScroll={(e) => setScrollX(e.nativeEvent.contentOffset.x)}
scrollEventThrottle={16}
style={styles.scroll}>
<Text style={styles.scrollText}>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
27 28 29 30 31 32 33 34 35 36 37 38 39 40
</Text>
</ScrollView>
</GestureDetector>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
modes: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 4,
padding: 4,
},
modeButton: {
width: '32%',
padding: 8,
backgroundColor: '#ddd',
borderRadius: 6,
},
modeButtonActive: {
backgroundColor: '#8f8',
},
modeText: {
fontSize: 11,
textAlign: 'center',
},
status: {
padding: 8,
fontSize: 18,
textAlign: 'center',
},
pager: {
flex: 1,
backgroundColor: 'green',
},
scrollContainer: {
paddingTop: 150,
alignItems: 'center',
},
scroll: {
width: 300,
height: 200,
backgroundColor: 'yellow',
},
scrollText: {
width: 1000,
},
spacer: {
height: 400,
},
nestedBox: {
width: 150,
height: 150,
alignSelf: 'center',
backgroundColor: 'yellow',
},
});
```
</details>1 parent 13da360 commit f232258
1 file changed
Lines changed: 1 addition & 1 deletion
File tree
- packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
127 | | - | |
| 127 | + | |
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
| |||
0 commit comments