13

I am trying to implement an onClick handler for a Compose TextField. Current functionality is to handle the TextField click but disable manually editing of the field. For my use case I would like to handle the click and do something else. I would like to keep the look and feel of a TextField, and would like the focus animation to also happen.

the readOnly property gives me what I want from a UX perspective, however when I click on the TextField the onClick handler is not called.

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

I have also tried to use pointerInput, how I am having the same problem.

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

With Compose being so new its hard to tell if this is a bug or intended.

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
lostintranslation
  • 23,756
  • 50
  • 159
  • 262

6 Answers6

18

Maybe the better way to detect click events in text field is using interaction source.

step 1 : - Create an interaction source

 val source = remember {
    MutableInteractionSource()
}

step 2: - pass it to text field

 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))
)

step: 3- collect pressed flow as state and observe changes to it.

 if ( source.collectIsPressedAsState().value)
        onClicked()
Zakir Sheikh
  • 734
  • 8
  • 14
14

Using readOnly = true the TextField is still enabled and the first click gives the focus to the field. You have to perform a double click to handle your onClick function.

Otherwise you can use the enabled property:

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

Something like:

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

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
7

To me, it seems the top answer has one major problem.

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

onClicked() will trigger even if press is canceled because you are not waiting to see the end state after the press. When you are scrolling, if your touch is in the same position as the textField, onClick will get triggered and I'm sure that's unwanted interaction 99% of the time.

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)))
    }

if you do something like this it eliminates the unwanted onClick while scrolling

Farrukh Tulkunov
  • 672
  • 1
  • 6
  • 14
  • What does the `dropDownExpanded.value = true` do? I don't see it anywhere else – radda Mar 10 '23 at 10:30
  • @radda it is a state to show the dropdown menu if you have one, in my case, there is one and I pass it to the composable that wraps the whole TextField and the DropDownMenu – Farrukh Tulkunov Mar 14 '23 at 07:10
5

The correct asnwer would be to use interactions of InteractionSource and not collectIsPressedAsState! The last one just checks when you tap on something but it ignores the fact that you don't even release your finger

TextField(
    value = ...,
    onValueChange = { ... },
    interactionSource = remember { MutableInteractionSource() }
        .also { interactionSource ->
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collect {
                    if (it is PressInteraction.Release) {
                        // works like onClick
                    }
                }
            }
        }
)

From https://stackoverflow.com/a/70335041/7767664

user924
  • 8,146
  • 7
  • 57
  • 139
0

If you want to keep readOnly=true for UX reasons, clickable modifier won't work but this one will :

TextField(
    readOnly = true,
    modifier = Modifier.pointerInput(Unit) {
        awaitEachGesture {
            awaitFirstDown(pass = PointerEventPass.Initial)
            val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial)
            if (upEvent != null) {
                // YOUR CALLBACK
            }
        }
    }
)
Jscti
  • 14,096
  • 4
  • 62
  • 87
0

For someone who seeks for a simple onclick function without the need of focus animation, I figured out a way that @lostintranslation may have tried. The trick is clear focus in onFocusChanged. This way keeps the TextField enabled-style but with two problems:

  1. no focus animation (other animations work well)
  2. when user want to long press to copy the text, it calls the onclick first.
val focusManager = LocalFocusManager.current

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