Search code examples
androidkotlintextfieldandroid-jetpack-compose

Change Cursor Position and force SingleLine of TextField in Jetpack Compose


Couldn't find a way to change the cursor position of TextField and making it singleline. Someone find a way to do it?

At this time, I'm using the latest jetpack-compose 1.0.0-alpha03 version.


Solution

  • To set the cursor you need to set the selection of the TextFieldValue like this:

    @Composable
    fun Content() {
        val initTargetIndex = 3
        val initValue = "string"
        val initSelectionIndex = initTargetIndex.takeIf { it <= initValue.length } ?: initValue.length
        val textFieldValueState = remember {
            mutableStateOf(TextFieldValue(
                text = initValue,
                selection = TextRange(initSelectionIndex)
            ))
        }
        TextField(
            modifier = Modifier.height(50.dp),
            value = textFieldValueState.value,
            onValueChange = { tfv -> textFieldValueState.value = tfv}
        )
    }
    

    Keep in mind you need to update the selection yourself from the onValueChange otherwise the user cannot move the cursor or type/delete.

    To the single-line, you need to set a fixed height on the TextField Composable and you probably want to sanitize '\n' from the user input.

    @Composable
    fun Content() {
        val initTargetIndex = 3
        val initValue = "string"
        val initSelectionIndex = initTargetIndex.takeIf { it <= initValue.length } ?: initValue.length
        val textFieldValueState = remember {
            mutableStateOf(TextFieldValue(
                text = initValue,
                selection = TextRange(initSelectionIndex)
            ))
        }
        TextField(
            modifier = Modifier.height(50.dp),
            value = textFieldValueState.value,
            onValueChange = { tfv ->
                val sanitizedText = tfv.text.replace("\n", "")
                val needUpdate = sanitizedText.length >= tfv.text.length
                if (needUpdate) {
                    textFieldValueState.value = tfv
                }
            },
        
        )
    }
    

    For the latter, I sanitize the new text and compare the lengths of it and the state's text, if the new text is shorter I do not have to update the state because I just removed the character during sanitizing. If you just want to stop the user from adding new lines on their own, you can leave the height unconstrained.

    The previous solution ignores a pasted text with a line break, if you want to keep it this onValueChange implementation should handle it correctly:

    val onValueChange = {tfv ->
        textFieldValueState.value.let { old ->
            val sanitizedText = tfv.text.replace("\n", "")
            val lastPositionIndex = sanitizedText.length
            val needUpdate = sanitizedText.length < tfv.text.length
            val selection = if (needUpdate) {
                tfv.selection.copy(
                    start = old.selection.start.takeUnless { it > lastPositionIndex} ?: lastPositionIndex,
                    end = old.selection.end.takeUnless { it > lastPositionIndex} ?: lastPositionIndex
                )
            } else tfv.selection
            val composition = old.composition?.let { oldComp ->
                if (needUpdate) {
                   TextRange(
                        start = oldComp.start.takeUnless { it > lastPositionIndex} ?: lastPositionIndex,
                        end = oldComp.end.takeUnless { it > lastPositionIndex} ?: lastPositionIndex
                    )
                } else oldComp
            }
            textFieldValueState.value = tfv.copy(text = sanitizedText, selection = selection, composition = composition)
        }
    }