What is the simplest way to set the focus order in Jetpack Compose?
Asked Answered
C

6

27

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.

Cyrie answered 26/3, 2021 at 13:28 Comment(0)
C
31

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) }
    )
)
Cyrie answered 31/8, 2021 at 13:10 Comment(6)
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.onPreviewKeyEventsince 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.Nobility
I'm finding that adding this invokes the onPreviewKeyEvent action twice so it move focus twice – any clue why?Toastmaster
oh, its because there's both a key down and key up eventToastmaster
for me looks like keyboardActions is not neededSharpwitted
This works perfectly if you just need to "re-emit" onNext event in case you handle something there already. (If you listen to onNext it ignores it, this way you can restore default behaviour)Medardas
They keyboardActions is to change the behavior with a soft keyboard.Cyrie
D
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... :(

Dipsomaniac answered 26/3, 2021 at 15:17 Comment(2)
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?Jimmyjimsonweed
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.Nobility
D
8

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.

Dottiedottle answered 26/9, 2021 at 16:34 Comment(0)
S
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()}
        )
)
Slavish answered 26/3, 2021 at 16:28 Comment(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.onPreviewKeyEventsince 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.Nobility
C
2

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
        )
    }
}
Chiliasm answered 25/2, 2022 at 5:36 Comment(0)
C
1

It seems Google has a recommended approach here that hasn't been suggested yet - https://developer.android.com/jetpack/compose/touch-input/focus/change-focus-traversal-order

val (first, second, third, fourth) = remember { FocusRequester.createRefs() }
Column {
    Row {
        TextButton(
            {},
            Modifier
                .focusRequester(first)
                .focusProperties { next = second }
        ) {
            Text("First field")
        }
        TextButton(
            {},
            Modifier
                .focusRequester(third)
                .focusProperties { next = fourth }
        ) {
            Text("Third field")
        }
    }

    Row {
        TextButton(
            {},
            Modifier
                .focusRequester(second)
                .focusProperties { next = third }
        ) {
            Text("Second field")
        }
        TextButton(
            {},
            Modifier
                .focusRequester(fourth)
                .focusProperties { next = first }
        ) {
            Text("Fourth field")
        }
    }
}
Catboat answered 24/11, 2023 at 0:12 Comment(1)
Thanks for this. It's good to see google's recommendation. This is basically the solution proposed by nglauber. Modifier.focusOrder has been deprecated in favor of Modifier.focusProperties and FocusRequester.createRefs is an experimental convenience method to create the focus requesters.Cyrie

© 2022 - 2024 — McMap. All rights reserved.