美文网首页
iOS、Android 实现电话号码格式化操作

iOS、Android 实现电话号码格式化操作

作者: l蓝色梦幻 | 来源:发表于2018-07-05 18:25 被阅读208次

控件简介

最近研究如何根据配置好的字符串去格式化电话号码等信息。由于项目要求,我们格式化的字符串是客户在网页端自定义的。因此要求我们这边需要针对不同的配置进行动态的格式化字符串。例如:需要格式化为 +86 XXX-XXXX-XXXX, X 替换为 number 去格式化字符串。网络上搜索了一下,基本上没有成熟的代码可用。索性,自己实现一套。

思路:

在 TextField 的代理方法中,func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool。我们可以在这个方法中拦截掉输入请求,自己管理字符串的键入与显示。这样就可以实现任意功能。在方法中,我的思路是:

  1. 根据当前未做变化前的显示字符串中计算出未格式化的数字字符串
  2. 根据当前显示字符串中的 Range 计算在数字字符串中的 Range
  3. 操作数字字符串中的 Range,并获取在数字字符串上的光标停留位置
  4. 生成显示的字符串,根据数字字符串的光标位置计算显示字符串的光标位置。

iOS 代码部分

代码部分:

  1. 根据原来的字符串生成未格式化的数字字符串

    private func getOriString() -> String {
        var str = ""
        for index in 0..<phoneFormate.count {
            let start = phoneFormate.index(phoneFormate.startIndex, offsetBy:index)
            let end = phoneFormate.index(start, offsetBy:1)
            let x = String(phoneFormate[start..<end])
            if textField.text!.count > index {
                let y = String(textField.text![start..<end])
                if String(x) == "X" {
                    str += y
                }
            }
        }
        return str
    }
    
  2. 根据字符串操作的 Range 计算在未格式化字符串上操作的 Range。

     private func getOriRange(_ range: NSRange) -> NSRange {
         var myLocation = 0
         var richLocation = false
         var currentLength: Int?
         var oriLength: Int?
         for index in 0..<phoneFormate.count {
             let start = phoneFormate.index(phoneFormate.startIndex, offsetBy:index)
             let end = phoneFormate.index(start, offsetBy:1)
             let x = String(phoneFormate[start..<end])
             
             if range.length == currentLength {
                 return NSRange(location: myLocation, length: oriLength ?? 0)
             }
             if range.location == index {
                 richLocation = true
                 currentLength = 0
                 oriLength = 0
                 if range.length == 0 {
                     return NSRange(location: myLocation, length: 0)
                 }
             }
             if String(x) == "X" {
                 if !richLocation {
                     myLocation += 1
                 }
                 if oriLength != nil {
                     oriLength = oriLength! + 1
                 }
             }
             
             if currentLength != nil {
                 currentLength = currentLength! + 1
             }
         }
         return NSRange(location: myLocation, length: oriLength ?? 0)
     }
    
  3. 操作未格式化的字符串,并返回最终光标在未格式化字符串上的位置

    private func operationOriString(oriStr: String, oriRange: NSRange, string: String) -> Int {
        var selectedIndex = 0
        if string.count == 0 {
            origineString = (oriStr as NSString).replacingCharacters(in: oriRange, with: string)
            selectedIndex = oriRange.location
        } else {
            let start = oriStr.index(oriStr.startIndex, offsetBy:oriRange.location + oriRange.length)
            let firstStr = String(oriStr[oriStr.startIndex..<start])
            let lastStr = String(oriStr[start..<oriStr.endIndex])
            
            selectedIndex = firstStr.count + 1
            origineString = firstStr + string + lastStr
        }
        if origineString.count > formateInfo.numberCount {
            origineString = String(origineString[origineString.startIndex..<origineString.index(origineString.startIndex, offsetBy:formateInfo.numberCount)])
        }
        return selectedIndex
    }
    
  4. 生成格式化后的字符串,并根据原始字符串的光标位置生成格式化后字符串的光标

    private func generateStr(_ oriIndex: Int) -> (String, Int) {
        var showStr = ""
        var index = 0
        var selectedIndex = 0
        for f in 0..<phoneFormate.count {
            let start = phoneFormate.index(phoneFormate.startIndex, offsetBy:f)
            let end = phoneFormate.index(start, offsetBy:1)
            let x = String(phoneFormate[start..<end])
            if oriIndex == index {
                selectedIndex = f
            }
            if String(x) == "X" {
                if origineString.count > index {
                    let start = origineString.index(origineString.startIndex, offsetBy:index)
                    if origineString.count > 1 {
                        let end = origineString.index(start, offsetBy:1)
                        showStr += String(origineString[start..<end])
                    } else {
                        showStr += origineString
                    }
                    index += 1
                } else {
                    break
                }
            } else {
                if origineString.count > index {
                    showStr += x
                } else {
                    break
                }
            }
        }
        if oriIndex == index && selectedIndex == 0 {
            selectedIndex = phoneFormate.count
        }
        return (showStr, selectedIndex)
    }
    
  5. 总的调用函数。

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        // 没有格式化,当做普通的 TextField 使用
        if self.phoneFormate == "" && self.regularFormate == "" {
            return true
        }
        
        // 字符串超长,并且当前操作是输入操作,光标放置到最后。
        if origineString.count >= formateInfo.numberCount && string.count > 0 {
            textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument)
            return false
        }
        // 手动管理修改字符串。
        self.modifyValueIn(range: range, string: string)
        // 发送事件,外部可以获取实时字符串
        sendActions(for: .editingChanged)
        
        return false
    }
    
    public func modifyValueIn(range: NSRange, string: String)  {
        // 1. 提取原始字符串
        let oriStr = self.getOriString()
        
        // 2. 根据 range 生成原始字符串 range
        let oriRange: NSRange = self.getOriRange(range)
        
        // 3. 操作原始字符串, 并获取在显示字符串中字符光标的位置
        let selectedIndex = self.operationOriString(oriStr: oriStr, oriRange: oriRange, string: string)
        
        // 4. 生成显示字符串
        let showStr = self.generateStr(selectedIndex)
        
        // 5. 计算最终的光标移位
        textField.insertText(showStr.0)
        textField.text = showStr.0
        if string.count == 0 {
            textField.selectedTextRange = textField.textRange(from: textField.endOfDocument, to: textField.endOfDocument)
        } else {
            let newPosition = textField.position(from: textField.beginningOfDocument, offset: showStr.1)
            textField.selectedTextRange = textField.textRange(from: newPosition!, to: newPosition!)
        }
    }
    

Android 代码部分

Android 思路与 iOS 版本的思路相同。不过不同之处在于,iOS 使用的是代理方法中的 func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool。拦截掉输入请求。而 Android 中并没有对应的方法。但是 Android 有着 TextWatcher 与 iOS 的代理方法类似。经过一些测试,我对 TextWatcher 的实现做了一部分修改,可以实现与 iOS 类似的功能。

  1. 针对 TextWatcher 做出计算类的修改, 在 afterTextChanged 方法中实现计算方法就可以了。

    private class PhoneFormateInfo(var numberCount: Int = 0)
    private class NSRange(var location: Int, var length: Int)
    private class GenerateStringStatus(var showStr: String, var selectedIndex: Int)
    
    private var beginStr: String? = null
    private var replacementString: String = ""
    private var range: NSRange? = null
    
    private fun reset() {
        range = null
        beginStr = null
        replacementString = ""
    }
    
    override fun afterTextChanged(s: Editable?) {
        if (phoneFormate == "" && regularFormate == "") {
            reset()
            return
        }
        if (origineString.length >= formateInfo.numberCount && replacementString!!.isNotEmpty()) {
            editView.removeTextChangedListener(this)
            editView.setText(beginStr!!)
            editView.setSelection(beginStr!!.length)
            textUpdateListener?.textDidChanged(beginStr!!)
            judgeRegular()
            editView.addTextChangedListener(this)
            reset()
            return
        }
    
        modifyValueIn(range!!, replacementString!!)
        reset()
    
        judgeRegular()
    }
    
    
    // 输入框的原内容字符串S,从索引位置start开始,有count个字符即将被替换,替换这个count个字符的新的字符个数为after。注意:S是变化之前的输入框内容。
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        beginStr = s.toString()
        range = if (count > 0) {
            NSRange(start, count)
        } else {
            NSRange(start, 0)
        }
    }
    
    // 在变化时的新的字符串S里,从索引位置start开始,有count个字符,是替换了原来的before个字符的。注意:S是变化之后的输入框内容
    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        if (count > 0) {
            val string = s.toString().substring(start, start + count)
            for (index in 0 until string.length) {
                val charStr = string[index]
                val num = charStr.toInt()
                if (num in 48..57) {
                    replacementString += charStr.toString()
                }
            }
        }
    }
    
  2. 同样的思路,实现格式化字符串

    /// 生成原始字符串
    private fun getOriString(): String {
        var str = ""
        for (index in 0 until phoneFormate.length) {
            val x = phoneFormate[index].toString()
            if (beginStr!!.length > index) {
                val y = beginStr!![index].toString()
                if (x == "X") {
                    str += y
                }
            }
        }
        return str
    }
    
    // 根据字符串操作范围生成原始字符串操作范围
    private fun getOriRange(range: NSRange): NSRange {
        var myLocation = 0
        var richLocation = false
        var currentLength: Int? = null
        var oriLength: Int? = null
        for (index in 0 until phoneFormate.length) {
            val x = phoneFormate[index].toString()
    
            if (currentLength != null && range.length == currentLength) {
                return NSRange(myLocation, oriLength ?: 0)
            }
            if (range.location == index) {
                richLocation = true
                currentLength = 0
                oriLength = 0
                if (range.length == 0) {
                    return NSRange(myLocation, 0)
                }
            }
            if (x == "X") {
                if (!richLocation) {
                    myLocation += 1
                }
                if (oriLength != null) {
                    oriLength = oriLength!! + 1
                }
            }
    
            if (currentLength != null) {
                currentLength = currentLength!! + 1
            }
        }
        return NSRange(myLocation, oriLength ?: 0)
    }
    
    private fun generateStr(oriIndex: Int): GenerateStringStatus {
        var showStr = ""
        var index = 0
        var selectedIndex = 0
        for (f in 0 until phoneFormate.length) {
            val x = phoneFormate[f].toString()
    
            if (oriIndex == index) {
                selectedIndex = f
            }
            if (x == "X") {
                if (origineString.length > index) {
                    if (origineString.length > 1) {
                        showStr += origineString[index].toString()
                    } else {
                        showStr += origineString
                    }
                    index += 1
                } else {
                    break
                }
            } else {
                if (origineString.length > index) {
                    showStr += x
                } else {
                    break
                }
            }
        }
        if (oriIndex == index && selectedIndex == 0) {
            selectedIndex = phoneFormate.length
        }
        return GenerateStringStatus(showStr, selectedIndex)
    }
    
    private fun operationOriString(oriStr: String, oriRange: NSRange, string: String): Int {
        var selectedIndex = 0
        if (string.isEmpty()) {
            origineString = oriStr.replaceRange(oriRange.location, oriRange.location + oriRange.length, string)
            selectedIndex = oriRange.location
        } else {
    
            val firstStr = oriStr.substring(0, oriRange.location + oriRange.length).toString()
            val lastStr = oriStr.substring( oriRange.location + oriRange.length, oriStr.length).toString()
    
            selectedIndex = firstStr.length + 1
            origineString = firstStr + string + lastStr
        }
        if (origineString.length > formateInfo.numberCount) {
            origineString = origineString.substring(0, formateInfo.numberCount)
        }
        return selectedIndex
    }
    
    private fun modifyValueIn(range: NSRange, string: String)  {
        // 1. 提取原始字符串
        val oriStr = getOriString()
    
        // 2. 根据 range 生成原始字符串 range
        val oriRange: NSRange = getOriRange(range)
    
        // 3. 操作原始字符串, 并获取在显示字符串中字符光标的位置
        val selectedIndex = operationOriString(oriStr, oriRange, string)
    
        // 4. 生成显示字符串
        val showStr = generateStr(selectedIndex)
        editView.removeTextChangedListener(this)
    
        // 5. 计算最终的光标移位
        editView.setText(showStr.showStr)
        if (string.isEmpty()) {
            editView.setSelection(showStr.showStr.length)
        } else {
            editView.setSelection(showStr.selectedIndex)
        }
        textUpdateListener?.textDidChanged(showStr.showStr)
        editView.addTextChangedListener(this)
    }
    
  3. 由于 Android 版本在复制粘贴可能存在问题,因此需要禁止掉它的复制粘贴功能

    fun canPaste(): Boolean {
        return false
    }
    
    fun canCut(): Boolean {
        return false
    }
    
    fun canCopy(): Boolean {
        return false
    }
    
    fun canSelectAllText(): Boolean {
        return false
    }
    
    fun canSelectText(): Boolean {
        return false
    }
    
    fun textCanBeSelected(): Boolean {
        return false
    }
    
    override fun onTextContextMenuItem(id: Int): Boolean {
        return true
    }
    
    init {
        isLongClickable = false
        setTextIsSelectable(false)
    
        inputType = InputType.TYPE_CLASS_NUMBER
        filters = listOf(InputFilter { _, _, _, _, _, _ -> null }).toTypedArray()
    
        customSelectionActionModeCallback = object: ActionMode.Callback {
            override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
                return false
            }
    
            override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
                return false
            }
    
            override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
                return false
            }
    
            override fun onDestroyActionMode(mode: ActionMode?) {
            }
        }
    }
    

至此,Android 功能也就完成了。

Android 注意点:在代码中设置 text 值的时候,TextWatcher 同样调用了。因此我在设置 text 的时候拿掉了 TextWatcher 代理,设置完成后加上了 TextWatcher 代理。目前来看还算 ok。

总结

这么写的好处在于可以自定义格式,并且代码通用。不需要一个格式写一套格式代码。

Android 是由于公司同事没有时间实现,因此我就看了一下 Android 的 TextEdit 的原理实现了一下。可能存在问题,敬请指教。

相关文章

网友评论

      本文标题:iOS、Android 实现电话号码格式化操作

      本文链接:https://www.haomeiwen.com/subject/fetmuftx.html