开发者

Mask an EditText with Phone Number Format NaN like in PhoneNumberUtils

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 7144, the editText shows "714-4". I would like the editText to be dynamically 开发者_运维问答updated to format ###-###-#### whenever the user inputs a digit. how can this be done? also, I am handling more than one editTexts.


Easiest way to do this is to use the built in Android PhoneNumberFormattingTextWatcher.

So basically you get your EditText in code and set your text watcher like this...

EditText inputField = (EditText) findViewById(R.id.inputfield);
inputField.addTextChangedListener(new PhoneNumberFormattingTextWatcher());

Nice thing about using PhoneNumberFormattingTextWatcher is that it will format your number entry correctly based on your locale.


Above answer is right but it works with country specific. if anyone want such formatted phone number(###-###-####). Then use this:

etPhoneNumber.addTextChangedListener(new TextWatcher() {
                @Override
                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                    int digits = etPhoneNumber.getText().toString().length();
                    if (digits > 1)
                        lastChar = etPhoneNumber.getText().toString().substring(digits-1);
                }

                @Override
                public void onTextChanged(CharSequence s, int start, int before, int count) {
                    int digits = etPhoneNumber.getText().toString().length();
                    Log.d("LENGTH",""+digits);
                    if (!lastChar.equals("-")) {
                        if (digits == 3 || digits == 7) {
                            etPhoneNumber.append("-");
                        }
                    }
                }

                @Override
                public void afterTextChanged(Editable s) {

                }
            });

Declare String lastChar = " " in your activity.

Now add this line in xml of your edittext

android:inputType="phone"

That's all.

Edited: If you want your edittext lenght to limit 10 digits add line below also:

android:maxLength="12"

(It is 12 because "-" will take space two times)


Just add the following to EditText for Phone Number to get a formatted phone number(###-###-####)

Phone.addTextChangedListener(new TextWatcher() {

        int length_before = 0;

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            length_before = s.length();
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {

        }

        @Override
        public void afterTextChanged(Editable s) {
            if (length_before < s.length()) {
                if (s.length() == 3 || s.length() == 7)
                    s.append("-");
                if (s.length() > 3) {
                    if (Character.isDigit(s.charAt(3)))
                        s.insert(3, "-");
                }
                if (s.length() > 7) {
                    if (Character.isDigit(s.charAt(7)))
                        s.insert(7, "-");
                }
            }
        }
    });


My script, example taken from here description here


<android.support.design.widget.TextInputLayout
    android:id="@+id/numphone_layout"
    app:hintTextAppearance="@style/MyHintText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"

    android:layout_marginTop="8dp">

    <android.support.design.widget.TextInputEditText
        android:id="@+id/edit_text_numphone"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/MyEditText"
        android:digits="+() 1234567890-"
        android:hint="@string/hint_numphone"
        android:inputType="phone"
        android:maxLength="17"
        android:textSize="14sp" />
</android.support.design.widget.TextInputLayout>

 @Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 TextInputEditText phone = (TextInputEditText) findViewById(R.id.edit_text_numphone);
 //Add to mask
    phone.addTextChangedListener(textWatcher);
}


   TextWatcher textWatcher = new TextWatcher() {
    private boolean mFormatting; // this is a flag which prevents the  stack overflow.
    private int mAfter;

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // nothing to do here..
    }

    //called before the text is changed...
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        //nothing to do here...
        mAfter  =   after; // flag to detect backspace..

    }

    @Override
    public void afterTextChanged(Editable s) {
        // Make sure to ignore calls to afterTextChanged caused by the work done below
        if (!mFormatting) {
            mFormatting = true;
            // using US or RU formatting...
            if(mAfter!=0) // in case back space ain't clicked...
            {
                String num =s.toString();
                String data = PhoneNumberUtils.formatNumber(num, "RU");
                if(data!=null)
                {
                    s.clear();
                    s.append(data);
                    Log.i("Number", data);//8 (999) 123-45-67 or +7 999 123-45-67
                }

            }
            mFormatting = false;
        }
    }
};


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()
    }
}


The above solutions do not take backspace into consideration so when you delete some numbers after typing, the format tends to mess up. Below code corrects this issue.

phoneNumberEditText.addTextChangedListener(new TextWatcher() {

        int beforeLength;

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            beforeLength = phoneNumberEditText.length();
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            int digits = phoneNumberEditText.getText().toString().length();
            if (beforeLength < digits && (digits == 3 || digits == 7)) {
                phoneNumberEditText.append("-");
            }
        }

        @Override
        public void afterTextChanged(Editable s) { }
    });


This code allow you enter phone number with mask ### - ### - #### (without spaces) and also here is fixed the issue with deletion of phone digits:

editText.addTextChangedListener(new TextWatcher() {
            final static String DELIMITER = "-";
            String lastChar;

            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                int digits = editText.getText().toString().length();
                if (digits > 1)
                    lastChar = editText.getText().toString().substring(digits-1);
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                int digits = editText.getText().length();
                // prevent input dash by user
                if (digits > 0 && digits != 4 && digits != 8) {
                    CharSequence last = s.subSequence(digits - 1, digits);
                    if (last.toString().equals(DELIMITER))
                        editText.getText().delete(digits - 1, digits);
                }
                // inset and remove dash
                if (digits == 3 || digits == 7) {
                    if (!lastChar.equals(DELIMITER))
                        editText.append("-"); // insert a dash
                    else
                        editText.getText().delete(digits -1, digits); // delete last digit with a dash
                }
                dataModel.setPhone(s.toString());
            }

            @Override
            public void afterTextChanged(Editable s) {}
        });

Layout:

<EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:imeOptions="actionDone"
            android:textAlignment="textStart"
            android:inputType="number"
            android:digits="-0123456789"
            android:lines="1"
            android:maxLength="12"/>


Here is my solution

How run in Activity/Fragment (f.e in onViewCreated):

//field in class
private val exampleIdValidator by lazy { ExampleIdWatcher(exampleIdField.editText!!) }

exampleIdField.editText?.addTextChangedListener(exampleIdValidator)

Validatior class:

    import android.text.Editable
    import android.text.TextWatcher
    import android.widget.EditText

    class ExampleIdWatcher(exampleIdInput: EditText) : TextWatcher {

        private var exampleIdInput: EditText = exampleIdInput

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        }

        override fun onTextChanged(userInput: CharSequence?, start: Int, before: Int, count: Int) {
            if (userInput!!.isNotEmpty() && !areSpacesCorrect(userInput)) {

                val stringTextWithoutWhiteSpaces: String = userInput.toString().replace(" ", "")

                val textSB: StringBuilder = StringBuilder(stringTextWithoutWhiteSpaces)

                when {
                    textSB.length > 8 -> {
                        setSpacesAndCursorPosition(textSB, 2, 6, 10)
                    }
                    textSB.length > 5 -> {
                        setSpacesAndCursorPosition(textSB, 2, 6)
                    }
                    textSB.length > 2 -> {
                        setSpacesAndCursorPosition(textSB, 2)
                    }
                }
            }
        }

        override fun afterTextChanged(s: Editable?) {
        }

        private fun setSpacesAndCursorPosition(textSB: StringBuilder, vararg ts: Int) {
            for (t in ts) // ts is an Array
                textSB.insert(t, SPACE_CHAR)
            val currentCursorPosition = getCursorPosition(exampleIdInput.selectionStart)
            exampleIdInput.setText(textSB.toString())
            exampleIdInput.setSelection(currentCursorPosition)
        }

        private fun getCursorPosition(currentCursorPosition: Int): Int {
            return if (EXAMPLE_ID_SPACE_CHAR_CURSOR_POSITIONS.contains(currentCursorPosition)) {
                currentCursorPosition + 1
            } else {
                currentCursorPosition
            }
        }

        private fun areSpacesCorrect(userInput: CharSequence?): Boolean {
            EXAMPLE_ID_SPACE_CHAR_INDEXES.forEach {
                if (userInput!!.length > it && userInput[it].toString() != SPACE_CHAR) {
                    return false
                }
            }
            return true
        }

        companion object {
            private val EXAMPLE_ID_SPACE_CHAR_INDEXES: List<Int> = listOf(2, 6, 10)
            private val EXAMPLE_ID_SPACE_CHAR_CURSOR_POSITIONS: List<Int> = EXAMPLE_ID_SPACE_CHAR_INDEXES.map { it + 1 }
            private const val SPACE_CHAR: String = " "
        }
    }

Layout:

    <com.google.android.material.textfield.TextInputEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:digits=" 0123456789"
        android:inputType="numberPassword"
        android:maxLength="14"
        tools:text="Example text" />

Result is:

XX XXX XXX XXX
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜