From cb7af02cc7fb3173c43422635ac84e85cac88170 Mon Sep 17 00:00:00 2001 From: Jason Zheng Date: Thu, 24 Sep 2026 18:01:52 -0700 Subject: [PATCH] Fix TextInput absorbing scroll gestures on Android Summary: Fixes the issue described in #25594 and #12167, where TextInputs on Android absorb scroll gestures when textAlign is set to "center" or "right". For TextInputs inside a ScrollView, starting a scroll gesture on the TextInput would not scroll the ScrollView. ### Root Cause The root cause is the `onTouchEvent()` function introduced in [commit 372d001](https://github.com/react/react-native/commit/372d001a5d5e24b68d194e01c356d973642fac86). The smart scroll implementation checks if the TextInput can itself scroll in any direction. If it cannot, then the parent can intercept the gesture to apply normal scroll behavior. However, when textAlign is set to "center" or "right", `canScrollHorizontally()` returns true, as Android treats the TextInput as capable of scrolling left/right. In these cases, the existing conditional logic blocks all scroll gestures, even if a vertical swipe is applied. ### This PR Modify `ReactEditText` so that we only check to see if the TextInput can scroll in the direction of the provided touch gesture. If a TextInput can scroll horizontally but not vertically, vertical touch swipes will no longer interfere with the scroll gesture. Touch slop is also used to distinguish intentional swipe gestures from small micro-touch events. Changelog: [Android][Fixed] Prevent TextInput from preventing scroll gestures Differential Revision: D121519676 --- .../react/views/textinput/ReactEditText.kt | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index 754f10026624..2bed60a1e6a1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -36,6 +36,7 @@ import android.view.Menu import android.view.MenuItem import android.view.MotionEvent import android.view.View +import android.view.ViewConfiguration import android.view.ViewGroup import android.view.accessibility.AccessibilityNodeInfo import android.view.inputmethod.EditorInfo @@ -91,6 +92,7 @@ import com.facebook.react.views.text.internal.span.ReactStrikethroughSpan import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan import com.facebook.react.views.text.internal.span.ReactUnderlineSpan import java.util.concurrent.CopyOnWriteArrayList +import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -132,6 +134,9 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private var scrollWatcher: ScrollWatcher? private var keyListener: InternalKeyListener? = null private var detectScrollMovement = false + private var touchDownX = 0f + private var touchDownY = 0f + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop private var onKeyPress = false private val textAttributes: TextAttributes private var typefaceDirty = false @@ -316,6 +321,8 @@ public open class ReactEditText public constructor(context: Context) : AppCompat override fun onTouchEvent(ev: MotionEvent): Boolean { when (ev.action) { MotionEvent.ACTION_DOWN -> { + touchDownX = ev.x + touchDownY = ev.y detectScrollMovement = true // Disallow parent views to intercept touch events, until we can detect if we should be // capturing these touches or not. @@ -324,21 +331,33 @@ public open class ReactEditText public constructor(context: Context) : AppCompat MotionEvent.ACTION_MOVE -> if (detectScrollMovement) { - if ( - !canScrollVertically(-1) && - !canScrollVertically(1) && - !canScrollHorizontally(-1) && - !canScrollHorizontally(1) - ) { - // We cannot scroll, let parent views take care of these touches. - this.parent.requestDisallowInterceptTouchEvent(false) + val deltaX = ev.x - touchDownX + val deltaY = ev.y - touchDownY + val dominantAxisDistance = max(abs(deltaX), abs(deltaY)) + if (dominantAxisDistance > touchSlop) { + if (!canScrollInGestureDirection(deltaX, deltaY)) { + // We cannot scroll, let parent views take care of these touches. + this.parent.requestDisallowInterceptTouchEvent(false) + } + detectScrollMovement = false } - detectScrollMovement = false } } return super.onTouchEvent(ev) } + private fun canScrollInGestureDirection(deltaX: Float, deltaY: Float): Boolean { + if (abs(deltaY) > abs(deltaX)) { + val canScrollUp = deltaY > 0 && canScrollVertically(-1) + val canScrollDown = deltaY <= 0 && canScrollVertically(1) + return canScrollUp || canScrollDown + } else { + val canScrollLeft = deltaX > 0 && canScrollHorizontally(-1) + val canScrollRight = deltaX <= 0 && canScrollHorizontally(1) + return canScrollLeft || canScrollRight + } + } + // Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't // since we only allow JS to change focus, which in turn causes TextView to crash. override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {