Jetpack compose 中的电话号码视觉转换

发布于 2025-01-10 04:48:13 字数 247 浏览 0 评论 0原文

如何在jetpack compose中实现电话号码可视化转换?我已阅读这篇文章< /a> 为卡号。

我想将我的电话号码格式化为这样 xx xxx xx xx

How can I implement phone number visual transformation in jetpack compose? I have read this article for the card number.

And I want to format my phone number like this xx xxx xx xx

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

任性一次 2025-01-17 04:48:14

动态电话号码格式化程序。

结果

一些使用不同掩码的示例。

@Composable
fun LoginScreen() {
    var phoneNumber by rememberSaveable { mutableStateOf("") }
    Column {
        PhoneField(phoneNumber,
            mask = "000 000 00 00",
            maskNumber = '0',
            onPhoneChanged = { phoneNumber = it })
        Spacer(modifier = Modifier.padding(8.dp))

        PhoneField(phoneNumber,
            mask = "(000) 000 00 00",
            maskNumber = '0',
            onPhoneChanged = { phoneNumber = it })
        Spacer(modifier = Modifier.padding(8.dp))

        PhoneField(phoneNumber,
            mask = "+7-000-000-00-00",
            maskNumber = '0',
            onPhoneChanged = { phoneNumber = it })
    }
}

@Composable
fun PhoneField(
    phone: String,
    modifier: Modifier = Modifier,
    mask: String = "000 000 00 00",
    maskNumber: Char = '0',
    onPhoneChanged: (String) -> Unit
) {
    TextField(
        value = phone,
        onValueChange = { it ->
            onPhoneChanged(it.take(mask.count { it == maskNumber }))
        },
        label = {
            Text(text = "Phone number")
        },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
        visualTransformation = PhoneVisualTransformation(mask, maskNumber),
        modifier = modifier.fillMaxWidth(),
    )
}

class PhoneVisualTransformation(val mask: String, val maskNumber: Char) : VisualTransformation {

    private val maxLength = mask.count { it == maskNumber }

    override fun filter(text: AnnotatedString): TransformedText {
        val trimmed = if (text.length > maxLength) text.take(maxLength) else text

        val annotatedString = buildAnnotatedString {
            if (trimmed.isEmpty()) return@buildAnnotatedString

            var maskIndex = 0
            var textIndex = 0
            while (textIndex < trimmed.length && maskIndex < mask.length) {
                if (mask[maskIndex] != maskNumber) {
                    val nextDigitIndex = mask.indexOf(maskNumber, maskIndex)
                    append(mask.substring(maskIndex, nextDigitIndex))
                    maskIndex = nextDigitIndex
                }
                append(trimmed[textIndex++])
                maskIndex++
            }
        }

        return TransformedText(annotatedString, PhoneOffsetMapper(mask, maskNumber))
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is PhonedVisualTransformation) return false
        if (mask != other.mask) return false
        if (maskNumber != other.maskNumber) return false
        return true
    }

    override fun hashCode(): Int {
        return mask.hashCode()
    }
}

private class PhoneOffsetMapper(val mask: String, val numberChar: Char) : OffsetMapping {

    override fun originalToTransformed(offset: Int): Int {
        var noneDigitCount = 0
        var i = 0
        while (i < offset + noneDigitCount) {
            if (mask[i++] != numberChar) noneDigitCount++
        }
        return offset + noneDigitCount
    }

    override fun transformedToOriginal(offset: Int): Int =
        offset - mask.take(offset).count { it != numberChar }
}

The dynamic phone number formatter.

The result

Some examples with different masks.

@Composable
fun LoginScreen() {
    var phoneNumber by rememberSaveable { mutableStateOf("") }
    Column {
        PhoneField(phoneNumber,
            mask = "000 000 00 00",
            maskNumber = '0',
            onPhoneChanged = { phoneNumber = it })
        Spacer(modifier = Modifier.padding(8.dp))

        PhoneField(phoneNumber,
            mask = "(000) 000 00 00",
            maskNumber = '0',
            onPhoneChanged = { phoneNumber = it })
        Spacer(modifier = Modifier.padding(8.dp))

        PhoneField(phoneNumber,
            mask = "+7-000-000-00-00",
            maskNumber = '0',
            onPhoneChanged = { phoneNumber = it })
    }
}

@Composable
fun PhoneField(
    phone: String,
    modifier: Modifier = Modifier,
    mask: String = "000 000 00 00",
    maskNumber: Char = '0',
    onPhoneChanged: (String) -> Unit
) {
    TextField(
        value = phone,
        onValueChange = { it ->
            onPhoneChanged(it.take(mask.count { it == maskNumber }))
        },
        label = {
            Text(text = "Phone number")
        },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
        visualTransformation = PhoneVisualTransformation(mask, maskNumber),
        modifier = modifier.fillMaxWidth(),
    )
}

class PhoneVisualTransformation(val mask: String, val maskNumber: Char) : VisualTransformation {

    private val maxLength = mask.count { it == maskNumber }

    override fun filter(text: AnnotatedString): TransformedText {
        val trimmed = if (text.length > maxLength) text.take(maxLength) else text

        val annotatedString = buildAnnotatedString {
            if (trimmed.isEmpty()) return@buildAnnotatedString

            var maskIndex = 0
            var textIndex = 0
            while (textIndex < trimmed.length && maskIndex < mask.length) {
                if (mask[maskIndex] != maskNumber) {
                    val nextDigitIndex = mask.indexOf(maskNumber, maskIndex)
                    append(mask.substring(maskIndex, nextDigitIndex))
                    maskIndex = nextDigitIndex
                }
                append(trimmed[textIndex++])
                maskIndex++
            }
        }

        return TransformedText(annotatedString, PhoneOffsetMapper(mask, maskNumber))
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is PhonedVisualTransformation) return false
        if (mask != other.mask) return false
        if (maskNumber != other.maskNumber) return false
        return true
    }

    override fun hashCode(): Int {
        return mask.hashCode()
    }
}

private class PhoneOffsetMapper(val mask: String, val numberChar: Char) : OffsetMapping {

    override fun originalToTransformed(offset: Int): Int {
        var noneDigitCount = 0
        var i = 0
        while (i < offset + noneDigitCount) {
            if (mask[i++] != numberChar) noneDigitCount++
        }
        return offset + noneDigitCount
    }

    override fun transformedToOriginal(offset: Int): Int =
        offset - mask.take(offset).count { it != numberChar }
}
放血 2025-01-17 04:48:14

您只需根据您需要的模式修改您提供的示例链接中的一些参数即可。您需要考虑所需的最大长度,每个部分之间需要多少空间。

例如,在您给定的链接中: http://zenandroid.io/using-the-jetpack-composes-visualtransformation-to-create-a-credit-card-text-input/

  1. 他们在 AnnotatedString.Builder() 您需要在 1、4、6 上使用它。
  2. 然后他们添加了 2 个空格,这就是为什么他们在 originalToTransformed 中添加了 2,4,6 之类的空格,但是您需要 1,2,3 & transformedToOriginal 中的推导相同

完整代码示例:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Test()
                }
            }
        }
    }
}


@Composable
fun Test() {

    var mobileNumber by rememberSaveable { mutableStateOf("") }
    Column {
        Row(modifier = Modifier.padding(all = 10.dp)) {
            Text(
                text = "Mobile number",
                fontSize = 14.sp,
                modifier = Modifier.weight(1f)
            )
            BasicTextField(
                value = mobileNumber,
                onValueChange = { mobileNumber = it },
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
                visualTransformation = { mobileNumberFilter(it) }
            )
        }
        Box(
            modifier = Modifier
                .height(1.dp)
                .padding(start = 10.dp)
                .fillMaxWidth()
                .background(Color.Gray)
        )

        Spacer(modifier = Modifier.height(20.dp))
        Text(text = "Actual value:\n$mobileNumber")
    }

}

const val mask = "xx xxx xx xx"

fun mobileNumberFilter(text: AnnotatedString): TransformedText {
    // change the length
    val trimmed =
        if (text.text.length >= 9) text.text.substring(0..8) else text.text

    val annotatedString = AnnotatedString.Builder().run {
        for (i in trimmed.indices) {
            append(trimmed[i])
            if (i == 1 || i == 4 || i == 6) {
                append(" ")
            }
        }
        pushStyle(SpanStyle(color = Color.LightGray))
        append(mask.takeLast(mask.length - length))
        toAnnotatedString()
    }

    val phoneNumberOffsetTranslator = object : OffsetMapping {
        override fun originalToTransformed(offset: Int): Int {
            if (offset <= 1) return offset
            if (offset <= 4) return offset + 1
            if (offset <= 6) return offset + 2
            if (offset <= 9) return offset + 3
            return 12
        }

        override fun transformedToOriginal(offset: Int): Int {
            if (offset <= 1) return offset
            if (offset <= 4) return offset - 1
            if (offset <= 6) return offset - 2
            if (offset <= 9) return offset - 3
            return 9
        }
    }

    return TransformedText(annotatedString, phoneNumberOffsetTranslator)
}

输出:

视觉转换

You can just modify some params from the example link that you provided according to the pattern you need. You need to consider the max length you want, how many spaces you need between each section.

For example in your given link here: http://zenandroid.io/using-the-jetpack-composes-visualtransformation-to-create-a-credit-card-text-input/

  1. They're adding space after every 4th character in AnnotatedString.Builder() You need it on 1, 4, 6.
  2. then they've added 2 spaces that's why they're adding spaces like 2,4,6 in originalToTransformed but you need 1,2,3 & same for deducting in transformedToOriginal

Complete code example:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Test()
                }
            }
        }
    }
}


@Composable
fun Test() {

    var mobileNumber by rememberSaveable { mutableStateOf("") }
    Column {
        Row(modifier = Modifier.padding(all = 10.dp)) {
            Text(
                text = "Mobile number",
                fontSize = 14.sp,
                modifier = Modifier.weight(1f)
            )
            BasicTextField(
                value = mobileNumber,
                onValueChange = { mobileNumber = it },
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
                visualTransformation = { mobileNumberFilter(it) }
            )
        }
        Box(
            modifier = Modifier
                .height(1.dp)
                .padding(start = 10.dp)
                .fillMaxWidth()
                .background(Color.Gray)
        )

        Spacer(modifier = Modifier.height(20.dp))
        Text(text = "Actual value:\n$mobileNumber")
    }

}

const val mask = "xx xxx xx xx"

fun mobileNumberFilter(text: AnnotatedString): TransformedText {
    // change the length
    val trimmed =
        if (text.text.length >= 9) text.text.substring(0..8) else text.text

    val annotatedString = AnnotatedString.Builder().run {
        for (i in trimmed.indices) {
            append(trimmed[i])
            if (i == 1 || i == 4 || i == 6) {
                append(" ")
            }
        }
        pushStyle(SpanStyle(color = Color.LightGray))
        append(mask.takeLast(mask.length - length))
        toAnnotatedString()
    }

    val phoneNumberOffsetTranslator = object : OffsetMapping {
        override fun originalToTransformed(offset: Int): Int {
            if (offset <= 1) return offset
            if (offset <= 4) return offset + 1
            if (offset <= 6) return offset + 2
            if (offset <= 9) return offset + 3
            return 12
        }

        override fun transformedToOriginal(offset: Int): Int {
            if (offset <= 1) return offset
            if (offset <= 4) return offset - 1
            if (offset <= 6) return offset - 2
            if (offset <= 9) return offset - 3
            return 9
        }
    }

    return TransformedText(annotatedString, phoneNumberOffsetTranslator)
}

Output:

visual transformation

洒一地阳光 2025-01-17 04:48:14

对于北美版本:

const val mask = "(xxx) xxx-xxxx"
fun mobileNumberFilter(text: AnnotatedString, formType: FormType): 
TransformedText {
    if (formType != FormType.SHIPPING_PHONE) {
        return VisualTransformation.None.filter(text)
    }

    // change the length
    val trimmed =
    if (text.text.length >= 14) text.text.substring(0..13) else text.text

    val annotatedString = AnnotatedString.Builder().run {
        for (i in trimmed.indices) {
            val trimmedPortion = trimmed[i]
            if (i == 0) {
               append("($trimmedPortion")
            } else {
               append(trimmedPortion)
            }
            if (i == 2) {
               append(") ")
            }
            if (i == 5) {
               append("-")
            }
        }
        pushStyle(
            SpanStyle(color = Color.LightGray)
        )
        try {
            append(mask.takeLast(mask.length - length))
        } catch (e: IllegalArgumentException) {
            Timber.d(e.localizedMessage?.plus(" reached end of phone number"))
        }

        toAnnotatedString()
    }

    val translator = object : OffsetMapping {
        override fun originalToTransformed(offset: Int): Int {

            if (offset <= 1) return offset
            if (offset <= 4) return offset + 1
            if (offset <= 9) return offset + 2
            return 14

        }

        override fun transformedToOriginal(offset: Int): Int {

            if (offset <= 1) return offset
            if (offset <= 4) return offset - 1
            if (offset <= 9) return offset - 2
            return 14

        }
    }

    return TransformedText(annotatedString, translator)
}

要获得更清晰的版本,请参阅这篇 Medium 文章:

添加电话号码视觉转换

For a north American version:

const val mask = "(xxx) xxx-xxxx"
fun mobileNumberFilter(text: AnnotatedString, formType: FormType): 
TransformedText {
    if (formType != FormType.SHIPPING_PHONE) {
        return VisualTransformation.None.filter(text)
    }

    // change the length
    val trimmed =
    if (text.text.length >= 14) text.text.substring(0..13) else text.text

    val annotatedString = AnnotatedString.Builder().run {
        for (i in trimmed.indices) {
            val trimmedPortion = trimmed[i]
            if (i == 0) {
               append("($trimmedPortion")
            } else {
               append(trimmedPortion)
            }
            if (i == 2) {
               append(") ")
            }
            if (i == 5) {
               append("-")
            }
        }
        pushStyle(
            SpanStyle(color = Color.LightGray)
        )
        try {
            append(mask.takeLast(mask.length - length))
        } catch (e: IllegalArgumentException) {
            Timber.d(e.localizedMessage?.plus(" reached end of phone number"))
        }

        toAnnotatedString()
    }

    val translator = object : OffsetMapping {
        override fun originalToTransformed(offset: Int): Int {

            if (offset <= 1) return offset
            if (offset <= 4) return offset + 1
            if (offset <= 9) return offset + 2
            return 14

        }

        override fun transformedToOriginal(offset: Int): Int {

            if (offset <= 1) return offset
            if (offset <= 4) return offset - 1
            if (offset <= 9) return offset - 2
            return 14

        }
    }

    return TransformedText(annotatedString, translator)
}

For an even cleaner version, see this Medium Article:

Adding Phone Number Visual transformation

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文