使用电话号码格式 NaN 屏蔽 EditText,如 PhoneNumberUtils

Posted

技术标签:

【中文标题】使用电话号码格式 NaN 屏蔽 EditText,如 PhoneNumberUtils【英文标题】:Mask an EditText with Phone Number Format NaN like in PhoneNumberUtils 【发布时间】:2011-05-08 08:33:06 【问题描述】:

我想让用户在 editText 中输入电话号码,以便在用户每次输入号码时动态更改格式。也就是说,当用户输入最多 4 位数字(如 7144)时,editText 显示“714-4”。 我希望在用户输入数字时动态更新editText以格式化###-###-####。如何才能做到这一点?另外,我处理的编辑文本不止一个。

【问题讨论】:

【参考方案1】:

在 Kotlin 中用于 android 的动态掩码。这个工作正常,严格符合电话号码掩码。你可以提供任何你想要的面具。

EDIT1:我有一个新版本,可以锁定用户在键盘上键入的不需要的字符。

/**
 * 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, "")
    

编辑 2: 单元测试

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

【讨论】:

【参考方案2】:

这是我的解决方案

如何在 Activity/Fragment 中运行(f.e in onViewCreated):

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

exampleIdField.editText?.addTextChangedListener(exampleIdValidator)

验证器类:

    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 = " "
        
    

布局:

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

结果是:

XX XXX XXX XXX

【讨论】:

虽然这段代码 sn-p 可以解决问题,但including an explanation 确实有助于提高帖子的质量。请记住,您正在为将来的读者回答问题,而这些人可能不知道您的代码建议的原因。也请尽量不要用解释性的 cmets 挤满你的代码,这会降低代码和解释的可读性!【参考方案3】:

此代码允许您输入带有掩码 ### - ### - ####(不带空格)的电话号码,并且这里还修复了删除电话数字的问题:

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) 
        );

布局:

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

【讨论】:

是的,这有效并且还处理了背压场景【参考方案4】:

上述解决方案没有考虑退格,因此当您在键入后删除一些数字时,格式往往会混乱。下面的代码纠正了这个问题。

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)  
    );

【讨论】:

【参考方案5】:

最简单的方法是使用内置的 Android PhoneNumberFormattingTextWatcher。

所以基本上你在代码中得到你的 EditText 并像这样设置你的文本观察器......

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

使用 PhoneNumberFormattingTextWatcher 的好处是它会根据您的语言环境正确格式化您的号码输入。

【讨论】:

如果您需要自定义掩码,您会发现此答案很有用:***.com/a/34907607/1013929 editTextPhone.addTextChangedListener(PhoneNumberFormattingTextWatcher()) Kotlin【参考方案6】:

我的脚本,示例取自这里description here


<android.support.design.widget.TextInputLayout
    android:id="@+id/numphone_layout"
    app:hintTextAppearance="@style/MyHintText"
    android:layout_
    android:layout_

    android:layout_marginTop="8dp">

    <android.support.design.widget.TextInputEditText
        android:id="@+id/edit_text_numphone"
        android:layout_
        android:layout_
        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;
        
    
;

【讨论】:

这是唯一有效的最佳解决方案【参考方案7】:

只需将以下内容添加到电话号码的 EditText 即可获得格式化的电话号码(###-###-####)

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, "-");
                
            
        
    );

【讨论】:

【参考方案8】:

以上答案是正确的,但它适用于特定国家/地区。如果有人想要这种格式的电话号码(###-###-####)。然后使用这个:

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) 

                
            );

在你的活动中声明String lastChar = " "

现在在你的edittext的xml中添加这一行

android:inputType="phone"

就是这样。

已编辑:如果您希望编辑文本长度限制为 10 位数字,请在下面添加行:

android:maxLength="12"

(是12,因为“-”会占用两次空间)

【讨论】:

这适用于 XXX-XXX-XXXX 格式的电话号码。只需复制粘贴即可。

以上是关于使用电话号码格式 NaN 屏蔽 EditText,如 PhoneNumberUtils的主要内容,如果未能解决你的问题,请参考以下文章

EditText电话号码格式化输入删除案例

Android 电话号码格式不适用于键盘和 EditText

从android中的edittext框中获取XXX-XXX-XXXX格式的电话号码

EditText 的 Android 电话号码掩码

如何将字符串号码格式化为美国电话号码格式

期望值中的 NaN,即使被屏蔽,也会在权重矩阵中引入 NaN