KevinMartinez
06/17/2023, 5:15 PMKevinMartinez
06/17/2023, 5:40 PMclass PhoneNumberVisualTransformation(
countryCode: String
) : VisualTransformation {
private val phoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode)
override fun filter(text: AnnotatedString): TransformedText {
if (text.isEmpty()) {
return TransformedText(AnnotatedString(EMPTY_STRING), OffsetMapping.Identity)
}
val transformation = reformat(text, Selection.getSelectionEnd(text))
return TransformedText(
AnnotatedString(transformation.formatted.orEmpty()),
object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return transformation.originalToTransformed[
offset.coerceIn(transformation.originalToTransformed.indices)
]
}
override fun transformedToOriginal(offset: Int): Int {
return transformation.transformedToOriginal[
offset.coerceIn(transformation.transformedToOriginal.indices)
]
}
}
)
}
private fun reformat(s: CharSequence, cursor: Int): Transformation {
phoneNumberFormatter.clear()
val curIndex = cursor - ONE_INT
var formatted: String? = null
var lastNonSeparator = ZERO_INT.toChar()
var hasCursor = false
s.forEachIndexed { index, char ->
if (PhoneNumberUtils.isNonSeparator(char)) {
if (lastNonSeparator.code != ZERO_INT) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor)
hasCursor = false
}
lastNonSeparator = char
}
if (index == curIndex) {
hasCursor = true
}
}
if (lastNonSeparator.code != ZERO_INT) {
formatted = getFormattedNumber(lastNonSeparator, hasCursor)
}
val originalToTransformed = mutableListOf<Int>()
val transformedToOriginal = mutableListOf<Int>()
var specialCharsCount = 0
formatted?.forEachIndexed { index, char ->
if (!PhoneNumberUtils.isNonSeparator(char)) {
specialCharsCount++
} else {
originalToTransformed.add(index)
}
transformedToOriginal.add(index - specialCharsCount)
}
originalToTransformed.add(originalToTransformed.maxOrNull()?.plus(ONE_INT) ?: ZERO_INT)
transformedToOriginal.add(transformedToOriginal.maxOrNull()?.plus(ONE_INT) ?: ZERO_INT)
return Transformation(formatted, originalToTransformed, transformedToOriginal)
}
private fun getFormattedNumber(lastNonSeparator: Char, hasCursor: Boolean): String? {
return if (hasCursor) {
phoneNumberFormatter.inputDigitAndRememberPosition(lastNonSeparator)
} else {
phoneNumberFormatter.inputDigit(lastNonSeparator)
}
}
private data class Transformation(
val formatted: String?,
val originalToTransformed: List<Int>,
val transformedToOriginal: List<Int>
)
}
Martin Sloup
06/19/2023, 9:05 AMPhoneNumberVisualTransformation
class by adding an additional step after formatting the phone number. Here's the updated code:
class PhoneNumberVisualTransformation(
countryCode: String
) : VisualTransformation {
private val phoneNumberFormatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(countryCode)
override fun filter(text: AnnotatedString): TransformedText {
if (text.isEmpty()) {
return TransformedText(AnnotatedString(EMPTY_STRING), OffsetMapping.Identity)
}
val transformation = reformat(text, Selection.getSelectionEnd(text))
return TransformedText(
AnnotatedString(transformation.formatted.orEmpty()),
object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return transformation.originalToTransformed[
offset.coerceIn(transformation.originalToTransformed.indices)
]
}
override fun transformedToOriginal(offset: Int): Int {
return transformation.transformedToOriginal[
offset.coerceIn(transformation.transformedToOriginal.indices)
]
}
}
)
}
private fun reformat(s: CharSequence, cursor: Int): Transformation {
phoneNumberFormatter.clear()
val curIndex = cursor - 1
var formatted: String? = null
var lastNonSeparator = '0'
var hasCursor = false
s.forEachIndexed { index, char ->
if (PhoneNumberUtils.isNonSeparator(char)) {
if (lastNonSeparator != '0') {
formatted = getFormattedNumber(lastNonSeparator, hasCursor)
hasCursor = false
}
lastNonSeparator = char
}
if (index == curIndex) {
hasCursor = true
}
}
if (lastNonSeparator != '0') {
formatted = getFormattedNumber(lastNonSeparator, hasCursor)
}
val originalToTransformed = mutableListOf<Int>()
val transformedToOriginal = mutableListOf<Int>()
var specialCharsCount = 0
formatted?.forEachIndexed { index, char ->
if (!PhoneNumberUtils.isNonSeparator(char)) {
specialCharsCount++
} else {
originalToTransformed.add(index)
}
transformedToOriginal.add(index - specialCharsCount)
}
originalToTransformed.add(originalToTransformed.maxOrNull()?.plus(1) ?: 0)
transformedToOriginal.add(transformedToOriginal.maxOrNull()?.plus(1) ?: 0)
// Always put the cursor at the end
val cursorPosition = originalToTransformed.size - 1
return Transformation(formatted, originalToTransformed, transformedToOriginal, cursorPosition)
}
private fun getFormattedNumber(lastNonSeparator: Char, hasCursor: Boolean): String? {
return if (hasCursor) {
phoneNumberFormatter.inputDigitAndRememberPosition(lastNonSeparator)
} else {
phoneNumberFormatter.inputDigit(lastNonSeparator)
}
}
private data class Transformation(
val formatted: String?,
val originalToTransformed: List<Int>,
val transformedToOriginal: List<Int>,
val cursorPosition: Int
)
}
In the reformat
function, I added a cursorPosition
variable to keep track of the position of the cursor. Then, at the end of the function, I included a cursorPosition
field in the Transformation
data class to store the calculated cursor position.
In the filter
function, after creating the TransformedText
object, you can set the cursor position using the cursorPosition
field:
return TransformedText(
AnnotatedString(transformation.formatted.orEmpty()),
object :
OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return transformation.originalToTransformed[offset.coerceIn(transformation.originalToTransformed.indices)]
}
override fun transformedToOriginal(offset: Int): Int {
return transformation.transformedToOriginal[offset.coerceIn(transformation.transformedToOriginal.indices)]
}
},
cursorPosition
)
With this modification, the cursor will always be positioned at the end of the text after reformatting.KevinMartinez
06/19/2023, 12:51 PM