How To Apply A Mask Date (mm/dd/yyyy) In Textfield With Jetpack Compose?
Solution 1:
You can do something different using the onValueChange
to define a max number of characters and using visualTransformation
to display your favorite format without changing the value in TextField
.
valmaxChar=8
TextField(
singleLine = true,
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
},
visualTransformation = DateTransformation()
)
where:
classDateTransformation() : VisualTransformation {
overridefunfilter(text: AnnotatedString): TransformedText {
return dateFilter(text)
}
}
fundateFilter(text: AnnotatedString): TransformedText {
val trimmed = if (text.text.length >= 8) text.text.substring(0..7) else text.text
varout = ""for (i in trimmed.indices) {
out += trimmed[i]
if (i % 2 == 1 && i < 4) out += "/"
}
val numberOffsetTranslator = object : OffsetMapping {
overridefunoriginalToTransformed(offset: Int): Int {
if (offset <= 1) return offset
if (offset <= 3) return offset +1if (offset <= 8) return offset +2return10
}
overridefuntransformedToOriginal(offset: Int): Int {
if (offset <=2) return offset
if (offset <=5) return offset -1if (offset <=10) return offset -2return8
}
}
return TransformedText(AnnotatedString(out), numberOffsetTranslator)
}
Solution 2:
The /
is being deleted but as soon as you delete, the length of the text becomes 2 or 5. So it checks the condition,
if (it.text.length == 2 || it.text.length == 5)
Since the condition is true now, the /
appends again into the text. So it seems like it is not at all being deleted.
One way to solve this is by storing the previous text length and checking if the text length now is greater than the previous text length.
To achieve this, declare a variable below maxCharDate
as
var previousTextLength = 0
And change the nested if condition to,
if ((it.text.length == 2 || it.text.length == 5) && it.text.length > previousTextLength)
And at last update the previousTextLength
variable. Below the emailErrorVisible.value = false
add
previousTextLength = it.text.length;
Solution 3:
I would suggest not only a date mask, but a simpler and generic solution for inputs masking.
A general formatter interface in order to implement any kind of mask.
interfaceMaskFormatter{
funformat(textToFormat: String): String
}
Implement our own formatters.
object DateFormatter : MaskFormatter {
funformat(textToFormat: String): String {
TODO("Format '01212022' into '01/21/2022'")
}
}
object CreditCardFormatter : MaskFormatter {
funformat(textToFormat: String): String {
TODO("Format '1234567890123456' into '1234 5678 9012 3456'")
}
}
And finally use this generic extension function for transforming your text field inputs and you won't need to care about the offsets at all.
internalfun MaskFormatter.toVisualTransformation(): VisualTransformation =
VisualTransformation {
val output = format(it.text)
TransformedText(
AnnotatedString(output),
object : OffsetMapping {
overridefunoriginalToTransformed(offset: Int): Int = output.length
overridefuntransformedToOriginal(offset: Int): Int = it.text.length
}
)
}
Some example usages:
// Date Exampleprivateconstval MAX_DATE_LENGTH = 8@ComposablefunDateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.matches("^\\d{0,$MAX_DATE_LENGTH}\$".toRegex())) {
date = it
}
},
visualTransformation = DateFormatter.toVisualTransformation()
)
}
// Credit Card Exampleprivateconstval MAX_CREDIT_CARD_LENGTH = 16@ComposablefunCreditCardTextField() {
var creditCard by remember { mutableStateOf("") }
TextField(
value = creditCard,
onValueChange = {
if (it.matches("^\\d{0,$MAX_CREDIT_CARD_LENGTH}\$".toRegex())) {
creditCard = it
}
},
visualTransformation = CreditCardFormatter.toVisualTransformation()
)
}
Solution 4:
It is because you are checking for the length of the string. Whenever the length is two, you insert a slash. Hence the slash gets deleted, and re-inserted.
Why don't you just create three TextFields and insert Slashes as Texts in between. Such logic can be very hard to perfect. Keen users can use it to crash your app, and also devs can insert malicious stuff, and exploit this flaw because the handling logic can have loopholes as well, so... It is better in my opinion to just go with the simplest (and what I think is more elegant) way of constructing.
Solution 5:
Implementation of VisualTranformation that accepts any type of mask for Jetpack Compose TextField:
classMaskVisualTransformation(privateval mask: String) : VisualTransformation {
privateval specialSymbolsIndices = mask.indices.filter { mask[it] != '#' }
overridefunfilter(text: AnnotatedString): TransformedText {
varout = ""var maskIndex = 0
text.forEach { char ->
while (specialSymbolsIndices.contains(maskIndex)) {
out += mask[maskIndex]
maskIndex++
}
out += char
maskIndex++
}
return TransformedText(AnnotatedString(out), offsetTranslator())
}
privatefunoffsetTranslator() = object : OffsetMapping {
overridefunoriginalToTransformed(offset: Int): Int {
val offsetValue = offset.absoluteValue
if (offsetValue == 0) return0var numberOfHashtags = 0val masked = mask.takeWhile {
if (it == '#') numberOfHashtags++
numberOfHashtags < offsetValue
}
return masked.length + 1
}
overridefuntransformedToOriginal(offset: Int): Int {
return mask.take(offset.absoluteValue).count { it == '#' }
}
}
}
How to use it:
@ComposablefunDateTextField() {
var date by remember { mutableStateOf("") }
TextField(
value = date,
onValueChange = {
if (it.length <= DATE_LENGTH) {
date = it
}
},
visualTransformation = MaskVisualTransformation(DATE_MASK)
)
}
object DateDefaults {
constval DATE_MASK = "##/##/####"constval DATE_LENGTH = 8// Equals to "##/##/####".count { it == '#' }
}
Post a Comment for "How To Apply A Mask Date (mm/dd/yyyy) In Textfield With Jetpack Compose?"