Search code examples
androidkotlinandroid-studioandroid-jetpack-composeandroid-compose-textfield

Implementing TextField with TransformedText - causing a crash when I exit focus, then bring the field back into focus


So i'm implementing a TextField with TransformedText to generate formatting for a phone number input. It works great for the 'happy path'. When the phone number is entered, you can go out of and back into focus on the TextField with no issue.

enter image description here

However, if I were to half fill in the field, as below, go out of focus and go back into focus the app will crash

enter image description here

The error message in the StackTrace is as follows:

java.lang.IllegalStateException: OffsetMapping.transformedToOriginal returned invalid mapping: 15 -> 10 is not in range of original text [0, 5]

So i'm aware the issue is with the number of characters I inserted into the TextField in this example (5), i'm just not sure how to catch this refocus to stop it crashing when I go back into focus, below is the code for the TextField and the TransformedText formatter

TextField

@Composable
private fun DisplayNumberTextField() {
    
    var num by remember { mutableStateOf("") }

    OutlinedTextField(
        value = num,
        onValueChange = {
            num = it
        },
        modifier = Modifier
            .fillMaxWidth(),
        singleLine = true,
        visualTransformation = {
            if (it.isBlank()) {
                TransformedText(it, OffsetMapping.Identity)
            } else {
                phoneNumberInputFormatter(it)
            }
        },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
    )
}

Formatter

    private fun phoneNumberInputFormatter(text: AnnotatedString): TransformedText {

    val mask = "+1 xxx-xxx-xxxx"

    val trimmed = if (text.text.length >= 10) text.text.substring(0..9) else text.text

    val annotatedString = AnnotatedString.Builder().run {
        for (i in trimmed.indices) {
            if (i == 0) {
                append("+1 ")
            }
            append(trimmed[i])
            if (i == 2 || i == 5) {
                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 <= 0) return offset + 2
            if (offset <= 3) return offset + 3
            if (offset <= 7) return offset + 4
            if (offset <= 10) return offset + 5
            return 15
        }

        override fun transformedToOriginal(offset: Int): Int {
            if (offset <= 0) return offset - 2
            if (offset <= 3) return offset - 3
            if (offset <= 7) return offset - 4
            if (offset <= 10) return offset - 5
            return 10
        }
    }
    return TransformedText(annotatedString, phoneNumberOffsetTranslator)
}

I've come to my solution from various SO threads, for example as well as other sources online, for example , and can't seem to see how to fix the OffsetTranslator to avoid this crash.

Any help would be much appreciated


Solution

  • Change OffsetMapping to this,

     val phoneNumberOffsetTranslator = object : OffsetMapping {
        override fun originalToTransformed(offset: Int): Int {
            return when {
                offset == 0 -> return 0
                offset <= 3 -> offset + 3
                offset <= 6 -> offset + 4
                else -> offset + 5
            }
        }
    
        override fun transformedToOriginal(offset: Int): Int {
            return text.length
        }
    }