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.
Here I have modified the selection
of TextFieldValue
so that cursor always be at the end of the message whenever a comma is appended.
CodePudding user response:
This kind of cases is exactly what VisualTransformation
is intended for.
Here's a Googler's comment on 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:
- (Recommended) Don't filter it and show error message. (irrelevant here)
- Use VisualTransformation for changing visual output without modifying edit buffer.
CodePudding user response:
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
)
}
}