Skip to content

Commit eb68505

Browse files
jasozhfacebook-github-bot
authored andcommitted
Fix TextInput breaking nested scroll gestures on Android
Summary: Fixes an Android-only bug where TextInputs can interfere with nested scroll gestures, causing scroll events to be performed on the outer parent instead of the inner ScrollView. ### Root Cause This issue occurs when you have a TextInput inside an inner ScrollView with `nestedScrollEnabled = true` inside an outer ScrollView. When a scroll gesture is performed on a TextInput that can scroll, the TextInput intercepts the gesture as expected. However, the next scroll gesture applied anywhere in the inner ScrollView scrolls the outer parent instead of the inner ScrollView. `inner.onInterceptTouchEvent()` calls `startNestedScroll()` on ACTION_DOWN and `stopNestedScroll()` on ACTION_UP. When the scroll gesture is on a scrollable TextInput, ReactEditText disallows parent views from intercepting touch events. This means on ACTION_UP, `inner.onInterceptTouchEvent()` and `stopNestedScroll()` is not triggered. Without `stopNestedScroll()`, the subsequent `startNestedScroll()` on the next ACTION_DOWN becomes a no-op, and the outer ScrollView intercepts the scroll gesture instead of the inner ScrollView. ### This PR Add a defensive `stopNestedScroll()` in `dispatchTouchEvent()` for ScrollViews. This matches the existing pattern already used by Android in `View.dispatchTouchEvent()` which includes the exact same defensive cleanup. Changelog: [Android][Fixed] Prevent TextInputs from interfering with nested scroll gestures Differential Revision: D122404118
1 parent 3d542f5 commit eb68505

5 files changed

Lines changed: 40 additions & 12 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5476,6 +5476,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android
54765476
public fun arrowScroll (I)Z
54775477
public fun canScrollHorizontally (I)Z
54785478
public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z
5479+
public fun dispatchTouchEvent (Landroid/view/MotionEvent;)Z
54795480
public fun draw (Landroid/graphics/Canvas;)V
54805481
public fun executeKeyEvent (Landroid/view/KeyEvent;)Z
54815482
public fun flashScrollIndicators ()V
@@ -5624,6 +5625,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc
56245625
public synthetic fun <init> (Landroid/content/Context;Lcom/facebook/react/views/scroll/FpsListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
56255626
public fun abortAnimation ()V
56265627
public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z
5628+
public fun dispatchTouchEvent (Landroid/view/MotionEvent;)Z
56275629
public fun draw (Landroid/graphics/Canvas;)V
56285630
public fun executeKeyEvent (Landroid/view/KeyEvent;)Z
56295631
public fun flashScrollIndicators ()V

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,14 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
645645
override fun shouldDelayChildPressedState(): Boolean =
646646
hasChildPressedStateDelay ?: super.shouldDelayChildPressedState()
647647

648+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
649+
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
650+
// Defensive cleanup for new gesture
651+
stopNestedScroll()
652+
}
653+
return super.dispatchTouchEvent(ev)
654+
}
655+
648656
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
649657
if (!scrollEnabled) return false
650658

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt‎

Lines changed: 9 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<<2f9a2ea0c223b08d9d1ea4517fd9671b>>
7+
* @generated SignedSource<<a32e8eb823c728bdb2200b163b82efe3>>
88
*/
99

1010
/**
@@ -565,6 +565,14 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
565565
override fun shouldDelayChildPressedState(): Boolean =
566566
hasChildPressedStateDelay ?: super.shouldDelayChildPressedState()
567567

568+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
569+
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
570+
// Defensive cleanup for new gesture
571+
stopNestedScroll()
572+
}
573+
return super.dispatchTouchEvent(ev)
574+
}
575+
568576
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
569577
if (!scrollEnabled) return false
570578
if (!PointerEvents.canChildrenBeTouchTarget(pointerEvents)) return true

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt‎

Lines changed: 13 additions & 11 deletions
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<<3b339a7d81f8e32cf6b6ca93ae9a388b>>
7+
* @generated SignedSource<<acbc1716191446a5c496c66dfa368b52>>
88
*/
99

1010
/**
@@ -461,15 +461,17 @@ constructor(private val fpsListener: FpsListener? = null) :
461461
companion object {
462462
const val REACT_CLASS: String = "RCTScrollView"
463463

464-
fun createExportedCustomDirectEventTypeConstants(): Map<String, Any> = mapOf(
465-
getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"),
466-
getJSEventName(ScrollEventType.BEGIN_DRAG) to
467-
mapOf("registrationName" to "onScrollBeginDrag"),
468-
getJSEventName(ScrollEventType.END_DRAG) to mapOf("registrationName" to "onScrollEndDrag"),
469-
getJSEventName(ScrollEventType.MOMENTUM_BEGIN) to
470-
mapOf("registrationName" to "onMomentumScrollBegin"),
471-
getJSEventName(ScrollEventType.MOMENTUM_END) to
472-
mapOf("registrationName" to "onMomentumScrollEnd"),
473-
)
464+
fun createExportedCustomDirectEventTypeConstants(): Map<String, Any> =
465+
mapOf(
466+
getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"),
467+
getJSEventName(ScrollEventType.BEGIN_DRAG) to
468+
mapOf("registrationName" to "onScrollBeginDrag"),
469+
getJSEventName(ScrollEventType.END_DRAG) to
470+
mapOf("registrationName" to "onScrollEndDrag"),
471+
getJSEventName(ScrollEventType.MOMENTUM_BEGIN) to
472+
mapOf("registrationName" to "onMomentumScrollBegin"),
473+
getJSEventName(ScrollEventType.MOMENTUM_END) to
474+
mapOf("registrationName" to "onMomentumScrollEnd"),
475+
)
474476
}
475477
}

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,14 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) :
557557
override fun shouldDelayChildPressedState(): Boolean =
558558
hasChildPressedStateDelay ?: super.shouldDelayChildPressedState()
559559

560+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
561+
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
562+
// Defensive cleanup for new gesture
563+
stopNestedScroll()
564+
}
565+
return super.dispatchTouchEvent(ev)
566+
}
567+
560568
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
561569
if (!scrollEnabled) return false
562570
if (!PointerEvents.canChildrenBeTouchTarget(pointerEvents)) return true

0 commit comments

Comments
 (0)