Delegate pointer events from TextFieldDecoratorModifierNode
Many complex pointer event handlers use PointerInputScope. We are migrating TextFieldDecoratorModifier to `SuspendingPointerInputModifierNode`, using delegation to take advantage of existing node implementation with minimal effect on our exiting `Modifier.Node`.
`detectTapAndPress` only consumes press and tap events which would make it less annoying to put `BasicTextField2` in a scrollable container.
Bug: b/277380808
Test: BasicTextField2Test
Change-Id: I82783dd967c186c0147eface0698258000cb840d
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
index a6277b5..e74c175 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
@@ -20,7 +20,11 @@
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardHelper
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text2.input.TextEditFilter
@@ -30,6 +34,7 @@
import androidx.compose.foundation.text2.input.TextFieldState
import androidx.compose.foundation.text2.input.internal.AndroidTextInputAdapter
import androidx.compose.foundation.text2.input.rememberTextFieldState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -55,7 +60,10 @@
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTextInputSelection
import androidx.compose.ui.test.performTextReplacement
+import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
@@ -63,6 +71,7 @@
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -894,6 +903,43 @@
assertThat(keyboardHelper.isSoftwareKeyboardShown()).isTrue()
}
+ @Test
+ fun swipingThroughTextField_doesNotGainFocus() {
+ rule.setContent {
+ BasicTextField2(
+ state = rememberTextFieldState(),
+ modifier = Modifier.testTag(Tag)
+ )
+ }
+
+ rule.onNodeWithTag(Tag).performTouchInput {
+ // swipe through
+ swipeRight(endX = right + 200, durationMillis = 1000)
+ }
+ rule.onNodeWithTag(Tag).assertIsNotFocused()
+ }
+
+ @Test
+ fun swipingTextFieldInScrollableContainer_doesNotGainFocus() {
+ val scrollState = ScrollState(0)
+ rule.setContent {
+ Column(Modifier.size(100.dp).verticalScroll(scrollState)) {
+ BasicTextField2(
+ state = rememberTextFieldState(),
+ modifier = Modifier.testTag(Tag)
+ )
+ Box(Modifier.size(200.dp))
+ }
+ }
+
+ rule.onNodeWithTag(Tag).performTouchInput {
+ // swipe through
+ swipeUp(durationMillis = 1000)
+ }
+ rule.onNodeWithTag(Tag).assertIsNotFocused()
+ assertThat(scrollState.value).isNotEqualTo(0)
+ }
+
private fun requestFocus(tag: String) =
rule.onNodeWithTag(tag).performSemanticsAction(SemanticsActions.RequestFocus)
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt
index 87fcc23..d31e2d8 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/internal/TextFieldDecoratorModifier.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text2.input.internal
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.detectTapAndPress
import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -25,7 +26,6 @@
import androidx.compose.foundation.text2.input.TextFieldCharSequence
import androidx.compose.foundation.text2.input.TextFieldState
import androidx.compose.foundation.text2.input.deselect
-import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusEventModifierNode
import androidx.compose.ui.focus.FocusManager
@@ -36,9 +36,10 @@
import androidx.compose.ui.input.key.KeyInputModifierNode
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.changedToDown
+import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.GlobalPositionAwareModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.PointerInputModifierNode
@@ -63,7 +64,6 @@
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastAny
/**
* Modifier element for most of the functionality of [BasicTextField2] that is attached to the
@@ -128,7 +128,7 @@
keyboardOptions: KeyboardOptions,
var keyboardActions: KeyboardActions,
var singleLine: Boolean,
-) : Modifier.Node(),
+) : DelegatingNode(),
SemanticsModifierNode,
FocusRequesterModifierNode,
FocusEventModifierNode,
@@ -137,6 +137,19 @@
KeyInputModifierNode,
CompositionLocalConsumerModifierNode {
+ private val pointerInputNode = SuspendingPointerInputModifierNode {
+ detectTapAndPress(onTap = {
+ if (!isFocused) {
+ requestFocus()
+ } else if (enabled && !readOnly) {
+ textInputSession?.showSoftwareKeyboard()
+ }
+ })
+ }
+ // TODO: remove `.node` after aosp/2462416 lands and merge everything into one delegated
+ // block
+ .also { delegated { it.node } }
+
var keyboardOptions: KeyboardOptions = keyboardOptions.withDefaultsFrom(filter?.keyboardOptions)
private set
@@ -302,17 +315,11 @@
pass: PointerEventPass,
bounds: IntSize
) {
- if (pass == PointerEventPass.Main && pointerEvent.changes.fastAny { it.changedToDown() }) {
- if (!isFocused) {
- requestFocus()
- } else if (enabled && !readOnly) {
- textInputSession?.showSoftwareKeyboard()
- }
- }
+ pointerInputNode.onPointerEvent(pointerEvent, pass, bounds)
}
override fun onCancelPointerInput() {
- // Nothing to do yet, since onPointerEvent isn't handling any gestures.
+ pointerInputNode.onCancelPointerInput()
}
override fun onPreKeyEvent(event: KeyEvent): Boolean {