Skip to content

Commit 99fca5f

Browse files
jasozhmeta-codesync[bot]
authored andcommitted
Fix TextInput absorbing scroll gestures on Android (#58678)
Summary: Pull Request resolved: #58678 Fixes the issue described in [#25594](#25594) and [#12167](#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](372d001). 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 Reviewed By: Abbondanzo Differential Revision: D121519676 fbshipit-source-id: 23f78c99213dc0c34c2ae81cce11e706f79ec4e5
1 parent eff2413 commit 99fca5f

2 files changed

Lines changed: 46 additions & 11 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import android.view.Menu
3636
import android.view.MenuItem
3737
import android.view.MotionEvent
3838
import android.view.View
39+
import android.view.ViewConfiguration
3940
import android.view.ViewGroup
4041
import android.view.accessibility.AccessibilityNodeInfo
4142
import android.view.inputmethod.EditorInfo
@@ -91,6 +92,7 @@ import com.facebook.react.views.text.internal.span.ReactStrikethroughSpan
9192
import com.facebook.react.views.text.internal.span.ReactTextPaintHolderSpan
9293
import com.facebook.react.views.text.internal.span.ReactUnderlineSpan
9394
import java.util.concurrent.CopyOnWriteArrayList
95+
import kotlin.math.abs
9496
import kotlin.math.max
9597
import kotlin.math.min
9698

@@ -132,6 +134,9 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
132134
private var scrollWatcher: ScrollWatcher?
133135
private var keyListener: InternalKeyListener? = null
134136
private var detectScrollMovement = false
137+
private var touchDownX = 0f
138+
private var touchDownY = 0f
139+
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
135140
private var onKeyPress = false
136141
private val textAttributes: TextAttributes
137142
private var typefaceDirty = false
@@ -316,6 +321,8 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
316321
override fun onTouchEvent(ev: MotionEvent): Boolean {
317322
when (ev.action) {
318323
MotionEvent.ACTION_DOWN -> {
324+
touchDownX = ev.x
325+
touchDownY = ev.y
319326
detectScrollMovement = true
320327
// Disallow parent views to intercept touch events, until we can detect if we should be
321328
// capturing these touches or not.
@@ -324,21 +331,33 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
324331

325332
MotionEvent.ACTION_MOVE ->
326333
if (detectScrollMovement) {
327-
if (
328-
!canScrollVertically(-1) &&
329-
!canScrollVertically(1) &&
330-
!canScrollHorizontally(-1) &&
331-
!canScrollHorizontally(1)
332-
) {
333-
// We cannot scroll, let parent views take care of these touches.
334-
this.parent.requestDisallowInterceptTouchEvent(false)
334+
val deltaX = ev.x - touchDownX
335+
val deltaY = ev.y - touchDownY
336+
val dominantAxisDistance = max(abs(deltaX), abs(deltaY))
337+
if (dominantAxisDistance > touchSlop) {
338+
if (!canScrollInGestureDirection(deltaX, deltaY)) {
339+
// We cannot scroll, let parent views take care of these touches.
340+
this.parent.requestDisallowInterceptTouchEvent(false)
341+
}
342+
detectScrollMovement = false
335343
}
336-
detectScrollMovement = false
337344
}
338345
}
339346
return super.onTouchEvent(ev)
340347
}
341348

349+
private fun canScrollInGestureDirection(deltaX: Float, deltaY: Float): Boolean {
350+
if (abs(deltaY) > abs(deltaX)) {
351+
val canScrollUp = deltaY > 0 && canScrollVertically(-1)
352+
val canScrollDown = deltaY <= 0 && canScrollVertically(1)
353+
return canScrollUp || canScrollDown
354+
} else {
355+
val canScrollLeft = deltaX > 0 && canScrollHorizontally(-1)
356+
val canScrollRight = deltaX <= 0 && canScrollHorizontally(1)
357+
return canScrollLeft || canScrollRight
358+
}
359+
}
360+
342361
// Consume 'Enter' key events: TextView tries to give focus to the next TextInput, but it can't
343362
// since we only allow JS to change focus, which in turn causes TextView to crash.
344363
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {

‎packages/rn-tester/js/examples/ScrollView/ScrollViewExample.js‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ const examples: Array<RNTesterModuleExample> = [
471471
},
472472
},
473473
{
474+
name: 'scrollTextInputsVertical',
474475
title: '<ScrollView> TextInputs\n',
475476
description:
476477
'Vertical scroll gestures on a TextInput should scroll the ScrollView unless the input can also scroll vertically.',
@@ -479,6 +480,7 @@ const examples: Array<RNTesterModuleExample> = [
479480
},
480481
},
481482
{
483+
name: 'scrollTextInputsHorizontal',
482484
title: '<ScrollView> TextInputs (horizontal = true)\n',
483485
description:
484486
'Horizontal scroll gestures on a TextInput should scroll the ScrollView unless the input can also scroll horizontally.',
@@ -634,7 +636,7 @@ const VerticalScrollTextInputs = () => {
634636

635637
return (
636638
<View>
637-
<RNTesterText style={styles.text}>
639+
<RNTesterText style={styles.text} testID="vertical_status">
638640
scrolled = {verticalScrolled ? 'true' : 'false'}
639641
</RNTesterText>
640642
<ScrollView
@@ -645,23 +647,30 @@ const VerticalScrollTextInputs = () => {
645647
onScroll={event => {
646648
setVerticalScrolled(event.nativeEvent.contentOffset.y > 0);
647649
}}>
648-
<TextInput placeholder="TextInput" style={styles.textInput} />
650+
<TextInput
651+
placeholder="TextInput"
652+
style={styles.textInput}
653+
testID="text_input_vertical"
654+
/>
649655
<TextInput
650656
placeholder="TextInput (center)"
651657
style={styles.textInput}
652658
textAlign="center"
659+
testID="text_input_vertical_center"
653660
/>
654661
<TextInput
655662
placeholder="TextInput (right)"
656663
style={styles.textInput}
657664
textAlign="right"
665+
testID="text_input_vertical_right"
658666
/>
659667
<TextInput
660668
defaultValue={
661669
'A TextInput with a value that overflows the input width so it can be scrolled horizontally.'
662670
}
663671
placeholder="TextInput"
664672
style={styles.textInput}
673+
testID="text_input_vertical_overflow_width"
665674
/>
666675
<TextInput
667676
defaultValue={
@@ -670,6 +679,7 @@ const VerticalScrollTextInputs = () => {
670679
placeholder="TextInput"
671680
style={styles.textInput}
672681
multiline
682+
testID="text_input_vertical_overflow_height"
673683
/>
674684
<View
675685
style={[styles.textInput, {backgroundColor: '#cccccc', height: 200}]}
@@ -682,6 +692,7 @@ const VerticalScrollTextInputs = () => {
682692
y: 0,
683693
});
684694
}}
695+
testID="scroll_to_top_button"
685696
/>
686697
</View>
687698
);
@@ -711,18 +722,21 @@ const HorizontalScrollTextInputs = () => {
711722
styles.textInput,
712723
{backgroundColor: '#cccccc', width: 100, height: '100%'},
713724
]}
725+
testID="left_filler"
714726
/>
715727
<View style={{gap: 20}}>
716728
<TextInput
717729
placeholder="TextInput"
718730
style={[styles.textInput, {width: 200}]}
731+
testID="text_input_horizontal"
719732
/>
720733
<TextInput
721734
defaultValue={
722735
'A TextInput with a value that overflows the input width so it can be scrolled horizontally.'
723736
}
724737
placeholder="TextInput"
725738
style={[styles.textInput, {width: 200}]}
739+
testID="text_input_horizontal_overflow_width"
726740
/>
727741
<TextInput
728742
defaultValue={
@@ -731,6 +745,7 @@ const HorizontalScrollTextInputs = () => {
731745
placeholder="TextInput"
732746
style={[styles.textInput, {width: 200}]}
733747
multiline
748+
testID="text_input_horizontal_overflow_height"
734749
/>
735750
</View>
736751
<View
@@ -747,6 +762,7 @@ const HorizontalScrollTextInputs = () => {
747762
x: 0,
748763
});
749764
}}
765+
testID="scroll_to_start_button"
750766
/>
751767
</View>
752768
);

0 commit comments

Comments
 (0)