From dd78d2f933d0d8a075ff660e87e97c69b56cf954 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 25 Sep 2025 13:29:58 -0700 Subject: [PATCH] Add responderIgnoreScroll prop on ScrollView (#53951) Summary: ## Changelog: [Android] [Changed] - Add responderIgnoreScroll prop on ScrollView When it's set to true (default), emit touchcancel from native Android ScrollView, instead of letting Responder System terminate responder at scroll. When false, `onTouchCancel` on ScrollView children will not be invoked, but `onTouchEnd` will be invoked (if touch starts on it) - which aligns with iOS. Allow opting in this new behavior at component level because (1) touchCancel is meaningful especially on android, and dev may be using onTouchCancel handler, if we enable this everywhere the handler is broken, (2) there's likely some issue in responder system when transferring js responder Differential Revision: D82761242 --- ...roidHorizontalScrollViewNativeComponent.js | 1 + .../Components/ScrollView/ScrollView.d.ts | 13 ++++++++++++ .../Components/ScrollView/ScrollView.js | 13 ++++++++++++ .../ScrollView/ScrollViewNativeComponent.js | 1 + .../ScrollViewNativeComponentType.js | 1 + .../scroll/ReactHorizontalScrollView.java | 10 ++++++++++ .../ReactHorizontalScrollViewManager.kt | 8 ++++++++ .../react/views/scroll/ReactScrollView.java | 10 ++++++++++ .../views/scroll/ReactScrollViewManager.kt | 5 +++++ .../HostPlatformScrollViewProps.cpp | 20 ++++++++++++++++++- .../scrollview/HostPlatformScrollViewProps.h | 1 + packages/react-native/ReactNativeApi.d.ts | 20 ++++++++++--------- 12 files changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js b/packages/react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js index 7f2179f5fde9..f2b9ad332317 100644 --- a/packages/react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js @@ -59,6 +59,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { process: require('../../StyleSheet/processColor').default, }, pointerEvents: true, + responderIgnoreScroll: true, }, }; diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts b/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts index b6e538ddc5a1..487840cc8b57 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts @@ -586,6 +586,19 @@ export interface ScrollViewPropsAndroid { * Causes the scrollbars not to turn transparent when they are not in use. The default value is false. */ persistentScrollbar?: boolean | undefined; + + /** + * Emit touchcancel from native Android ScrollView, instead of letting Responder System + * terminate responder at scroll. + * + * When it's set to false, `onTouchCancel` on ScrollView children will not be invoked, but + * `onTouchEnd` will be invoked (if touch starts on it) - which aligns with iOS. + * + * The default value is true. + * + * @platform android + */ + responderIgnoreScroll?: boolean | undefined; } export interface ScrollViewProps diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js index fdb830dc6e62..afca032702c4 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollView.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollView.js @@ -388,6 +388,19 @@ export type ScrollViewPropsAndroid = $ReadOnly<{ * @platform android */ fadingEdgeLength?: ?number | {start: number, end: number}, + + /** + * Emit touchcancel from native Android ScrollView, instead of letting Responder System + * terminate responder at scroll. + * + * When it's set to false, `onTouchCancel` on ScrollView children will not be invoked, but + * `onTouchEnd` will be invoked (if touch starts on it) - which aligns with iOS. + * + * The default value is true. + * + * @platform android + */ + responderIgnoreScroll?: ?boolean, }>; type StickyHeaderComponentType = component( diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js index 82f9c93baf34..9aa8b2201fb4 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js @@ -88,6 +88,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = }, pointerEvents: true, isInvertedVirtualizedList: true, + responderIgnoreScroll: true, }, } : { diff --git a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js index 1feee6999541..8b84bea268f3 100644 --- a/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +++ b/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js @@ -63,6 +63,7 @@ export type ScrollViewNativeProps = $ReadOnly<{ pagingEnabled?: ?boolean, persistentScrollbar?: ?boolean, pinchGestureEnabled?: ?boolean, + responderIgnoreScroll?: ?boolean, scrollEnabled?: ?boolean, scrollEventThrottle?: ?number, scrollIndicatorInsets?: ?EdgeInsetsProp, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index 63491c9d1c50..498ea4b1c575 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -133,6 +133,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper; private int mFadingEdgeLengthStart = 0; private int mFadingEdgeLengthEnd = 0; + private boolean mResponderIgnoreScroll = true; public ReactHorizontalScrollView(Context context) { this(context, null); @@ -196,6 +197,7 @@ private void initView() { mMaintainVisibleContentPositionHelper = null; mFadingEdgeLengthStart = 0; mFadingEdgeLengthEnd = 0; + mResponderIgnoreScroll = true; } /* package */ void recycleView() { @@ -1722,4 +1724,12 @@ public void setLastScrollDispatchTime(long lastScrollDispatchTime) { public long getLastScrollDispatchTime() { return mLastScrollDispatchTime; } + + public void setResponderIgnoreScroll(boolean responderIgnoreScroll) { + mResponderIgnoreScroll = responderIgnoreScroll; + } + + public boolean getResponderIgnoreScroll() { + return mResponderIgnoreScroll; + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt index 5a364167cf7e..b9e31282e788 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt @@ -413,6 +413,14 @@ constructor(private val fpsListener: FpsListener? = null) : // Do Nothing: Align with static ViewConfigs } + @ReactProp(name = "responderIgnoreScroll") + public fun setResponderIgnoreScroll( + view: ReactHorizontalScrollView, + responderIgnoreScroll: Boolean, + ) { + view.responderIgnoreScroll = responderIgnoreScroll + } + public companion object { public const val REACT_CLASS: String = "AndroidHorizontalScrollView" } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index d98ba44be579..3c3bd3b912bd 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -131,6 +131,7 @@ public class ReactScrollView extends ScrollView private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper; private int mFadingEdgeLengthStart; private int mFadingEdgeLengthEnd; + private boolean mResponderIgnoreScroll; public ReactScrollView(Context context) { this(context, null); @@ -193,6 +194,7 @@ private void initView() { mMaintainVisibleContentPositionHelper = null; mFadingEdgeLengthStart = 0; mFadingEdgeLengthEnd = 0; + mResponderIgnoreScroll = true; } /* package */ void recycleView() { @@ -1527,4 +1529,12 @@ public void setLastScrollDispatchTime(long lastScrollDispatchTime) { public long getLastScrollDispatchTime() { return mLastScrollDispatchTime; } + + public void setResponderIgnoreScroll(boolean responderIgnoreScroll) { + mResponderIgnoreScroll = responderIgnoreScroll; + } + + public boolean getResponderIgnoreScroll() { + return mResponderIgnoreScroll; + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt index 32f017cdd2ae..889fa331ba5b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt @@ -428,6 +428,11 @@ constructor(private val fpsListener: FpsListener? = null) : } } + @ReactProp(name = "responderIgnoreScroll") + public fun setResponderIgnoreScroll(view: ReactScrollView, responderIgnoreScroll: Boolean) { + view.responderIgnoreScroll = responderIgnoreScroll + } + public companion object { public const val REACT_CLASS: String = "RCTScrollView" diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp index 802ef8265a7b..ab37d0a40bc0 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp @@ -38,6 +38,15 @@ HostPlatformScrollViewProps::HostPlatformScrollViewProps( rawProps, "nestedScrollEnabled", sourceProps.nestedScrollEnabled, + true)), + responderIgnoreScroll( + ReactNativeFeatureFlags::enableCppPropsIteratorSetter() + ? sourceProps.responderIgnoreScroll + : convertRawProp( + context, + rawProps, + "responderIgnoreScroll", + sourceProps.responderIgnoreScroll, true)) {} @@ -57,6 +66,7 @@ void HostPlatformScrollViewProps::setProp( switch (hash) { RAW_SET_PROP_SWITCH_CASE_BASIC(sendMomentumEvents); RAW_SET_PROP_SWITCH_CASE_BASIC(nestedScrollEnabled); + RAW_SET_PROP_SWITCH_CASE_BASIC(responderIgnoreScroll); } } @@ -76,7 +86,11 @@ SharedDebugStringConvertibleList HostPlatformScrollViewProps::getDebugProps() debugStringConvertibleItem( "nestedScrollEnabled", nestedScrollEnabled, - defaultScrollViewProps.nestedScrollEnabled)}; + defaultScrollViewProps.nestedScrollEnabled), + debugStringConvertibleItem( + "responderIgnoreScroll", + responderIgnoreScroll, + defaultScrollViewProps.responderIgnoreScroll)}; } #endif @@ -348,6 +362,10 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps( result["nestedScrollEnabled"] = nestedScrollEnabled; } + if (responderIgnoreScroll != oldProps->responderIgnoreScroll) { + result["responderIgnoreScroll"] = responderIgnoreScroll; + } + return result; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h index 4c54decb9399..fbea4bbc7f00 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h @@ -32,6 +32,7 @@ class HostPlatformScrollViewProps final : public BaseScrollViewProps { bool sendMomentumEvents{}; bool nestedScrollEnabled{}; + bool responderIgnoreScroll{}; #pragma mark - DebugStringConvertible diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 7771f1d4193c..f4ef4835b363 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<575e58047a8edf6bbc016768775a258f>> + * @generated SignedSource<<339a44ddc978dd30410a496c72abd5d5>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -4452,6 +4452,7 @@ declare type ScrollViewNativeProps = Readonly< pagingEnabled?: boolean persistentScrollbar?: boolean pinchGestureEnabled?: boolean + responderIgnoreScroll?: boolean scrollEnabled?: boolean scrollEventThrottle?: number scrollIndicatorInsets?: EdgeInsetsProp @@ -4484,6 +4485,7 @@ declare type ScrollViewPropsAndroid = { readonly nestedScrollEnabled?: boolean readonly overScrollMode?: "always" | "auto" | "never" readonly persistentScrollbar?: boolean + readonly responderIgnoreScroll?: boolean readonly scrollPerfTag?: string } declare type ScrollViewPropsIOS = { @@ -5967,8 +5969,8 @@ export { EventSubscription, // b8d084aa ExtendedExceptionData, // 5a6ccf5a FilterFunction, // bf24c0e3 - FlatList, // cbb48cbe - FlatListProps, // 451be810 + FlatList, // 7d957603 + FlatListProps, // ab1d8dcf FocusEvent, // 529b43eb FontVariant, // 7c7558bb GestureResponderEvent, // b466f6d6 @@ -6101,14 +6103,14 @@ export { ScrollToLocationParamsType, // d7ecdad1 ScrollView, // 7fb7c469 ScrollViewImperativeMethods, // eb20aa46 - ScrollViewProps, // 27986ff5 - ScrollViewPropsAndroid, // 84e2134b + ScrollViewProps, // 7b402e77 + ScrollViewPropsAndroid, // 50083685 ScrollViewPropsIOS, // d83c9733 ScrollViewScrollToOptions, // 3313411e SectionBase, // b376bddc - SectionList, // ff1193b2 + SectionList, // 4c2799db SectionListData, // 119baf83 - SectionListProps, // c9ac8e07 + SectionListProps, // 85e8c6ff SectionListRenderItem, // 1fad0435 SectionListRenderItemInfo, // 745e1992 Separators, // 6a45f7e3 @@ -6173,9 +6175,9 @@ export { ViewStyle, // c2db0e6e VirtualViewMode, // 85a69ef6 VirtualizedList, // 4d513939 - VirtualizedListProps, // a99d36db + VirtualizedListProps, // 632e6d18 VirtualizedSectionList, // 446ba0df - VirtualizedSectionListProps, // 6cd4b378 + VirtualizedSectionListProps, // c5101563 WrapperComponentProvider, // 9cf3844c codegenNativeCommands, // e16d62f7 codegenNativeComponent, // ed4c8103