diff --git a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt index a3d36d95f1..85e8a61b5a 100644 --- a/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt +++ b/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt @@ -161,6 +161,9 @@ class NativeViewGestureHandler : GestureHandler() { cancel() } else { hook.sendTouchEvent(view, event) + if (shouldStopNestedScroll()) { + view.stopNestedScroll() + } if ((state == STATE_UNDETERMINED || state == STATE_BEGAN) && hook.canActivate(view)) { activate() @@ -206,9 +209,19 @@ class NativeViewGestureHandler : GestureHandler() { action = MotionEvent.ACTION_CANCEL } hook.sendTouchEvent(view, event) + if (shouldStopNestedScroll()) { + view?.stopNestedScroll() + } event.recycle() } + // Once the handler is active, it delivers touches straight to the view's `onTouchEvent`. Normally + // touches arrive through `View.dispatchTouchEvent`, which also ends the nested scroll when the finger + // goes up. Because we skip it, the nested scroll stays open and the parent never finds out that the + // gesture is over - e.g. SwipeRefreshLayout never fires refresh (#4485). While the handler is not + // active, the view still receives touches the regular way, so Android takes care of it. + private fun shouldStopNestedScroll() = state == STATE_ACTIVE && hook.shouldStopNestedScroll() + override fun onCancel() = dispatchCancelEventToView() override fun onFail() = dispatchCancelEventToView() @@ -348,6 +361,12 @@ class NativeViewGestureHandler : GestureHandler() { */ fun canBegin(event: MotionEvent) = true + /** + * Whether the view's nested scroll should be stopped when the active gesture ends. Touches + * are fed through `onTouchEvent`, so `View.dispatchTouchEvent` never gets to do it. + */ + fun shouldStopNestedScroll() = false + /** * Checks whether handler can activate. Used by TextViewHook. */ @@ -523,6 +542,10 @@ class NativeViewGestureHandler : GestureHandler() { private class ScrollViewHook : NativeViewGestureHandlerHook { override fun shouldCancelRootViewGestureHandlerIfNecessary() = true + + // ScrollView starts a nested scroll on DOWN but never stops it itself. Without this the + // parent's `onStopNestedScroll` never runs, e.g. SwipeRefreshLayout never triggers refresh. + override fun shouldStopNestedScroll() = true } private class ReactViewGroupHook : NativeViewGestureHandlerHook {