在Compose TextField中检测点击事件

15

我正在尝试为Compose TextField实现一个onClick处理程序。当前功能是处理TextField的单击事件,但禁用手动编辑字段。对于我的用例,我想处理点击并执行其他操作。我希望保留TextField的外观和感觉,并希望焦点动画也发生。

readOnly属性从用户体验角度给了我想要的结果,但当我单击TextField时,onClick处理程序不会被调用。

TextField(
  value = text,
  onValueChange = { text = it},
  readOnly = true,
  modifier = Modifier
    .clickable(onClick = {
      Log.i("TextField", "Clicked")
    })
)

我也尝试使用pointerInput,但是我遇到了同样的问题。

TextField(
  value = text,
  onValueChange = { text = it},
  readOnly = true,
  modifier = Modifier
    .pointerInput(Unit) {
      detectTapGestures(onTap = {
        Log.i("TextField", "Clicked")
      }
    }
)

由于Compose相对较新,很难确定这是一个错误还是故意的。

6个回答

18

也许在文本字段中检测点击事件的更好方法是使用交互源。

步骤1:- 创建交互源

 val source = remember {
    MutableInteractionSource()
}

步骤二:将其传递到文本字段

 OutlinedTextField(
    value = text,
    readOnly = true,
    onValueChange = {},
    textStyle = MaterialTheme.typography.body1.copy(
        lineHeight = if (isFocused) 25.sp else TextUnit.Unspecified,
        fontWeight = if (isFocused) FontWeight.SemiBold else FontWeight.Normal
    ),
    label = {
        Text(
            text = label,
            fontWeight = if (isFocused) FontWeight.SemiBold else FontWeight.Normal,
            style = MaterialTheme.typography.caption
        )
    },
    interactionSource = source,
    colors = TextFieldDefaults.outlinedTextFieldColors(
        textColor = if (isFocused) MaterialTheme.colors.primary else LocalContentColor.current
    ),
    maxLines = if (isFocused) Int.MAX_VALUE else 2,
    modifier = Modifier
        .padding(
            start = COUNTER_WIDTH + 16.dp,
            top = padding,
            bottom = padding,
            end = 16.dp
        )
        .fillMaxWidth()
        .verticalScroll(enabled = isFocused, state = rememberScrollState())
        .animateContentSize(animationSpec = tween(DURATION))
)

步骤三:将按下的流量收集为状态并观察其变化。

 if ( source.collectIsPressedAsState().value)
        onClicked()

1
在我看来,这似乎是最准确的答案,没有任何副作用或折衷方案。 - rewgoes
5
@rewgoes 我认为存在副作用 https://dev59.com/iFEG5IYBdhLWcg3wWbBH#73740797 - Farrukh Tulkunov
1
@rewgoes 请查看 https://dev59.com/iFEG5IYBdhLWcg3wWbBH#73740797 - ino
1
这不太好,它会在你轻触字段时立即触发,甚至不会移开手指。 - user924

15

使用 readOnly = true 属性时,TextField 仍然可用,并且第一次点击会使其获得焦点。您需要执行双击才能处理你的 onClick 函数。

否则,您可以使用 enabled 属性:

   TextField(
        enabled = false,
        modifier = Modifier.clickable(onClick = {/* ... */})
    )

带有以下类似的内容:

TextField(
    value = text,
    onValueChange = { text = it},
    enabled = false,
    modifier = Modifier
        .clickable { text= "Clicked"},
    colors = TextFieldDefaults.textFieldColors(
        disabledTextColor = LocalContentColor.current.copy(LocalContentAlpha.current),
        disabledLabelColor =  MaterialTheme.colors.onSurface.copy(ContentAlpha.medium)
    )
)

enter image description here


听起来我只需要想办法将文本和标签颜色恢复为非禁用状态。 - lostintranslation
1
@lostintranslation 是的,我已经更新了答案,并提供了一个示例,将 disabledTextColordisabledLabelColor 恢复为默认的非禁用颜色。 - Gabriele Mariotti
谢谢。出于好奇,是否有一种方法可以附加自己的点击处理程序,在使用readOnly时也能正常工作?我试图走这条路,但没有取得太大的成功。 - lostintranslation
@lostintranslation 抱歉,但我只能通过在该字段上双击来使用您的代码。 - Gabriele Mariotti

10
正确的答案是使用InteractionSourceinteractions而不是collectIsPressedAsState!后者只检查您何时轻敲某个东西,但它忽略了您甚至没有释放手指的事实。
TextField(
    value = ...,
    onValueChange = { ... },
    interactionSource = remember { MutableInteractionSource() }
        .also { interactionSource ->
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collect {
                    if (it is PressInteraction.Release) {
                        // works like onClick
                    }
                }
            }
        }
)

来自 https://dev59.com/yFEG5IYBdhLWcg3wU6VP#70335041


8

在我看来,最佳答案存在一个主要问题。

if ( source.collectIsPressedAsState().value)
    onClicked()

onClicked()即使按下的操作被取消,也会触发,因为您没有等待按下后的最终状态。 当您滚动时,如果您的触摸与textField处于相同位置,则会触发onClick,我确定99%的情况下都是不需要的交互。

val pressedState=source.interactions.collectAsState(
      initial = PressInteraction.Cancel(PressInteraction.Press(Offset.Zero)))
       
    if (pressedState.value is PressInteraction.Release)
    {
        dropDownExpanded.value = true
        onClick?.invoke()
        source.tryEmit(PressInteraction.Cancel(PressInteraction.Press(Offset.Zero)))
    }

如果你像这样做,它会在滚动时消除不需要的onClick。


dropDownExpanded.value = true 做什么?我没有在其他地方看到它。 - radda
@radda 这是一个状态,用于显示下拉菜单(如果有的话)。在我的情况下,我有一个下拉菜单,并将其传递给包装整个 TextField 和 DropDownMenu 的可组合项。 - Farrukh Tulkunov

1
对于那些寻求简单的点击功能而不需要焦点动画的人来说,我找到了一种方法,可能@lostintranslation已经尝试过。这个技巧是在onFocusChanged中清除焦点。这种方式保持了TextField的启用样式,但存在两个问题:
  1. 没有焦点动画(其他动画效果良好)
  2. 当用户想要长按复制文本时,会先调用点击事件。
val focusManager = LocalFocusManager.current

TextField(
        value = ...,
        label = ...,
        readOnly = true,
        modifier = Modifier.onFocusChanged {
                if (it.isFocused) {
                    yourOnClick()
                    // clear focus here
                    focusManager.clearFocus()
          }
    )

1
如果出于用户体验的原因,您想保持readOnly=true,那么clickable修饰符将不起作用,但这个修饰符会起作用:
TextField(
    readOnly = true,
    modifier = Modifier.pointerInput(Unit) {
        awaitEachGesture {
            awaitFirstDown(pass = PointerEventPass.Initial)
            val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial)
            if (upEvent != null) {
                // YOUR CALLBACK
            }
        }
    }
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接