Search code examples
androidandroid-layoutuser-interfacetextfieldandroid-jetpack-compose

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


While exploring TextField in a Jetpack Compose, I came across a case where I have to modify input typed in the field. For example, adding a comma after entering 3 characters.

This is how I made it.

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

But after running it, I realized that after the comma is added, keyboard view changed back to alphabets from numbers/symbols which should not be the case. See video output below for clarity

As you can see in the below video when I typed "111" comma was appended and suddenly keyboard's numeric view changed to alphabets again.

Jetpack Compose: Keyboard changing issue


Here I have modified the selection of TextFieldValue so that cursor always be at the end of the message whenever a comma is appended.


Solution

  • As per the mentioned answer above, VisualTransformation is the perfect solution for such cases and we should not directly modify TextField's buffer. Because VisualTransformation just changes the visual output of text and not actual text.

    I've written an article on this scenario here where I've explained this in detail.

    Solution:

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