控件简介
最近研究如何根据配置好的字符串去格式化电话号码等信息。由于项目要求,我们格式化的字符串是客户在网页端自定义的。因此要求我们这边需要针对不同的配置进行动态的格式化字符串。例如:需要格式化为 +86 XXX-XXXX-XXXX, X 替换为 number 去格式化字符串。网络上搜索了一下,基本上没有成熟的代码可用。索性,自己实现一套。
思路:
在 TextField 的代理方法中,func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
。我们可以在这个方法中拦截掉输入请求,自己管理字符串的键入与显示。这样就可以实现任意功能。在方法中,我的思路是:
- 根据当前未做变化前的显示字符串中计算出未格式化的数字字符串
- 根据当前显示字符串中的 Range 计算在数字字符串中的 Range
- 操作数字字符串中的 Range,并获取在数字字符串上的光标停留位置
- 生成显示的字符串,根据数字字符串的光标位置计算显示字符串的光标位置。
iOS 代码部分
代码部分:
-
根据原来的字符串生成未格式化的数字字符串
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 }
-
根据字符串操作的 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) }
-
操作未格式化的字符串,并返回最终光标在未格式化字符串上的位置
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 }
-
生成格式化后的字符串,并根据原始字符串的光标位置生成格式化后字符串的光标
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) }
-
总的调用函数。
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 类似的功能。
-
针对 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() } } } }
-
同样的思路,实现格式化字符串
/// 生成原始字符串 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) }
-
由于 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 的原理实现了一下。可能存在问题,敬请指教。