Skip to content Skip to sidebar Skip to footer

How To Apply A Mask Date (mm/dd/yyyy) In Textfield With Jetpack Compose?

I have a TextField in which there cannot be more than 10 characters, and the user is required to enter date in the format 'mm/dd/yyyy'. Whenever user types first 2 characters I app

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

enter image description here

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:

enter image description here

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?"