Android Jetpack Compose:修改输入文本后键盘从数字变为字母

Android Jetpack Compose: Keyboard changing from numeric to alphabets after modifying input text

在 Jetpack Compose 中探索 TextField 时,我遇到了一个情况,我必须修改字段中输入的内容。 例如输入3个字符后加一个逗号.

我就是这样做的。

@Composable
fun TFDemo() {
    var fieldValue by remember { mutableStateOf(TextFieldValue("")) }

    TextField(
        value = fieldValue,
        onValueChange = {
            val newMessage = it.text.let { text -> if (text.length == 3) "$text," else text }
            fieldValue = it.copy(newMessage, selection = TextRange(newMessage.length))
        },
        keyboardOptions = KeyboardOptions(autoCorrect = false),
    )
}

但是在 运行 之后,我意识到添加逗号后,键盘视图从 numbers/symbols 变回了字母表 这不应该是这样的。 为了清晰起见,请参阅下面的视频输出

正如您在下面的视频中所看到的,当我输入“111”时附加了逗号,键盘的数字视图突然再次变为字母。


这里我修改了TextFieldValueselection,这样每当附加逗号时,光标总是在消息的末尾。

这种情况正是 VisualTransformation 的目的。

这是 Google 员工对 another issue 的评论:

I don't think we can fix this issue easily.

The filtering text in onValueChanged callback is generally not recommended because the text state is shared with out process IME(software keyboard). The filtering text means the text content changes internally, then the new state is notified to IME. This is not a normal path to IME and different IME reacts differently to this unexpected state change. Some IME may try to reconstruct the composition, others may give up and start new session, etc. This is mostly due of the historical reason and hard to fix from now. So, please avoid filtering text in onValueChanged callback and consider following alternatives:

  1. (Recommended) Don't filter it and show error message. (irrelevant here)
  2. Use VisualTransformation for changing visual output without modifying edit buffer.

根据上面提到的答案,VisualTransformation 是这种情况的完美解决方案,我们不应该直接修改 TextField 的缓冲区。因为 VisualTransformation 只是更改文本的视觉输出而不是实际文本。

I've written an article on this scenario here 我已经详细解释了这一点。

解决方案:

@Composable
fun TextFieldDemo() {
    var message by remember { mutableStateOf("") }

    TextField(
        value = message,
        placeholder = { Text("Enter amount or message") },
        onValueChange = { message = it },
        visualTransformation = AmountOrMessageVisualTransformation()
    )
}

class AmountOrMessageVisualTransformation : VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText {

        val originalText = text.text
        val formattedText = formatAmountOrMessage(text.text)

        val offsetMapping = object : OffsetMapping {

            override fun originalToTransformed(offset: Int): Int {
                if (originalText.isValidFormattableAmount) {
                    val commas = formattedText.count { it == ',' }
                    return when {
                        offset <= 1 -> offset
                        offset <= 3 -> if (commas >= 1) offset + 1 else offset
                        offset <= 5 -> if (commas == 2) offset + 2 else offset + 1
                        else -> 8
                    }
                }
                return offset
            }

            override fun transformedToOriginal(offset: Int): Int {
                if (originalText.isValidFormattableAmount) {
                    val commas = formattedText.count { it == ',' }
                    return when (offset) {
                        8, 7 -> offset - 2
                        6 -> if (commas == 1) 5 else 4
                        5 -> if (commas == 1) 4 else if (commas == 2) 3 else offset
                        4, 3 -> if (commas >= 1) offset - 1 else offset
                        2 -> if (commas == 2) 1 else offset
                        else -> offset
                    }
                }
                return offset
            }
        }

        return TransformedText(
            text = AnnotatedString(formattedText),
            offsetMapping = offsetMapping
        )
    }
}