Jetpack Compose中设置焦点顺序的最简单方法是什么?

23

我有一列TextField,类似于:

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

我希望每个TextField在用户按下Tab键或键盘上的下一个按钮时都能聚焦到下一个TextField。目前按下Tab会在TextField中插入制表符。按下下一个按钮不起作用。我可以为每个TextField创建一个FocusRequester,并将keyboardActions的onNext设置为请求下一个字段的焦点。这有点繁琐,而且并没有解决Tab的行为。

6个回答

24

我最近发现了这篇文章:https://medium.com/google-developer-experts/focus-in-jetpack-compose-6584252257fe

它解释了一种处理焦点的不同方式,这种方式相当简单。

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) }
    )
)

1
只需添加几个内容:不要使用it.key.keyCode == Key.Tab.keyCode,而是可以直接使用it.key == Key.Tab,但是如果您想避免实验性API选择,则可以改为使用it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB。此外,最好使用Modifier.onPreviewKeyEvent,因为Modifier.onKeyEvent在传播按键事件之前调用onValueChange,所以您可能会遇到这样的情况:您刚刚离开的TextField现在具有用户不知道的额外制表符。 - omiwrench
我发现添加这个会触发onPreviewKeyEvent动作两次,所以它会移动焦点两次 - 有什么线索吗? - kassim
哦,这是因为存在按键按下和松开两种事件。 - kassim
对我来说,似乎不需要 keyboardActions - k4dima
如果你只是需要在已经处理了某些东西的情况下“重新发出”下一个事件,那么这个方法非常完美。(如果你监听了下一个事件,它会忽略它,这样你就可以恢复默认行为) - undefined

8

不确定是否最简单,但是您可以为每个字段创建一个FocusRequester对象,并按照您想要的顺序请求焦点。

@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)
    )
}

我从这里获取了这段代码,但是它并没有解决标签问题... :(


5
很遗憾,这个解决方案不能在键盘覆盖焦点TextField时将屏幕上移...有什么办法可以实现吗? - Andrei Mesh
Gary Wang 提出的解决方案更加简洁,因为它不需要创建 FocusRequester 列表,也不需要将 TextField 包装在新的 composable 中。 - omiwrench

7

我目前使用的是compose_version = '1.0.2'

当你点击“下一个”按钮时,焦点不会移动,因为尽管默认键盘操作是将焦点移动到下一个,但Compose似乎不知道下一个应该是哪个。为每个项目创建FocusRequester并通过Modifier.focusOrder() {}设置它们的焦点顺序可能有效(顺便说一句,如果你选择此方法,则不需要将keyboardActionsonNext设置为请求焦点),但是由于你的TextField都在同一个Column中,所以可以只设置keyboardActions以告诉Compose将焦点向下移动到下一个文本框。像这样:

        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) }
                )
            )
        }

在你完成这个步骤之后,IME键盘上的下一步按钮应该可以正常使用。

对于 Tab 键,由于 TextField 不会自动处理 Tab 键,因此您可能希望在 Modifier.onKeyEvent{} 中使用 focusManager 以与上面的示例相同的方式移动焦点。


5

关于顺序,您可以查看@nglauber的答案

要使用Tab键,您可以使用onKeyEvent修饰符

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()}
        )
)

1
只需添加几个内容:不要使用it.key.keyCode == Key.Tab.keyCode,而是可以直接使用it.key == Key.Tab,但是如果您想避免实验性API选择,则可以改为使用it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB。此外,最好使用Modifier.onPreviewKeyEvent,因为Modifier.onKeyEvent在传播按键事件之前调用onValueChange,所以您可能会遇到这样的情况:您刚刚离开的TextField现在具有用户不知道的额外制表符。 - omiwrench

1

设置 singleLine,请尝试

OutlinedTextField(
...
singleLine = true,
)

简单示例

@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
        )
    }
}

1
看起来谷歌在这里有一个推荐的方法,但尚未提出建议 - 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")
        }
    }
}

谢谢这个。很高兴看到谷歌的推荐。这基本上是nglauber提出的解决方案。Modifier.focusOrder已被弃用,推荐使用Modifier.focusProperties,而FocusRequester.createRefs是一个实验性的便利方法,用于创建焦点请求者。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接