如何使用 Jetpack Compose 在 TextField 中应用屏蔽日期 (mm/dd/yyyy)?

How to apply a mask date (mm/dd/yyyy) in TextField with Jetpack Compose?

我有一个文本字段,其中不能超过 10 个字符,并且要求用户以“mm/dd/yyyy”格式输入日期。每当用户键入前 2 个字符时,我都会附加“/”,当用户键入接下来的 2 个字符时,我会再次附加“/”。

我做了以下事情来实现这个:

            var maxCharDate = 10

            TextField(
                value = query2,
                onValueChange = {
                    if (it.text.length <= maxCharDate) {
                        if (it.text.length == 2 || it.text.length == 5)
                            query2 = TextFieldValue(it.text + "/", selection = TextRange(it.text.length+1))
                        else
                            query2 = it
                    }
                    emailErrorVisible.value = false
                },
                label = {
                    Text(
                        "Date of Birth (mm/dd/yyyy)",
                        color = colorResource(id = R.color.bright_green),
                        fontFamily = FontFamily(Font(R.font.poppins_regular)),
                        fontSize = with(LocalDensity.current) { dimensionResource(id = R.dimen._12ssp).toSp() })
                },
                  .
                  .
                  .

除了附加的“/”不会在按退格键时被删除,而其他字符会被删除外,它可以正常工作。

如何使“/”在按退格键时也被删除?

这是因为您正在检查字符串的长度。只要长度为二,就插入斜杠。因此斜杠被删除,然后重新插入。

您为什么不创建三个 TextField 并在其间插入斜线作为文本。这样的逻辑很难完善。敏锐的用户可以使用它来使您的应用程序崩溃,开发人员也可以插入恶意内容并利用此缺陷,因为处理逻辑也可能存在漏洞,所以...我认为最好使用最简单的(并且我认为更优雅的)构建方式。

正在删除/,但是一旦删除,文本的长度就会变成2或5。所以它会检查条件,

if (it.text.length == 2 || it.text.length == 5)

由于条件现在为真,/ 再次追加到文本中。所以它似乎根本没有被删除。

解决这个问题的一种方法是存储以前的文本长度并检查现在的文本长度是否大于以前的文本长度。

为此,在maxCharDate下面声明一个变量为

var previousTextLength = 0

并将嵌套的 if 条件更改为,

if ((it.text.length == 2 || it.text.length == 5) && it.text.length > previousTextLength)

最后更新 previousTextLength 变量。在 emailErrorVisible.value = false 下面添加

previousTextLength = it.text.length;

您可以做一些不同的事情,使用 onValueChange 定义最大字符数,并使用 visualTransformation 显示您喜欢的格式而不更改值在 TextField.

val maxChar = 8
TextField(
    singleLine = true,
    value = text,
    onValueChange = {
        if (it.length <= maxChar) text = it
    },
    visualTransformation = DateTransformation()
)

其中:

class DateTransformation() : VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText {
        return dateFilter(text)
    }
}

fun dateFilter(text: AnnotatedString): TransformedText {

    val trimmed = if (text.text.length >= 8) text.text.substring(0..7) else text.text
    var out = ""
    for (i in trimmed.indices) {
        out += trimmed[i]
        if (i % 2 == 1 && i < 4) out += "/"
    }

    val numberOffsetTranslator = object : OffsetMapping {
        override fun originalToTransformed(offset: Int): Int {
            if (offset <= 1) return offset
            if (offset <= 3) return offset +1
            if (offset <= 8) return offset +2
            return 10
        }

        override fun transformedToOriginal(offset: Int): Int {
            if (offset <=2) return offset
            if (offset <=5) return offset -1
            if (offset <=10) return offset -2
            return 8
        }
    }

    return TransformedText(AnnotatedString(out), numberOffsetTranslator)
}

接受 Jetpack Compose TextField 的任何类型掩码的 VisualTranformation 的实现:

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 == '#' }
        }
    }
}

使用方法:

@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 == '#' }
}

我不仅会建议日期掩码,还会建议更简单通用的输入掩码解决方案。

一个通用的格式化程序接口,用于实现任何类型的掩码。

interface MaskFormatter {
    fun format(textToFormat: String): String
}

实现我们自己的格式化程序。

object DateFormatter : MaskFormatter {
    override fun format(textToFormat: String): String {
        TODO("Format '01212022' into '01/21/2022'")
    }
}

object CreditCardFormatter : MaskFormatter {
    override fun format(textToFormat: String): String {
        TODO("Format '1234567890123456' into '1234 5678 9012 3456'")
    }
}

最后使用这个通用扩展函数来转换您的文本字段输入,您根本不需要关心偏移量。

internal fun MaskFormatter.toVisualTransformation(): VisualTransformation =
    VisualTransformation {
        val output = format(it.text)
        TransformedText(
            AnnotatedString(output),
            object : OffsetMapping {
                override fun originalToTransformed(offset: Int): Int = output.length
                override fun transformedToOriginal(offset: Int): Int = it.text.length
            }
        )
    }

一些用法示例:

// Date Example
private const val MAX_DATE_LENGTH = 8

@Composable
fun DateTextField() {
    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 Example
private const val MAX_CREDIT_CARD_LENGTH = 16

@Composable
fun CreditCardTextField() {
    var creditCard by remember { mutableStateOf("") }
    TextField(
        value = creditCard,
        onValueChange = {
            if (it.matches("^\d{0,$MAX_CREDIT_CARD_LENGTH}$".toRegex())) {
                creditCard = it
            }
        },
        visualTransformation = CreditCardFormatter.toVisualTransformation()
    )
}