Jetpack compose. How to format entered phone number with mask?

173 Views Asked by At

I'm trying to implement visual transformation to showing formatted phone number with mask.

The problem is that the mask is dynamic, and it can be received from the backend, depending on the characters that the client has already entered, so it takes some time to receive it.

Here is my visual transformation:

class PhoneVisualTransformation(val mask: String) :
    VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText {
        val source = text.text
        val formattedPhone = source.formatByMask(mask = mask)

        return TransformedText(
            text = AnnotatedString(source),
            offsetMapping = offsetFilter(text = formattedPhone),
        )
    }

    private fun offsetFilter(text: String): OffsetMapping {
        val numberOffsetTranslator = object : OffsetMapping {
            override fun originalToTransformed(offset: Int): Int {
                val transformedOffsets = text
                    .mapIndexedNotNull { index, c ->
                        index
                            .takeIf { !isFormattingCharacter(c) }
                            ?.plus(1)
                    }.let { offsetList ->
                        listOf(0) + offsetList
                    }

                return transformedOffsets[offset]
            }

            override fun transformedToOriginal(offset: Int): Int {
                return text
                    .mapIndexedNotNull { index, c ->
                        index.takeIf { isFormattingCharacter(c) }
                    }
                    .count { separatorIndex ->
                        separatorIndex < offset
                    }
                    .let { separatorCount ->
                        offset - separatorCount
                    }
            }
        }

        return numberOffsetTranslator
    }

    private fun isFormattingCharacter(char: Char): Boolean {
        return listOf('(', ')', '-', ' ').any { char == it }
    }
}

formatByMask is extension which use redmadrobot input mask library.

 InputTextField(
            modifier = modifier,
            input = state.phone,
            visualTransformation = PhoneVisualTransformation(
                mask = state.mask,
            ),
            onValueChange = {
                viewModel.onValueChange(it)
            },
        ) 

And my ViewModel:

fun onValueChange(newValue: String) {
  viewModelScope.launch {
    val phoneDetials = getPhoneDetailsByInput(newValue)
    _state.update {
       it.copy(phone = phoneDetails.phone, mask = phoneDetails.mask)
    }
}

} }

getPhoneDetailsByInput() is suspend function which returns mask from backend. The problem is that offset in my OffsetMapping is always 0.

What am I doing wrong ? Perhaps I took the wrong approach altogether. Please help me.

1

There are 1 best solutions below

1
KEMAL On
    fun formatPhoneNumber(phoneNumber: String): String {
        // Remove all non-digit characters
        val digits = phoneNumber.filter { it.isDigit() }
    
        // Apply formatting
        return buildString {
            for (i in digits.indices) {
                when (i) {
                    0 -> append('(')
                    3 -> append(") ")
                    6 -> append('-')
                }
                append(digits[i])
                if (i == 9) break // no more digits
            }
        }
    }

Then compose

@Composable
fun PhoneNumberTextField() {
    var phoneNumber by remember { mutableStateOf("") }

    TextField(
        value = phoneNumber,
        onValueChange = { 
            val formatted = formatPhoneNumber(it)
            if (formatted.length <= 14) { // (123) 456-7890 is 14 characters long
                phoneNumber = formatted
            }
        },
        label = { Text("Phone Number") },
        // Add other TextField properties as needed
    )
}