这一天,产品提出了一个神奇的需求:用户姓名设置框限制,允许最多8个汉字或16个英文字母或数字。
简单来说就是长度限制最大为16个字符,一个汉字认为是两个字符。虽然其实跟用户解释一个汉字等于两个字符这件事情是很奇怪的,但是毕竟是需求,我们来实现一下吧。
首先简单说一下要求:
只允许输入数字汉字或英文字母
输入限制为最大16字符
汉字计为2字符,数字和英文计为1字符
当输入超过16字符时(如拼音输入法一次性输入多个文字),输入内容截取到最大部分
首先由于汉字需要记为2字符,那么最简单的android:maxLength="16"
这种方法就不可以使用了。并且由于需要限制输入的内容,那么我们使用InputFilter来控制实现。
创建一个自定义Filter类,继承自InputFilter,并复写filter方法。简单介绍下filter的几个参数:
source : 新输入的字符串
start : 新输入的字符串起始下标
end : 新输入的字符串结尾下标
dest : 之前文本框内容
dstart : 原内容起始下标
dend : 原内容结尾下标
而filter的返回值,则是经过自定义规则处理之后,需要输入进去的内容。
判断输入内容是否合法,我使用了正则表达式进行判断,并且对输入的内容进行限制和截取。
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public CharSequence filter (CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { String sourceText = source.toString(); String destText = dest.toString(); Matcher matcher = mPattern.matcher(source); if (!matcher.matches()) { return "" ; } if (TextUtils.isEmpty(sourceText)) { return "" ; } int destLength = getLength(destText); int sourceLength = getLength(sourceText); if (destLength >= MAX_LENGTH) { return "" ; } else if (destLength + sourceLength > MAX_LENGTH) { return splitStr(sourceText, MAX_LENGTH - destLength); } return sourceText; }
经过测试:当输入"你好你好你好1234"之后,就不允许输入新的内容了。大功告成,提交代码下班。
可是当第二天测试的报告,却发现了另一个奇怪的现象。当用户使用带联想词功能的输入法时(英文输入法,或者miui的输入法等),由于用户预输入的拼音和单词,是会被输入法拦截并设置到EditText中,使得filter被反复触发,会导致用户无法输入完整拼音或单词,甚至输入内容重复累加等问题。
打印日志后发现,英文联想词输入法情况下,在sourceText中,竟然包括了dest文本中的内容,那么这个时候如果我们再去判断sourceText的文本,就会触发BUG。
那么有没有什么办法,可以判断用户是否处于联想词输入模式下呢。答案是:有的。
在网络上查询了许久之后,发现了一个很有趣的实现方法,预输入模式下,输入的内容是带有一个下划线的,那么判断输入的这部分内容是否带下划线,就可以判断出输入的内容是在什么模式下的了。
1 2 3 4 5 6 7 8 9 SpannableString ss = new SpannableString(source); Object[] spans = ss.getSpans(0 , ss.length(), Object.class); if (spans != null ) { for (Object span : spans) { if (span instanceof UnderlineSpan) { } } }
添加之后,确实预输入时不会出现输入错乱等问题了,但是最大输入的长度却无法控制了,因为预输入结束后,输入法是直接把内容设置到EditText里了,没有经过Filter。
这时就可以使用一个经常用到的方法addTextChangedListener
对输入的内容进行控制,因为任何字符的改变都会经过这里,只要在这里,对超出长度的内容进行截取就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 editText.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged (CharSequence s, int start, int before, int count) { String str = s.toString(); if (getLength(str) > MAX_LENGTH) { int splitIndex = getSplitIndex(str, MAX_LENGTH); editText.setText(str.substring(0 , splitIndex)); editText.setSelection(editText.length()); } } });
经过测试,这回可以满足要求了,即使在预输入模式下,也可以正确的限制输入长度,内容和规则了。优化一下代码,将功能整合到bindEditText,方便以后需求的使用。
最后,完整代码如下,使用bindEditText方法,就可以将此规则绑定到目标EditText上了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 public class NameInputFilter implements InputFilter { private static final int MAX_LENGTH = 16 ; private static final String regCN = "[\u4E00-\u9FA5]" ; private static final String regEnNum = "[a-zA-Z0-9]" ; private static final String regAll = "^[\u4E00-\u9FA5a-zA-Z0-9]+$" ; private static final String regExceptText = "((?![\u4E00-\u9FA5aa-zA-Z0-9]).)" ; private Pattern mCNPattern; private Pattern mEnNumPattern; private Pattern mPattern; public NameInputFilter () { mPattern = Pattern.compile(regAll); mCNPattern = Pattern.compile(regCN); mEnNumPattern = Pattern.compile(regEnNum); } @Override public CharSequence filter (CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { String sourceText = source.toString(); String destText = dest.toString(); SpannableString ss = new SpannableString(source); Object[] spans = ss.getSpans(0 , ss.length(), Object.class); if (spans != null ) { for (Object span : spans) { if (span instanceof UnderlineSpan) { return sourceText.replaceAll(regExceptText, "" ); } } } Matcher matcher = mPattern.matcher(source); if (!matcher.matches()) { return "" ; } if (TextUtils.isEmpty(sourceText)) { return "" ; } int destLength = getLength(destText); int sourceLength = getLength(sourceText); if (destLength >= MAX_LENGTH) { return "" ; } else if (destLength + sourceLength > MAX_LENGTH) { return splitStr(sourceText, MAX_LENGTH - destLength); } return sourceText; } public int getLength (String str) { int length = 0 ; char [] chars = str.toCharArray(); for (char aChar : chars) { Matcher cnMatcher = mCNPattern.matcher(String.valueOf(aChar)); if (cnMatcher.matches()) { length = length + 2 ; continue ; } Matcher matcher = mEnNumPattern.matcher(String.valueOf(aChar)); if (matcher.matches()) { length = length + 1 ; } } return length; } public int getSplitIndex (String str, int maxLength) { int length = 0 ; int splitIndex = 0 ; char [] chars = str.toCharArray(); for (char aChar : chars) { if (length >= maxLength) { return splitIndex; } Matcher cnMatcher = mCNPattern.matcher(String.valueOf(aChar)); if (cnMatcher.matches()) { length = length + 2 ; } else { length = length + 1 ; } splitIndex++; } return str.length(); } private String splitStr (String str, int size) { int length = 0 ; char [] chars = str.toCharArray(); StringBuilder result = new StringBuilder(); for (char aChar : chars) { Matcher cnMatcher = mCNPattern.matcher(String.valueOf(aChar)); if (cnMatcher.matches()) { length = length + 2 ; if (length <= size) { result.append(aChar); } else { break ; } continue ; } Matcher matcher = mEnNumPattern.matcher(String.valueOf(aChar)); if (matcher.matches()) { length = length + 1 ; if (length <= size) { result.append(aChar); } else { break ; } } } return result.toString(); } public void bindEditText (EditText editText) { editText.setFilters(new InputFilter[]{this }); editText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged (CharSequence s, int start, int count, int after) { } @Override public void onTextChanged (CharSequence s, int start, int before, int count) { String str = s.toString(); if (getLength(str) > MAX_LENGTH) { int splitIndex = getSplitIndex(str, MAX_LENGTH); editText.setText(str.substring(0 , splitIndex)); editText.setSelection(editText.length()); } } @Override public void afterTextChanged (Editable s) { } }); } }
我本以为事情就这样结束了,第二天测试跟我反馈,在华为的手机上,问题依旧存在。
经过对source
和dest
的比对,我发现,当检测出下划线部分之后,返回处理后的String其实在这里是有问题的。
因为:华为的输入法会判断返回CharSequence是否有下划线,决定是否替换!!
也就是说,我必须传入原来的,或者说重建一个带下划线的SpannableString才会被处理到。
那么我们在发现符号后,重建一个带下划线的SpannableString。
并且在此时发现,文本长度的过滤和截取,实际上完全由TextChangedListener处理比较好,因为有些输入法在source里面夹带了一些东西,原样传回去才不容易出BUG。
修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Override public CharSequence filter (CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { String sourceText = source.toString(); String destText = dest.toString(); SpannableString ss = new SpannableString(source); Object[] spans = ss.getSpans(0 , ss.length(), Object.class); if (spans != null ) { for (Object span : spans) { if (span instanceof UnderlineSpan) { String s = sourceText.replaceAll(regExceptText, "" ); if (s.equals(sourceText)){ return source; }else { SpannableString str = new SpannableString(s); str.setSpan(new UnderlineSpan(),0 ,s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return str; } } } } Matcher matcher = mPattern.matcher(source); if (!matcher.matches() && !TextUtils.isEmpty(sourceText)) { return "" ; } return source; }
当然,如果没有这种奇怪的需求就完事皆无了,不是么。