Skip to content

Commit 6c2a8df

Browse files
authored
[Android] Stop nested scroll when the root starts intercepting (#4592)
## Description When a gesture activates during a pull-to-refresh on an RN `FlatList` with an RN `RefreshControl`, the spinner parks half-pulled and `onRefresh` fires only on the next touch. Reported with `ReanimatedSwipeable` rows in #4563. RN drives the pull through nested scrolling and `SwipeRefreshLayout` finishes it only in `onStopNestedScroll`, which Android triggers from `View.dispatchTouchEvent` by calling `stopNestedScroll()` after the final UP or CANCEL. When the root view starts intercepting, `cancelTouchesInInterceptedViews` feeds `ACTION_CANCEL` straight to `onTouchEvent`, so that cleanup never runs. Same omission #4492 fixed on the `NativeViewGestureHandler` path. The sweep now calls `stopNestedScroll()` after `onTouchEvent`, as [`View.dispatchTouchEvent` does](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/View.java;l=16587). It is a no-op without an open nested scroll. An interrupted pull now snaps back, or fires `onRefresh` if it was past the threshold. ## Test plan - Before #4582 (main at 993bbe7): RN `FlatList` + RN `RefreshControl` with `ReanimatedSwipeable` rows and with bare v3 pan rows, drag a row down then sideways. Short pull snaps back and the row still swipes, long pull fires `onRefresh` immediately. Previously the spinner parked until the next touch. - On current main: single-finger drag leaves the pull to the scroll view (gesture cancelled by #4582). Hold a pull with one finger and swipe another row with a second finger: spinner snaps back or refreshes at once instead of parking. - GH `FlatList` + RN `RefreshControl`, GH `FlatList` + GH `RefreshControl`, plain pulls and plain row swipes unchanged. iOS unaffected. <details> <summary>Repro</summary> ```tsx import { useCallback, useState } from 'react'; import { FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native'; import { GestureDetector, GestureHandlerRootView, usePanGesture } from 'react-native-gesture-handler'; const DATA = Array.from({ length: 30 }, (_, i) => `Row ${i}`); function Row({ item }: { item: string }) { const pan = usePanGesture({ activeOffsetX: [-10, 10] }); return ( <GestureDetector gesture={pan}> <View style={styles.row}> <Text>{item}</Text> </View> </GestureDetector> ); } export default function App() { const [refreshing, setRefreshing] = useState(false); const onRefresh = useCallback(() => { setRefreshing(true); setTimeout(() => setRefreshing(false), 1000); }, []); return ( <GestureHandlerRootView style={styles.container}> <FlatList data={DATA} keyExtractor={(item) => item} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />} renderItem={({ item }) => <Row item={item} />} /> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1 }, row: { padding: 20, backgroundColor: 'white', borderBottomWidth: 1, borderColor: '#eee' }, }); ``` <details>
1 parent cdb3c9d commit 6c2a8df

1 file changed

Lines changed: 1 addition & 0 deletions

File tree

‎packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,7 @@ class GestureHandlerOrchestrator(
774774
continue
775775
}
776776
view.onTouchEvent(cancelEvent)
777+
view.stopNestedScroll()
777778
}
778779

779780
cancelEvent.recycle()

0 commit comments

Comments
 (0)