diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 29a04dfbfc07..13665d1d2f2d 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5476,6 +5476,7 @@ public class com/facebook/react/views/scroll/ReactHorizontalScrollView : android public fun arrowScroll (I)Z public fun canScrollHorizontally (I)Z public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z + public fun dispatchTouchEvent (Landroid/view/MotionEvent;)Z public fun draw (Landroid/graphics/Canvas;)V public fun executeKeyEvent (Landroid/view/KeyEvent;)Z public fun flashScrollIndicators ()V @@ -5624,6 +5625,7 @@ public class com/facebook/react/views/scroll/ReactScrollView : android/widget/Sc public synthetic fun (Landroid/content/Context;Lcom/facebook/react/views/scroll/FpsListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun abortAnimation ()V public fun dispatchGenericMotionEvent (Landroid/view/MotionEvent;)Z + public fun dispatchTouchEvent (Landroid/view/MotionEvent;)Z public fun draw (Landroid/graphics/Canvas;)V public fun executeKeyEvent (Landroid/view/KeyEvent;)Z public fun flashScrollIndicators ()V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt index 1bfb84f45368..bdc4922457a7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt @@ -645,6 +645,14 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : override fun shouldDelayChildPressedState(): Boolean = hasChildPressedStateDelay ?: super.shouldDelayChildPressedState() + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (ev.actionMasked == MotionEvent.ACTION_DOWN) { + // Defensive cleanup for new gesture + stopNestedScroll() + } + return super.dispatchTouchEvent(ev) + } + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { if (!scrollEnabled) return false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt index 45fee3cd19d1..98a1de5368da 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt @@ -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<<2f9a2ea0c223b08d9d1ea4517fd9671b>> + * @generated SignedSource<> */ /** @@ -565,6 +565,14 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : override fun shouldDelayChildPressedState(): Boolean = hasChildPressedStateDelay ?: super.shouldDelayChildPressedState() + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (ev.actionMasked == MotionEvent.ACTION_DOWN) { + // Defensive cleanup for new gesture + stopNestedScroll() + } + return super.dispatchTouchEvent(ev) + } + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { if (!scrollEnabled) return false if (!PointerEvents.canChildrenBeTouchTarget(pointerEvents)) return true diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt index 50ec11cbe1e1..632c1d5c73f3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollViewManager.kt @@ -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<<3b339a7d81f8e32cf6b6ca93ae9a388b>> + * @generated SignedSource<> */ /** @@ -461,15 +461,17 @@ constructor(private val fpsListener: FpsListener? = null) : companion object { const val REACT_CLASS: String = "RCTScrollView" - fun createExportedCustomDirectEventTypeConstants(): Map = mapOf( - getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"), - getJSEventName(ScrollEventType.BEGIN_DRAG) to - mapOf("registrationName" to "onScrollBeginDrag"), - getJSEventName(ScrollEventType.END_DRAG) to mapOf("registrationName" to "onScrollEndDrag"), - getJSEventName(ScrollEventType.MOMENTUM_BEGIN) to - mapOf("registrationName" to "onMomentumScrollBegin"), - getJSEventName(ScrollEventType.MOMENTUM_END) to - mapOf("registrationName" to "onMomentumScrollEnd"), - ) + fun createExportedCustomDirectEventTypeConstants(): Map = + mapOf( + getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"), + getJSEventName(ScrollEventType.BEGIN_DRAG) to + mapOf("registrationName" to "onScrollBeginDrag"), + getJSEventName(ScrollEventType.END_DRAG) to + mapOf("registrationName" to "onScrollEndDrag"), + getJSEventName(ScrollEventType.MOMENTUM_BEGIN) to + mapOf("registrationName" to "onMomentumScrollBegin"), + getJSEventName(ScrollEventType.MOMENTUM_END) to + mapOf("registrationName" to "onMomentumScrollEnd"), + ) } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt index b553f6af997d..9e98f34feeb1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt @@ -557,6 +557,14 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : override fun shouldDelayChildPressedState(): Boolean = hasChildPressedStateDelay ?: super.shouldDelayChildPressedState() + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + if (ev.actionMasked == MotionEvent.ACTION_DOWN) { + // Defensive cleanup for new gesture + stopNestedScroll() + } + return super.dispatchTouchEvent(ev) + } + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { if (!scrollEnabled) return false if (!PointerEvents.canChildrenBeTouchTarget(pointerEvents)) return true 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 { diff --git a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js index ef937f99089c..72580d9c0485 100644 --- a/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js +++ b/packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js @@ -470,6 +470,24 @@ const examples: Array = [ return ; }, }, + { + name: 'scrollTextInputsVertical', + title: ' TextInputs\n', + description: + 'Vertical scroll gestures on a TextInput should scroll the ScrollView unless the input can also scroll vertically.', + render(): React.Node { + return ; + }, + }, + { + name: 'scrollTextInputsHorizontal', + title: ' TextInputs (horizontal = true)\n', + description: + 'Horizontal scroll gestures on a TextInput should scroll the ScrollView unless the input can also scroll horizontally.', + render(): React.Node { + return ; + }, + }, ]; if (Platform.OS === 'ios') { @@ -611,6 +629,145 @@ const ScrollsChildToFocusExample = () => { ); }; +const VerticalScrollTextInputs = () => { + const _verticalScrollView = + useRef>(null); + const [verticalScrolled, setVerticalScrolled] = useState(false); + + return ( + + + scrolled = {verticalScrolled ? 'true' : 'false'} + + { + setVerticalScrolled(event.nativeEvent.contentOffset.y > 0); + }}> + + + + + + + +