Skip to content

Commit d2a8181

Browse files
Abbondanzometa-codesync[bot]
authored andcommitted
Forward ACTION_SCROLL events to SwipeRefreshLayout children (#55097)
Summary: Pull Request resolved: #55097 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] Reviewed By: mdvacca Differential Revision: D90209679
1 parent 2aee426 commit d2a8181

22 files changed

+190
-40
lines changed

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6025,6 +6025,7 @@ public abstract interface class com/facebook/react/views/scroll/VirtualView {
60256025
public final class com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout : androidx/swiperefreshlayout/widget/SwipeRefreshLayout {
60266026
public fun <init> (Lcom/facebook/react/bridge/ReactContext;)V
60276027
public fun canChildScrollUp ()Z
6028+
public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z
60286029
public fun onInterceptTouchEvent (Landroid/view/MotionEvent;)Z
60296030
public fun onLayout (ZIIII)V
60306031
public fun onTouchEvent (Landroid/view/MotionEvent;)Z

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<7a9f9d29e1d5f01df33a0893e143a6db>>
7+
* @generated SignedSource<<8cca42a9160a8a0e307a00e53cad46e8>>
88
*/
99

1010
/**
@@ -384,6 +384,12 @@ public object ReactNativeFeatureFlags {
384384
@JvmStatic
385385
public fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean = accessor.overrideBySynchronousMountPropsAtMountingAndroid()
386386

387+
/**
388+
* When enabled, ReactSwipeRefreshLayout will forward ACTION_SCROLL events to its child for proper handling.
389+
*/
390+
@JvmStatic
391+
public fun passScrollToSwipeRefreshChild(): Boolean = accessor.passScrollToSwipeRefreshChild()
392+
387393
/**
388394
* Enable reporting Performance Issues (`detail.devtools.performanceIssue`). Displayed in the V2 Performance Monitor and the "Performance Issues" sub-panel in DevTools.
389395
*/

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<a0dee36eb68677b468c991297f0e95f7>>
7+
* @generated SignedSource<<2adb48ad6907058ba092af9703994a57>>
88
*/
99

1010
/**
@@ -79,6 +79,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
7979
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
8080
private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null
8181
private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null
82+
private var passScrollToSwipeRefreshChildCache: Boolean? = null
8283
private var perfIssuesEnabledCache: Boolean? = null
8384
private var perfMonitorV2EnabledCache: Boolean? = null
8485
private var preparedTextCacheSizeCache: Double? = null
@@ -630,6 +631,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
630631
return cached
631632
}
632633

634+
override fun passScrollToSwipeRefreshChild(): Boolean {
635+
var cached = passScrollToSwipeRefreshChildCache
636+
if (cached == null) {
637+
cached = ReactNativeFeatureFlagsCxxInterop.passScrollToSwipeRefreshChild()
638+
passScrollToSwipeRefreshChildCache = cached
639+
}
640+
return cached
641+
}
642+
633643
override fun perfIssuesEnabled(): Boolean {
634644
var cached = perfIssuesEnabledCache
635645
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<ebb2bd0f070345e71737dab60a788a88>>
7+
* @generated SignedSource<<54ca4d2bf514ce152b2f566205716aa6>>
88
*/
99

1010
/**
@@ -146,6 +146,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
146146

147147
@DoNotStrip @JvmStatic public external fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean
148148

149+
@DoNotStrip @JvmStatic public external fun passScrollToSwipeRefreshChild(): Boolean
150+
149151
@DoNotStrip @JvmStatic public external fun perfIssuesEnabled(): Boolean
150152

151153
@DoNotStrip @JvmStatic public external fun perfMonitorV2Enabled(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<beceb761f215c0819c27da186138971e>>
7+
* @generated SignedSource<<8f895394bc6263275f42c0bfa10674b4>>
88
*/
99

1010
/**
@@ -141,6 +141,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
141141

142142
override fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean = false
143143

144+
override fun passScrollToSwipeRefreshChild(): Boolean = false
145+
144146
override fun perfIssuesEnabled(): Boolean = false
145147

146148
override fun perfMonitorV2Enabled(): Boolean = false

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<44f52bff0ecd0f26c2173581b09dccce>>
7+
* @generated SignedSource<<45b7473c077eb6fd4c2b46712962ddbd>>
88
*/
99

1010
/**
@@ -83,6 +83,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
8383
private var fuseboxNetworkInspectionEnabledCache: Boolean? = null
8484
private var hideOffscreenVirtualViewsOnIOSCache: Boolean? = null
8585
private var overrideBySynchronousMountPropsAtMountingAndroidCache: Boolean? = null
86+
private var passScrollToSwipeRefreshChildCache: Boolean? = null
8687
private var perfIssuesEnabledCache: Boolean? = null
8788
private var perfMonitorV2EnabledCache: Boolean? = null
8889
private var preparedTextCacheSizeCache: Double? = null
@@ -693,6 +694,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
693694
return cached
694695
}
695696

697+
override fun passScrollToSwipeRefreshChild(): Boolean {
698+
var cached = passScrollToSwipeRefreshChildCache
699+
if (cached == null) {
700+
cached = currentProvider.passScrollToSwipeRefreshChild()
701+
accessedFeatureFlags.add("passScrollToSwipeRefreshChild")
702+
passScrollToSwipeRefreshChildCache = cached
703+
}
704+
return cached
705+
}
706+
696707
override fun perfIssuesEnabled(): Boolean {
697708
var cached = perfIssuesEnabledCache
698709
if (cached == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<2bc1b7c78ced990301722e1c1dcc2dcf>>
7+
* @generated SignedSource<<d50ebf5805d13b4da427ce1318c6cabc>>
88
*/
99

1010
/**
@@ -141,6 +141,8 @@ public interface ReactNativeFeatureFlagsProvider {
141141

142142
@DoNotStrip public fun overrideBySynchronousMountPropsAtMountingAndroid(): Boolean
143143

144+
@DoNotStrip public fun passScrollToSwipeRefreshChild(): Boolean
145+
144146
@DoNotStrip public fun perfIssuesEnabled(): Boolean
145147

146148
@DoNotStrip public fun perfMonitorV2Enabled(): Boolean

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ import android.view.ViewConfiguration
1212
import android.view.ViewGroup
1313
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
1414
import com.facebook.react.bridge.ReactContext
15+
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
1516
import com.facebook.react.uimanager.PixelUtil
1617
import com.facebook.react.uimanager.events.NativeGestureUtil
1718
import kotlin.math.abs
1819

19-
/** Basic extension of [SwipeRefreshLayout] with ReactNative-specific functionality. */
20+
/**
21+
* Basic extension of [SwipeRefreshLayout] with ReactNative-specific functionality.
22+
*
23+
* This component wraps a scrollable child (typically a ScrollView or RecyclerView) and provides
24+
* pull-to-refresh functionality. It handles touch event interception for the refresh gesture while
25+
* properly forwarding other events to its children.
26+
*/
2027
public class ReactSwipeRefreshLayout(reactContext: ReactContext) :
2128
SwipeRefreshLayout(reactContext) {
2229

@@ -127,6 +134,31 @@ public class ReactSwipeRefreshLayout(reactContext: ReactContext) :
127134
return true
128135
}
129136

137+
/**
138+
* Dispatches generic motion events to children.
139+
*
140+
* This override ensures that [MotionEvent.ACTION_SCROLL] events (from joystick, scrollwheel, or
141+
* other pointing devices) are properly forwarded to child views.
142+
*/
143+
public override fun dispatchGenericMotionEvent(ev: MotionEvent): Boolean {
144+
// For ACTION_SCROLL events, dispatch to child for handling
145+
// The child ScrollView will use nested scrolling APIs to communicate with this
146+
// SwipeRefreshLayout
147+
if (
148+
ReactNativeFeatureFlags.passScrollToSwipeRefreshChild() &&
149+
ev.actionMasked == MotionEvent.ACTION_SCROLL
150+
) {
151+
val child = getChildAt(0)
152+
if (child != null) {
153+
val handled = child.dispatchGenericMotionEvent(ev)
154+
if (handled) {
155+
return true
156+
}
157+
}
158+
}
159+
return super.dispatchGenericMotionEvent(ev)
160+
}
161+
130162
private companion object {
131163
private const val DEFAULT_CIRCLE_TARGET = 64f
132164
}

packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<89d63e717ae8634f32613243bcbe2c40>>
7+
* @generated SignedSource<<77d79172d377c62d4d26ddd16dab56ec>>
88
*/
99

1010
/**
@@ -393,6 +393,12 @@ class ReactNativeFeatureFlagsJavaProvider
393393
return method(javaProvider_);
394394
}
395395

396+
bool passScrollToSwipeRefreshChild() override {
397+
static const auto method =
398+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("passScrollToSwipeRefreshChild");
399+
return method(javaProvider_);
400+
}
401+
396402
bool perfIssuesEnabled() override {
397403
static const auto method =
398404
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("perfIssuesEnabled");
@@ -806,6 +812,11 @@ bool JReactNativeFeatureFlagsCxxInterop::overrideBySynchronousMountPropsAtMounti
806812
return ReactNativeFeatureFlags::overrideBySynchronousMountPropsAtMountingAndroid();
807813
}
808814

815+
bool JReactNativeFeatureFlagsCxxInterop::passScrollToSwipeRefreshChild(
816+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
817+
return ReactNativeFeatureFlags::passScrollToSwipeRefreshChild();
818+
}
819+
809820
bool JReactNativeFeatureFlagsCxxInterop::perfIssuesEnabled(
810821
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
811822
return ReactNativeFeatureFlags::perfIssuesEnabled();
@@ -1109,6 +1120,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
11091120
makeNativeMethod(
11101121
"overrideBySynchronousMountPropsAtMountingAndroid",
11111122
JReactNativeFeatureFlagsCxxInterop::overrideBySynchronousMountPropsAtMountingAndroid),
1123+
makeNativeMethod(
1124+
"passScrollToSwipeRefreshChild",
1125+
JReactNativeFeatureFlagsCxxInterop::passScrollToSwipeRefreshChild),
11121126
makeNativeMethod(
11131127
"perfIssuesEnabled",
11141128
JReactNativeFeatureFlagsCxxInterop::perfIssuesEnabled),

packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<1cf1c6d1d2a98a495db315e0e7edcc32>>
7+
* @generated SignedSource<<35b3b4933e2c337c4d924c2cc545eac6>>
88
*/
99

1010
/**
@@ -207,6 +207,9 @@ class JReactNativeFeatureFlagsCxxInterop
207207
static bool overrideBySynchronousMountPropsAtMountingAndroid(
208208
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
209209

210+
static bool passScrollToSwipeRefreshChild(
211+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
212+
210213
static bool perfIssuesEnabled(
211214
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
212215

0 commit comments

Comments
 (0)