21

I have a column of TextFields, something like:

Column {
    TextField(
        value = ...,
        onValueChange = { ... },
        keyboardOptions = KeyboardOptions(imeAction = ImeAction.next),
    )
    TextField(
        value = ...,
        onValueChange = { ... },
        keyboardOptions = KeyboardOptions(imeAction = ImeAction.next),
    )
    .
    .
    .
}

I would like to have the focus on each TextField move to the next when the user press Tab, or the next button on the keyboard. Currently pressing Tab inserts a tab into the TextField. Pressing the next button does nothing. I can create a FocusRequester for each TextField and set the keyboardActions onNext to request focus on the next field for each one. This is a little tedious and it doesn't address the Tab behavior.

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
Sean
  • 2,632
  • 2
  • 27
  • 35

5 Answers5

20

I recently found this article: https://medium.com/google-developer-experts/focus-in-jetpack-compose-6584252257fe

It explains a different way to handle focus that is quite a bit simpler.

val focusManager = LocalFocusManager.current
TextField(
    modifier = Modifier
        .onPreviewKeyEvent {
            if (it.key == Key.Tab && it.nativeKeyEvent.action == ACTION_DOWN){
                focusManager.moveFocus(FocusDirection.Down)
                true
            } else {
                false
            }
        },
    value = text,
    onValueChange = { it -> text = it },
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
    keyboardActions = KeyboardActions(
        onNext = { focusManager.moveFocus(FocusDirection.Down) }
    )
)
kassim
  • 3,880
  • 3
  • 26
  • 27
Sean
  • 2,632
  • 2
  • 27
  • 35
  • 1
    Just a couple of things to add to this: instead of `it.key.keyCode == Key.Tab.keyCode` you can simply do `it.key == Key.Tab`, however if you want to avoid the experimental API opt-in you can instead do `it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB`. Also, it's better to use `Modifier.onPreviewKeyEvent`since `Modifier.onKeyEvent` calls `onValueChange` *before* propagating the key event, so you could end up in situations where the TextField you just tabbed out of now has an extra tab character that the user isn't aware of. – omiwrench Jan 05 '22 at 18:03
  • I'm finding that adding this invokes the onPreviewKeyEvent action twice so it move focus twice – any clue why? – kassim May 11 '22 at 08:59
  • oh, its because there's both a key down and key up event – kassim May 11 '22 at 09:06
  • for me looks like `keyboardActions` is not needed – k4dima Jan 14 '23 at 22:07
8

Not sure if it's the easier way, but you can create a FocusRequester object for each field and request the focus following the order that you want.

@Composable
fun FocusRequestScreen() {
    // Create FocusRequesters... (you can use createRefs function)
    val focusRequesters = List(3) { FocusRequester() }

    Column {
        TextFieldWithFocusRequesters(focusRequesters[0], focusRequesters[1])
        TextFieldWithFocusRequesters(focusRequesters[1], focusRequesters[2])
        TextFieldWithFocusRequesters(focusRequesters[2], focusRequesters[0])
    }
}

@Composable
private fun TextFieldWithFocusRequesters(
    focusRequester: FocusRequester,
    nextFocusRequester: FocusRequester
) {
    var state by rememberSaveable {
        mutableStateOf("Focus Transition Test")
    }
    TextField(
        value = state,
        onValueChange = { text -> state = text },
        // Here it is what you want...
        modifier = Modifier
            .focusOrder(focusRequester) {
                nextFocusRequester.requestFocus()
            }
        ,
        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
    )
}

I get this code from here. It didn't solve the tab issue though... :(

nglauber
  • 18,674
  • 6
  • 70
  • 75
  • 5
    Unfortunately this solution does not shift the screen up in case the focused `TextField` is covered by the keyboard... any clue how to achieve that? – Andrei Mesh Mar 29 '21 at 15:09
  • The solution proposed by Gary Wang seems a lot cleaner as it doesn't require you to create a list of `FocusRequester` nor wrap the `TextField` in a new composable. – omiwrench Jan 05 '22 at 17:41
7

I'm currently on compose_version = '1.0.2'.

Focus is not moved when you press the next button because, although the default keyboard action is to move focus to the next one, compose seems doesn't know which one should be the next one. Creating FocusRequester s for each item and set their focus order via Modifier.focusOrder() {} could work (btw, no need to set the keyboardActions's onNext to request focus if you are choosing this way), but since your TextFields are in the same Column, you can just set keyboardActions to tell compose move the focus to the one in the down direction. Something like:

        Column {
            val focusManager = LocalFocusManager.current
            TextField(
                value = "", onValueChange = {},
                keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next ),
                keyboardActions = KeyboardActions(
                    onNext = { focusManager.moveFocus(FocusDirection.Down) }
                )
            )
            TextField(
                value = "", onValueChange = {},
                keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next ),
                keyboardActions = KeyboardActions(
                    onNext = { focusManager.moveFocus(FocusDirection.Down) }
                )
            )
            TextField(
                value = "", onValueChange = {},
                keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next ),
                keyboardActions = KeyboardActions(
                    onNext = { focusManager.moveFocus(FocusDirection.Down) }
                )
            )
        }

After you did this, the next button on the IME keyboard should works.

For the Tab key, since TextField doesn't deal with Tab key automately, so you might want to use focusManager inside Modifier.onKeyEvent{} to move focus in the same way as the above example did.

Gary Wang
  • 340
  • 4
  • 13
5

About the order you can check the @nglauber answer.

To use the Tab key you can use the onKeyEvent modifier.

TextField(
    modifier = Modifier
        .focusRequester(focusRequester)
        .onKeyEvent {
            if (it.key.keyCode == Key.Tab.keyCode){
                focusRequesterNext.requestFocus()
                true //true -> consumed
            } else false },
    value = text,
    onValueChange = { it -> text = it },
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
    keyboardActions = KeyboardActions(
        onNext = {focusRequesterNext.requestFocus()}
        )
)
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
  • 1
    Just a couple of things to add to this: instead of `it.key.keyCode == Key.Tab.keyCode` you can simply do `it.key == Key.Tab`, however if you want to avoid the experimental API opt-in you can instead do `it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB`. Also, it's better to use `Modifier.onPreviewKeyEvent`since `Modifier.onKeyEvent` calls `onValueChange` *before* propagating the key event, so you could end up in situations where the TextField you just tabbed out of now has an extra tab character that the user isn't aware of. – omiwrench Jan 05 '22 at 18:04
1

Set singleLine , please try

OutlinedTextField(
...
singleLine = true,
)

Simple example

@Composable
fun Test() {
    val focusManager = LocalFocusManager.current
    var text1 by remember {
        mutableStateOf("")
    }
    var text2 by remember {
        mutableStateOf("")
    }
    Column() {
        OutlinedTextField(value = text1, onValueChange = {
            text1 = it
        },
            keyboardOptions = KeyboardOptions(
                imeAction = ImeAction.Next,
                keyboardType = KeyboardType.Text
            ),
            keyboardActions = KeyboardActions(
                onNext ={
                    focusManager.moveFocus(FocusDirection.Down)
                }
            ),
            singleLine = true
            )
        OutlinedTextField(value = text2, onValueChange = {
            text2 = it
        },
            keyboardOptions = KeyboardOptions(
                imeAction = ImeAction.Next,
                keyboardType = KeyboardType.Text
            ),
            keyboardActions = KeyboardActions(
                onNext ={
                    focusManager.moveFocus(FocusDirection.Down)
                }
            ),
            singleLine = true
        )
    }
}
Yshh
  • 654
  • 1
  • 10