From b9453f746796fa35af5369860301fbcf6646cf73 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Thu, 8 Jan 2026 10:51:19 -0800 Subject: [PATCH] Forward ACTION_SCROLL events to SwipeRefreshLayout children Summary: This change adds support for joystick and scrollwheel input devices to trigger pull-to-refresh in SwipeRefreshLayout. Previously, `ACTION_SCROLL` events from non-touch input devices (joysticks, scrollwheels, etc.) were not being forwarded to child views wrapped in a SwipeRefreshLayout. This prevented child ScrollViews from receiving these events and handling pull-to-refresh gestures via nested scrolling. The fix overrides `dispatchGenericMotionEvent` in ReactSwipeRefreshLayout to explicitly forward `ACTION_SCROLL` events to the first child view before falling back to the default behavior. This allows child views to process the scroll events and communicate with the SwipeRefreshLayout parent through the standard nested scrolling APIs. Changelog: [Internal] Differential Revision: D90209679 --- .../ReactAndroid/api/ReactAndroid.api | 1 + .../swiperefresh/ReactSwipeRefreshLayout.kt | 30 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 3a8e4513db9900..e0692d80db130f 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6025,6 +6025,7 @@ public abstract interface class com/facebook/react/views/scroll/VirtualView { public final class com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout : androidx/swiperefreshlayout/widget/SwipeRefreshLayout { public fun (Lcom/facebook/react/bridge/ReactContext;)V public fun canChildScrollUp ()Z + public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z public fun onLayout (ZIIII)V public fun onTouchEvent (Landroid/view/MotionEvent;)Z diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt index 7574af95151106..59ae7dbb5e282f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt @@ -16,7 +16,13 @@ import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.events.NativeGestureUtil import kotlin.math.abs -/** Basic extension of [SwipeRefreshLayout] with ReactNative-specific functionality. */ +/** + * Basic extension of [SwipeRefreshLayout] with ReactNative-specific functionality. + * + * This component wraps a scrollable child (typically a ScrollView or RecyclerView) and provides + * pull-to-refresh functionality. It handles touch event interception for the refresh gesture while + * properly forwarding other events to its children. + */ public class ReactSwipeRefreshLayout(reactContext: ReactContext) : SwipeRefreshLayout(reactContext) { @@ -127,6 +133,28 @@ public class ReactSwipeRefreshLayout(reactContext: ReactContext) : return true } + /** + * Dispatches generic motion events to children. + * + * This override ensures that [MotionEvent.ACTION_SCROLL] events (from joystick, scrollwheel, or + * other pointing devices) are properly forwarded to child views. + */ + public override fun dispatchGenericMotionEvent(ev: MotionEvent): Boolean { + // For ACTION_SCROLL events, dispatch to child for handling + // The child ScrollView will use nested scrolling APIs to communicate with this + // SwipeRefreshLayout + if (ev.actionMasked == MotionEvent.ACTION_SCROLL) { + val child = getChildAt(0) + if (child != null) { + val handled = child.dispatchGenericMotionEvent(ev) + if (handled) { + return true + } + } + } + return super.dispatchGenericMotionEvent(ev) + } + private companion object { private const val DEFAULT_CIRCLE_TARGET = 64f }