Repository navigation
Commit f127c58
committed
[Android] Stop nested scroll when a native handler's gesture ends (#4492)
## Description
`ScrollView` and `FlatList` from Gesture Handler never fire `onRefresh`
when given React Native's `RefreshControl` on Android. The spinner
follows the pull but stays parked on release, and the refresh only
triggers on the next touch anywhere on the screen.
With a `refreshControl` RN wraps the scroll view in a
`SwipeRefreshLayout` and enables nested scrolling on it. The pull
reaches the layout through the nested-scroll API, and the layout
finishes it (fires refresh or snaps back) only in `onStopNestedScroll`.
Android calls `stopNestedScroll()` from `View.dispatchTouchEvent` at the
end of a gesture, but once `NativeViewGestureHandler` activates it
delivers touches straight to the view's `onTouchEvent`, so that cleanup
never runs. The nested scroll stays open and the layout is released one
touch late, by the CANCEL our root view dispatches on the next DOWN.
Gesture Handler's own `RefreshControl` avoids this because its handler
drives the `SwipeRefreshLayout` in touch-drag mode, which finishes the
spinner in `onTouchEvent` without relying on the nested-scroll cleanup.
The handler now calls `stopNestedScroll()` on the view after feeding it
the final UP or the synthetic CANCEL, mirroring
`View.dispatchTouchEvent`. It only does so while active, since below
that the view still receives the events through regular dispatch, and
only for views whose hook opts in through the new
`shouldStopNestedScroll()`. `ScrollViewHook` opts in.
Fixes #4485
## Test plan
- Pulled to refresh repeatedly on GH `ScrollView` + RN `RefreshControl`,
GH `FlatList` + RN `RefreshControl`, and `Gesture.Native()` around an RN
`ScrollView` + RN `RefreshControl`. `onRefresh` fires on every pull and
the spinner retracts.
- GH `ScrollView` + GH `RefreshControl` and RN `ScrollView` + RN
`RefreshControl` unchanged.
- Nested GH `ScrollView` inside GH `ScrollView`, with and without an RN
`RefreshControl` on the outer: scroll handover, fling, and
pull-to-refresh from inside the inner list work the same as with RN
scroll views.
<details>
<summary>Repro</summary>
```tsx
import React, { useCallback, useState } from 'react';
import { RefreshControl, Text, View } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
export default function App() {
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setRefreshing(true);
setTimeout(() => setRefreshing(false), 1000);
}, []);
return (
<ScrollView
style={{ flex: 1 }}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}>
{Array.from({ length: 40 }, (_, i) => (
<View key={i} style={{ padding: 20 }}>
<Text>Row {i}</Text>
</View>
))}
</ScrollView>
);
}
```
</details>1 parent 8714f60 commit f127c58
1 file changed
Lines changed: 23 additions & 0 deletions
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 | |
|---|---|---|---|
| |||
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
112 | 115 | | |
113 | 116 | | |
114 | 117 | | |
| |||
156 | 159 | | |
157 | 160 | | |
158 | 161 | | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
159 | 165 | | |
160 | 166 | | |
161 | 167 | | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
162 | 175 | | |
163 | 176 | | |
164 | 177 | | |
| |||
211 | 224 | | |
212 | 225 | | |
213 | 226 | | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
214 | 233 | | |
215 | 234 | | |
216 | 235 | | |
| |||
336 | 355 | | |
337 | 356 | | |
338 | 357 | | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
339 | 362 | | |
340 | 363 | | |
341 | 364 | | |
| |||
0 commit comments