Nicola De Fiorenze
Nicola De Fiorenze

Reputation: 2128

Jetpack Compose, custom cursor position in TextField

How can I set the cursor in a random position on a TextField when it it get focus? The equivalent of editText.setSelection(position) with the classic android view system.

This is the code I am using to have an edit text automatically receive the focus when it is added to the screen. I would like to be able to move the cursor from the default position which is 0

val (getText, setText) = remember { mutableStateOf("hello") }
AutofocusEditText(
    text = getText,
    setText = setText
)
    
...

@Composable
private fun AutofocusEditText(
    text: String,
    setText : (String) -> Unit
) {
    val focusState = remember { mutableStateOf(FocusState.Inactive) }
    val focusRequester = FocusRequester()
    val focusModifier = Modifier.focus()
    Row(
        modifier = Modifier.focusObserver { newFocusValue -> focusState.value = newFocusValue }
    ) {
        val focusRequesterModifier =
            Modifier.focusRequester(focusRequester)

        TextField(
            value = text,
            modifier = focusModifier.then(focusRequesterModifier),
            backgroundColor = Color.Transparent,
            onValueChange = setText,
            keyboardOptions = KeyboardOptions.Default.copy(
                imeAction = ImeAction.Done
            ),
            onImeActionPerformed = { action, softKeyboardController ->
                if (action == ImeAction.Done) {
                    softKeyboardController?.hideSoftwareKeyboard()
                }
            }
        )
    }
    onActive {
        focusRequester.requestFocus()
    }
}

Upvotes: 16

Views: 16590

Answers (3)

2jan222
2jan222

Reputation: 1924

You have to use the TextFieldValue version of TextField.

@Composable
fun TextField(
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    /* ... */) {/* Impl */}

Code examples: EDIT: Compose Version 1.1.1 (13.04.22)

enum class CursorSelectionBehaviour {
    START, END, SELECT_ALL
}

@Composable
fun AutofocusTextFieldExample(
    initValue: String,
    behaviour: CursorSelectionBehaviour = CursorSelectionBehaviour.END
) {
    val direction = LocalLayoutDirection.current
    var tfv by remember {
        val selection = when (behaviour) {
            CursorSelectionBehaviour.START -> {
                if (direction == Ltr) TextRange.Zero else TextRange(initValue.length)
            }
            CursorSelectionBehaviour.END -> {
                if (direction == Ltr) TextRange(initValue.length) else TextRange.Zero
            }
            CursorSelectionBehaviour.SELECT_ALL -> TextRange(0, initValue.length)
        }
        val textFieldValue = TextFieldValue(text = initValue, selection = selection)
        mutableStateOf(textFieldValue)
    }
    val focusRequester = remember { FocusRequester() }
    TextField(
        modifier = Modifier.focusRequester(focusRequester),
        value = tfv,
        onValueChange = { tfv = it }
    )
    LaunchedEffect(Unit) {
        focusRequester.requestFocus()
    }
}

OLD (04.01.21):

    @Composable
    fun AutoFocusingText() {
        val textState = remember { mutableStateOf(TextFieldValue()) }
        val focusState = remember { mutableStateOf(FocusState.Inactive) }
        val focusRequester = FocusRequester()
        val focusModifier = Modifier.focus()
        Row(
            modifier = Modifier.focusObserver { focusState.value = it }
        ) {
            val focusRequesterModifier = Modifier.focusRequester(focusRequester)
            TextField(
                modifier = focusModifier.then(focusRequesterModifier),
                value = textState.value,
                onValueChange = { value: TextFieldValue ->
                    textState.value = value
                }
            )
        }
        onActive {
            focusRequester.requestFocus()
        }
    }

If you have a non-empty string as an initial value you have to change the selection manually. Replace the empty TextFieldValue with: TextFieldValue(text = value, selection = TextRange(value.length, value.length))

When you want to extract the value like it is in your code. You either add the current selection as a parameter or extract it combined with the TextFieldValue. Otherwise, if the user edits in the middle of the text the cursor jumps back to the end on the next onValueChanged.

Upvotes: 19

VIN
VIN

Reputation: 6947

There is another way to control the cursor position without using LaunchedEffect to call focusRequester.requestFocus(). You can use call it in onGloballyPositioned. You can also show the keyboard in onGloballyPositioned if needed.


    val focusRequester = remember { FocusRequester() }
    val keyboard = LocalSoftwareKeyboardController.current

    val textFieldValue = TextFieldValue(text = initValue, selection = TextRange(initValue.length)) //place cursor at the end of the text
    var tfv by remember { mutableStateOf(textFieldValue) }

    TextField(
        modifier = Modifier
            .focusRequester(focusRequester)
            .onGloballyPositioned {
                focusRequester.requestFocus() // IMPORTANT
                keyboard?.show()
            },
        value = tfv,
        onValueChange = {
            tfv = it
        }
    )

Upvotes: 1

Eliyahu Shwartz
Eliyahu Shwartz

Reputation: 71

When the input is not updated just by the user and is also changed from outside of the component, this is a way to update the content while letting the user play with the cursor:

private fun InputField(input:String) {
 
  val inputLength = input.length
  val textFieldValue = TextFieldValue(input, TextRange(inputLength))
  var textFieldValueRembered by remember { mutableStateOf(textFieldValue) }

  //a way to detect a reformation of text while letting the user control the cursor location
  if (input != textFieldValue.text)
    textFieldValueRembered = textFieldValue

  TextField(
   textFieldValue = textFieldValueRembered,
    onValueChange = {
      onValueChange(it.text)
      phoneInputField = it
    }
  )

Upvotes: 0

Related Questions