I want to make user inputted phone number in an editText to dynamically change format every time the user inputs a number. That is, when user inputs up to 4 digits, like 714
Dynamic Mask for Android in Kotlin. This one is working fine and strictly fitting the phone number mask. You can provide any mask you whish.
EDIT1: I have a new version that locks event the unwanted chars typed by the user on the keyboard.
/**
* Text watcher allowing strictly a MASK with '#' (example: (###) ###-####
*/
class NumberTextWatcher(private var mask: String) : TextWatcher {
companion object {
const val MASK_CHAR = '#'
}
// simple mutex
private var isCursorRunning = false
private var isDeleting = false
override fun afterTextChanged(s: Editable?) {
if (isCursorRunning || isDeleting) {
return
}
isCursorRunning = true
s?.let {
val onlyDigits = removeMask(it.toString())
it.clear()
it.append(applyMask(mask, onlyDigits))
}
isCursorRunning = false
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
isDeleting = count > after
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
private fun applyMask(mask: String, onlyDigits: String): String {
val maskPlaceholderCharCount = mask.count { it == MASK_CHAR }
var maskCurrentCharIndex = 0
var output = ""
onlyDigits.take(min(maskPlaceholderCharCount, onlyDigits.length)).forEach { c ->
for (i in maskCurrentCharIndex until mask.length) {
if (mask[i] == MASK_CHAR) {
output += c
maskCurrentCharIndex += 1
break
} else {
output += mask[i]
maskCurrentCharIndex = i + 1
}
}
}
return output
}
private fun removeMask(value: String): String {
// extract all the digits from the string
return Regex("\\D+").replace(value, "")
}
}
EDIT 2: Unit tests
class NumberTextWatcherTest {
@Test
fun phone_number_test() {
val phoneNumberMask = "(###) ###-####"
val phoneNumberTextWatcher = NumberTextWatcher(phoneNumberMask)
val input = StringBuilder()
val expectedResult = "(012) 345-6789"
var result = ""
// mimic typing 10 digits
for (i in 0 until 10) {
input.append(i)
result = mimicTextInput(phoneNumberTextWatcher, result, i.toString()) ?: ""
}
Assert.assertEquals(input.toString(), "0123456789")
Assert.assertEquals(result, expectedResult)
}
@Test
fun credit_card_test() {
val creditCardNumberMask = "#### #### #### ####"
val creditCardNumberTextWatcher = NumberTextWatcher(creditCardNumberMask)
val input = StringBuilder()
val expectedResult = "0123 4567 8901 2345"
var result = ""
// mimic typing 16 digits
for (i in 0 until 16) {
val value = i % 10
input.append(value)
result = mimicTextInput(creditCardNumberTextWatcher, result, value.toString()) ?: ""
}
Assert.assertEquals(input.toString(), "0123456789012345")
Assert.assertEquals(result, expectedResult)
}
@Test
fun date_test() {
val dateMask = "####/##/##"
val dateTextWatcher = NumberTextWatcher(dateMask)
val input = "20200504"
val expectedResult = "2020/05/04"
val initialInputValue = ""
val result = mimicTextInput(dateTextWatcher, initialInputValue, input)
Assert.assertEquals(result, expectedResult)
}
@Test
fun credit_card_expiration_date_test() {
val creditCardExpirationDateMask = "##/##"
val creditCardExpirationDateTextWatcher = NumberTextWatcher(creditCardExpirationDateMask)
val input = "1121"
val expectedResult = "11/21"
val initialInputValue = ""
val result = mimicTextInput(creditCardExpirationDateTextWatcher, initialInputValue, input)
Assert.assertEquals(result, expectedResult)
}
private fun mimicTextInput(textWatcher: TextWatcher, initialInputValue: String, input: String): String? {
textWatcher.beforeTextChanged(initialInputValue, initialInputValue.length, initialInputValue.length, input.length + initialInputValue.length)
val newText = initialInputValue + input
textWatcher.onTextChanged(newText, 1, newText.length - 1, 1)
val editable: Editable = SpannableStringBuilder(newText)
textWatcher.afterTextChanged(editable)
return editable.toString()
}
}