3

I'm working on an edit screen in Jetpack Compose and seeing behavior where, when I tap the text field for the first time, the cursor appears at the beginning of the text in the field, rather than at the end.

OutlinedTextField(
    value = user.name,
    onValueChange = { onChange(user.copy(name = it)) },
    modifier = Modifier.fillMaxWidth(),
    label = { Text(text = stringResource(R.string.name)) },
    singleLine = true,
    enabled = enabled,
)

The screen shows up and the "Name" field appears populated. But when I tap into the field, the cursor appears at the beginning of the field.

When I debug, the field never renders where user.name is null or empty. I can move the cursor around and edit the text fine after the initial tap.

My goal is to avoid tapping twice to put the cursor at the end.

I tried having it go through a run where the user.name was "" before the real value gets passed to see if it needed some kind of "appending" to occur. But even with that the cursor appeared at the front on the first tap.

I also tried rendering the field with just the string "test" to see if that would place the cursor at the end. Also no, still at the front.

EDIT: seems like this is a bug somewhere. Created a minimally reproducible example and submitted a bug: https://issuetracker.google.com/issues/275157249

As best I can tell, if the text field is part of the first composition, the cursor will be at the beginning of the field, regardless of the text inside.

SeaMauFive
  • 101
  • 2
  • 6

2 Answers2

0

OutlinedTextField has two implementations. The first one is with value: String. The second one is value: TextFieldValue.

TextFieldValue allows to set cursor position.

Example:

@Composable
fun PasscodeInput(
    codeLength: Int,
    modifier: Modifier = Modifier,
    initialCode: String = "",
    onValueChange: (String) -> Unit,
    onValueFill: () -> Unit,
) {
    val scope = rememberCoroutineScope()
    val code = remember(initialCode) {
        mutableStateOf(TextFieldValue(initialCode, TextRange(initialCode.length)))
    }

    BasicTextField(
        value = code.value,
        onValueChange = {
            scope.launch {
                val text = it.text.filter { char -> char.isDigit() }
                if (text.length <= codeLength) onValueChange(text)
                if (text.length == codeLength) onValueFill()
            }
        },
        modifier = modifier,
        keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
        decorationBox = {
            CodeDecorationBox(codeLength, code.value.text)
        }
    )
}
  • If I use that example exactly, it works as advertised. But when I try to incorporate it into my example, `onValueChange` gets called repeatedly. I'm character limited in comments, but the only real difference I can track is accessing a field rather than passing the `String` itself. I don't know how that would affect anything though ```kotiln val code = remember(user.name) { mutableStateOf(TextFieldValue(user.name, TextRange(user.name.length))) } ``` – SeaMauFive Mar 25 '23 at 15:27
  • I think it happens because of `val code = remember(user.name)`. This code triggers recomposition. Try use `remember` without argument. `val code = remember { mutableStateOf(TextFieldValue(user.name, TextRange(user.name.length))) }` and `onValueChange = { code = it; onChange(user.copy(name = it)) },` – Aleksei Sinelnikov Mar 25 '23 at 16:43
  • That does stop the infinite calls, but it still doesn't put the cursor at the end correctly. What's weird is that, if I put a breakpoint inside the `remember` block, it _does_ work. I stop at some weird spots in code (putting a breakpoint there seems to add them it other spots too(?). But if I let it run without breakpoints, the cursor stays at the front. – SeaMauFive Mar 25 '23 at 20:21
  • Additionally, with breakpoints in other locations outside the `remember` I can see that the first composition, `code` does have `selection` set appropriately. But when I tap the field, `onValueChanged` is immediately called and `it` does _not_ have `selection` set correctly. It is `TextRange(0,0)`. But I can still see that the `code` has `TextRange(6,6)` – SeaMauFive Mar 25 '23 at 20:24
  • OK. I actually think I have something working... And no changes from my original code were necessary. When testing, this field was always rendered (composed?) on the very first screen and render (composition?) after the splash screen. If any screen comes first, the field behaves exactly as expected without using `TextFieldValue`. Either something is really up with my app to cause this weird behavior or is a bug. I'm gonna see if I can create a minimally reproducible example. – SeaMauFive Mar 26 '23 at 02:58
  • Is it possible to use VisualTransformation? – the_prole May 31 '23 at 18:33
0

when I tap the text field for the first time, the cursor appears at the beginning of the text in the field, rather than at the end.

Try adding your field in a row with max intrisic size

Row(modifier = Modifier.height(IntrinsicSize.Max)) { 
   // field
}

fixed the initial cursor positioning for my androidx.compose.material3.TextField, don't know why

the_prole
  • 8,275
  • 16
  • 78
  • 163