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

我有一个不能超过 10 个字符的 TextField,并且用户需要以“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() })
                },
                  .
                  .
                  .

它的工作原理是附加的“/”不会在按退格键时被删除,而其他字符会被删除。

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

stack overflow How to apply a mask date (mm/dd/yyyy) in TextField with Jetpack Compose?
原文答案
author avatar

接受的答案

您可以使用 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)
}

enter image description here


答案:

作者头像

/ 正在被删除,但是一旦您删除,文本的长度就会变为 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;
作者头像

这是因为您正在检查字符串的长度。只要长度为 2,就插入一个斜线。因此,斜线被删除并重新插入。

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

作者头像

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

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

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()
    )
}
作者头像

enter image description here

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