让 Android 输入框只能输入固定长度的中英文

很多 App 都会要求输入的字符种类、长度有所限制,在此之前我其实已经遇到了这样的需求,只能输入中文和英文,并且不同的语言所限制的长度不同。那个时候会觉得这样的限制比较麻烦,因为总认为涉及到中文的判断就比较麻烦,所以推脱说实现比较麻烦没有太多的时间,就给暂时压了下来。直到第二次遇到这样的需求我直到没办法再退了,结果 google 了下发现判断中文也没那么麻烦,使用正则判断中文字符范围即可。

另外类似需求的实现逻辑也可能不同,比如有的是中文、英文分别限制,中文可以输入 7 字符,英文可以输入 14 字符,但是只要有 1 个中文字就按照中文的长度限制,这样你输入了 1 个中文字之后就只能输入 6 个英文字符了,感觉并不合理。因此我的实现逻辑是:1 个中文字符算两个英文字符,然后计算总长度(在 Android 里不论中英文都默认计算为 1 个字符)。这样输入 1 个中文字后还能继续输入 12 个英文字。

一般简单的限制输入长度只需要设置 maxLength 即可,但是这种复合的需求,就只能通过 InputFilter 来实现了。InputFilter 是一个接口,并且 Android 官方提供很多种默认实现,有的很有意思,这里不一一列举,感兴趣的可以自己去看看。

InputFilter 只有一个方法:

1
2
3
4
5
6
7
8
override fun filter(source: CharSequence,   //新输入的文本,如果删除则为空
start: Int, //固定是 0,删除也是 0
end: Int, //新输入文本的长度,不区分中英文,删除为 0
dest: Spanned, //已有的文本
dstart: Int, //已有文本的长度,删除则=原有长度-被删除文本的长度
dend: Int) //已有文本的长度==dstart,删除也是一样
: CharSequence? {
}

source 代表新输入待验证过滤的字符串,startend 是新输入字符串的起始;

dest 是被验证通过已经输入的字符,dstartdend 是已经输入文本的起始;

如果是输入状态,那么 dstart == dend,但是 start < end

如果是删除状态,那么 start == end,但是 dstart < dend ,并且两者之差就是删除字符的长度。

这个方法所返回值的不同所表示的意义也不同:

  • 返回 null 表示不做过滤和限制;
  • 返回 “” 表示全部过滤,不能输入任何字符;
  • 返回非空字符串表示过滤通过字符串的内容,当然字符串的内容我们完全可以自定义;

有了以上认识可以更好的完成目标:

  1. 判断新输入的字符是中文还是英文(目前来看还没有能够同时输入包含中英文的情况);
  2. 对中英文按不同长度分别计算总长度,去掉超出的部分;
  3. 返回对应的值

实现如下:

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
class SizeFilterWithTextAndLetter(private var letterMaxLength: Int) : InputFilter {
private val chinesePattern = Pattern.compile("[\u4e00-\u9fa5]+")
private val letterPattern = Pattern.compile("[a-zA-Z]+")

override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
val isAdd = dstart == dend
if (!isAdd) {//删除文字不限制
return null
}
val originLength = calculateLength(dest)

val isChinese = chinesePattern.matcher(source.toString()).find()
val isLetter = letterPattern.matcher(source.toString()).find()
if (isChinese) {
return when {
originLength >= letterMaxLength -> ""
originLength + source.length * 2 > letterMaxLength -> {
var length = (letterMaxLength - originLength + 1) / 2
if (length < 0) length = 0
source.subSequence(0, length)
}
else -> null
}
} else if (isLetter) {
return when {
originLength >= letterMaxLength -> ""
originLength + source.length > letterMaxLength -> {
letterMaxLength = source.length
val length = letterMaxLength
source.subSequence(0, length)
}
else -> null
}
}

return ""
}

/**
* 计算原有文本占据的长度
*
* @param dest 原文本
* @return 总长度
*/
private fun calculateLength(dest: Spanned): Int {
var chineseLength = 0
var letterLength = 0
for (i in 0 until dest.length) {
if (chinesePattern.matcher(dest.subSequence(i, i + 1)).find()) {
chineseLength += 2
}
if (letterPattern.matcher(dest.subSequence(i, i + 1)).find()) {
letterLength += 1
}
}
return chineseLength + letterLength
}
}

使用方法:

1
2
//过滤器是以数组的形式设置,此处限制输入 14 英文字符,也就是 7 中文字符
edit_value?.filters = arrayOf(SizeFilterWithTextAndLetter(14))


以上实现方法其实还是有缺陷的。实测在不同设备不同 Android 版本下, filter 方法接受到的参数值是有区别的,比如 Samsang Galaxy S4, Android 5.0:

输入的时候 dstart!=dent ,那么按照原来的代码,会误判为删除模式,因此无法达到限制输入字符的目的。

既然 dstart == dend 的判断不可靠,那我就不会再这样做了,我们只判断新输入字符与原有字符是否符合要求即可。另外在实际测试的时候我发现有的手机或者输入法的 source 会返回完整的字符串,即不仅包含原有字符,也包含了新输入的字符,这样就不能简单的把 source 追加到 dest 里面了。

经过各种测试,便有了下面新的实现:

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
class SizeFilterWithTextAndLetter(private val letterMaxLength: Int) : InputFilter {
private val chinesePattern = Pattern.compile("[\u4e00-\u9fa5]+")
private val letterPattern = Pattern.compile("[a-zA-Z]+")

override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int,
dend: Int): CharSequence? {
if (source.length > dest.length) {
return filterSourceLongThanDest(source, start, end, dest, dstart, dend)
}

val originLength = calculateLength(dest)

val isChinese = chinesePattern.matcher(source.toString()).find()
val isLetter = letterPattern.matcher(source.toString()).find()
if (isChinese) {
return when {
originLength >= letterMaxLength -> ""
originLength + source.length * 2 > letterMaxLength -> {
var length = (letterMaxLength - originLength + 1) / 2
//汉字长度为 1 仍然会切出一个汉子,还是会超出长度
if (length < 0 || length == 1) length = 0
source.subSequence(0, length)
}
else -> null
}
} else if (isLetter) {
return when {
originLength >= letterMaxLength -> ""
originLength + source.length > letterMaxLength -> {
val length = letterMaxLength - originLength
source.subSequence(0, length)
}
else -> null
}
}

return ""
}

/**
* 返回的 source 包含全量文本情况的处理
*/
private fun filterSourceLongThanDest(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int,
dend: Int): CharSequence? {
val originLength = calculateLength(dest)
val newText = source.subSequence(dest.length, source.length)
val isChinese = chinesePattern.matcher(newText.toString()).find()
val isLetter = letterPattern.matcher(newText.toString()).find()
if (isChinese) {
return when {
originLength >= letterMaxLength -> ""
originLength + newText.length * 2 > letterMaxLength -> {
var length = (letterMaxLength - originLength + 1) / 2
//汉字长度为 1 仍然会切出一个汉子,还是会超出长度
if (length < 0 || length == 1) length = 0
newText.subSequence(0, length)
}
else -> null
}
} else if (isLetter) {
return when {
originLength >= letterMaxLength -> dest
originLength + newText.length > letterMaxLength -> {
val length = letterMaxLength - originLength
newText.subSequence(0, length)
}
else -> null
}
}

return ""
}

/**
* 计算原有文本占据的长度
*
* @param dest 原文本
* @return 总长度
*/
private fun calculateLength(dest: Spanned): Int {
var chineseLength = 0
var letterLength = 0
for (i in 0 until dest.length) {
if (chinesePattern.matcher(dest.subSequence(i, i + 1)).find()) {
chineseLength += 2
}
if (letterPattern.matcher(dest.subSequence(i, i + 1)).find()) {
letterLength += 1
}
}
return chineseLength + letterLength
}
}

上面提到有的输入法会在输入的时候 source 会返回完整的字符,所以代码实现里加上了 source.length > dest.length 的判断,但实际上正常情况也会出现该种情况,比如连续输入一长串中文,但是不按下空格键使中文打印到文本框里,这样的情况虽然确实会走到 filterSourceLongThanDest 方法中,但并不会影响正常的输入,也不会出现字符顺序错乱的情况,因为在按下空格键或者选定中文字的时候,会把文本框中显示出来的拼音替换掉,这样又会进入正常的判断处理流程。

这样,我们在完全没有用到 start,end,dstart,dend 这些参数的情况下,就完成了字符的过滤。至此,字符过滤的处理就结束了。该段代码已经进入生产环境,并且运行良好.