Using Jetpack Compose, how can I format the TextField value to follow the "dd/mm/yyyy" format?
Asked
Active
Viewed 3,903 times
1 Answers
11
Updated Answer
Implementation of VisualTranformation that accepts any type of mask for Jetpack Compose TextField:
class MaskVisualTransformation(private val mask: String) : VisualTransformation {
private val specialSymbolsIndices = mask.indices.filter { mask[it] != '#' }
override fun filter(text: AnnotatedString): TransformedText {
var out = ""
var maskIndex = 0
text.forEach { char ->
while (specialSymbolsIndices.contains(maskIndex)) {
out += mask[maskIndex]
maskIndex++
}
out += char
maskIndex++
}
return TransformedText(AnnotatedString(out), offsetTranslator())
}
private fun offsetTranslator() = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
val offsetValue = offset.absoluteValue
if (offsetValue == 0) return 0
var numberOfHashtags = 0
val masked = mask.takeWhile {
if (it == '#') numberOfHashtags++
numberOfHashtags < offsetValue
}
return masked.length + 1
}
override fun transformedToOriginal(offset: Int): Int {
return mask.take(offset.absoluteValue).count { it == '#' }
}
}
}
How to use it:
@Composable
fun DateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.length <= DATE_LENGTH) {
date = it
}
},
visualTransformation = MaskVisualTransformation(DATE_MASK)
)
}
object DateDefaults {
const val DATE_MASK = "##/##/####"
const val DATE_LENGTH = 8 // Equals to "##/##/####".count { it == '#' }
}
Old Answer
Just as Jetpack Compose offers the Visual Transformation of the password, we can do our own visual transformation.
class DateTransformation : VisualTransformation {
// XX/XX/XXXX format
override fun filter(text: AnnotatedString): TransformedText {
var out = ""
text.text.forEachIndexed { index, char ->
when (index) {
2 -> out += "/$char"
4 -> out += "/$char"
else -> out += char
}
}
val numberOffsetTranslator = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 2) return offset
if (offset <= 4) return offset + 1
return offset + 2
}
override fun transformedToOriginal(offset: Int): Int {
if (offset <= 2) return offset
if (offset <= 5) return offset - 1
return offset - 2
}
}
return TransformedText(AnnotatedString(out), numberOffsetTranslator)
}
}
We return a TransformedText with the filter applied and an OffsetMapping that translates between the original and transformed text.
Now, we can use our Visual Transformation class in any TextField.
var text by remember { mutableStateOf("") }
TextField(
value = text,
visualTransformation = DateTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = {
if (it.length < 9) text = it
}
)

Thiago Souza
- 1,171
- 8
- 16
-
Hey, thanks for your solution. I just have a doubt. My state has a different text structure comparing to what it is on the field. On the field is "01/01/2000" and, in my state, the value is: "01012000". How can I solve this? – R0ck Dec 03 '22 at 00:58
-
In fact, the visual transformation only changes the visual. You have to change the value elsewhere, like in the viewmodel or usecase. You can use part of the code above to do this. Example: https://gist.github.com/the-thiago/4c3d421310501a4a7f3bef2bb94a6ef6 – Thiago Souza Dec 03 '22 at 13:36
-
Nice, thanks! I'd just change "out" to a StringBuilder to avoid multiple reallocations. – mrlem Mar 10 '23 at 10:27