EditText 限制输入字符个数的三种方式

最近有个需求是限制用户输入的字符个数,其中中文算2个,非中文字符算1个,比如“1个人”就算5个,当用户输入超过字数限制的时候可以截取并用toast提示用户,这是个非常简单的需求,实现也有很多方法。

首先我们实现检测中文的方法,网上有很多方式,主要是检测字符的unicode值范围:

1
2
3
fun isChinese(c: Char): Boolean {
return c.toInt() in 0x4E00..0x9FA5
}

如果是中文就算2个,否则算1个,函数实现如下:

1
fun getCharTextCount(c: Char) = if (Utils.isChinese(c)) 2 else 1

根据上面的规则,检测一个字符串的字数的函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 计算字符串的长度,中文加2,非中文加1
*/
@JvmStatic
fun calcTextLength(charSequence: CharSequence?): Int {
if (charSequence.isNullOrEmpty()) {
return 0;
}
var sum = 0
for (c in charSequence) {
sum += Utils.getCharTextCount(c)
}
return sum
}

这部分代码在Utils.java中,作为项目的函数工具类。下面列举各种实现方式并做对比。

1、使用InputFilter 限制字数

实现InputFilter过滤器, 需要覆盖一个叫filter的方法。

1
2
3
4
5
6
7
8
public abstract CharSequence filter (
    CharSequence source,  //输入的文字
    int start,  //输入的文字 开始位置
    int end,  //输入的文字 结束位置
    Spanned dest, //当前显示的内容
    int dstart,  //当前显示的内容 开始位置
    int dend //当前显示的内容 结束位置
);

一开始看这个filter函数,参数比较多,意思也比较相近,可能容易搞混,但是当你注意每个参数的含义后,会很好理解,其实就是”将dest中范围为dstart到dend的用source的start到end范围的替换”。接下来实现这个函数:

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
class TextLengthFilter(private val maxLength: Int = Utils.MAX_LENGTH, val listener: TextLengthListener? = null) :
InputFilter {
override fun filter(
source: CharSequence?,
start: Int,
end: Int,
dest: Spanned?,
dstart: Int,
dend: Int
): CharSequence {
if (source.isNullOrEmpty()) {
return ""
}
// bug fixed.
// val source: CharSequence = source.subSequence(start, end)
var sum = Utils.calcTextLength(dest as CharSequence, dstart, dend) + Utils.calcTextLength(source) - maxLength
if (sum > 0) {
val delete = Utils.getDeleteIndex(source, 0, source.length, sum)
listener?.onTextLengthOutOfLimit()
// 输入字符超过了限制,截取
return if (delete > 0) source.subSequence(0, delete) else ""
}
// 没有超过限制,直接返回source
return source
}
}

我们用Utils.calcTextLength(source: CharSequence, dstart: Int, dend: Int)来计算字符串除了[dstart,dend]外的字符数,因为通过上面的分析可知 [dstart,dend]范围内的字符是会被替换的,所以不需要计算总字数内。在代码中添加InputFilter监听即可实现功能 :

1
edit_inputfilter.filters = arrayOf(TextLengthFilter(listener = MainActivity@ this))

2、使用TextWatcher 限制字数

使用TextWather监听EditText的字符变化,我们需要实现三个抽象方法:

  • beforeTextChanged(CharSequence s, int start, int count, int after)
    s: 修改之前的文字。
    start: 字符串中即将发生修改的位置。
    count: 字符串中即将被修改的文字的长度。如果是新增的话则为0。
    after: 被修改的文字修改之后的长度。如果是删除的话则为0。
  • onTextChanged(CharSequence s, int start, int before, int count)
    s: 改变后的字符串
    start: 有变动的字符串的序号
    before: 被改变的字符串长度,如果是新增则为0
    count: 添加的字符串长度,如果是删除则为0。
  • afterTextChanged(Editable s)
    s: 修改后的文字

上面的注释已经写得很明白了,比如在beforeTextChanged回调中,我们可以知道插入字符的位置start,还有插入的个数after,被替换的个数count,这与InputFilter中的各个参数含义相近。实现这几个函数:

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
class TextLengthWatcher(private val maxLength: Int = Utils.MAX_LENGTH, val listener: TextLengthListener? = null) :
TextWatcher {
private var destCount: Int = 0
private var dStart: Int = 0
private var dEnd: Int = 0
override fun afterTextChanged(s: Editable) {
// count是输入后的字符长度
val count = Utils.calcTextLength(s)
if (count > maxLength) {
// 超过了sum个字符,需要截取
var sum = count - maxLength
val delete = Utils.getDeleteIndex(s, dStart, dEnd, sum)
listener?.onTextLengthOutOfLimit()
if (delete < dEnd) {
// 输入字符超过了限制,截取
s.delete(delete, dEnd)
}
}
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
destCount = Utils.calcTextLength(s)
// 获取输入字符的起始位置
dStart = start
// 获取输入字符的个数
dEnd = start + after
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
}
}

需要注意的是,在TextWatcher中修改文本(Editable.delete、EditText.setText等)要小心不要陷入死循环。即:文字改变->watcher接收到通知->setText->文字改变->watcher接受到通知->…。所以我们在修改文本前加了一个结束条件count > maxLength。在代码中添加TextWatcher监听即可实现功能 :

1
edit_textwatcher.addTextChangedListener(TextLengthWatcher(listener = MainActivity@ this))

3、使用InputConnection 限制字数

InputConnection 是输入法和应用内View(通常是EditText)交互的通道,输入法的文本输入和删改事件,包括key event事件都是通过InputConnection发送给EditText。示意图如下:
QQ20181126-170024@2x.png
InputConnection有几个关键方法,通过重写这几个方法,我们基本可以拦截软键盘的所有输入和点击事件:

1
2
3
4
5
6
7
8
9
10
11
12
//当输入法输入了字符,包括表情,字母、文字、数字和符号等内容,会回调该方法
public boolean commitText(CharSequence text, int newCursorPosition)
//当有按键输入时,该方法会被回调。比如点击退格键时,搜狗输入法应该就是通过调用该方法,
//发送keyEvent的,但谷歌输入法却不会调用该方法,而是调用下面的deleteSurroundingText()方法。
public boolean sendKeyEvent(KeyEvent event);
//当有文本删除操作时(剪切,点击退格键),会触发该方法
public boolean deleteSurroundingText(int beforeLength, int afterLength)
//结束组合文本输入的时候,回调该方法
public boolean finishComposingText()

从中可以发现,我们可以利用commitText来拦截用户的输入。设置InputConnection的方法在EditText类里面,所以我们继承EditText自定义一个TextLengthEditText。完全重写InputConnection的成本是很高的,我们可以继承InputConnectionWrapper类 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
inner class TextLengthInputConnecttion(
val target: InputConnection,
private val maxLength: Int = Utils.MAX_LENGTH,
val listener: TextLengthListener? = null
) : InputConnectionWrapper(target, false) {
override fun commitText(source: CharSequence, newCursorPosition: Int): Boolean {
val count = Utils.calcTextLength(source)
val destCount = Utils.calcTextLength(text as CharSequence, selectionStart, selectionEnd)
if (count + destCount > maxLength) {
// 超过了sum个字符,需要截取
var sum = count + destCount - maxLength
val delete = Utils.getDeleteIndex(source, 0, source.length, sum)
listener?.onTextLengthOutOfLimit()
// 输入字符超过了限制,截取
return super.commitText(if (delete > 0) source.subSequence(0, delete) else "", newCursorPosition)
}
return super.commitText(source, newCursorPosition)
}
}

我们把TextLengthInputConnecttion定义成inner class,这样才能够访问外部类的成员。最后还需要通过重写EditText的onCreateInputConnection方法来设置InputConnection :

1
2
3
override fun onCreateInputConnection(outAttrs: EditorInfo?): InputConnection {
return TextLengthInputConnecttion(super.onCreateInputConnection(outAttrs), listener = TextLengthEditText@ this)
}

直接把自定义的TextLengthEditText添加在layout xml文件中即可实现功能。

总结

本文介绍了三种限制字符个数的方法,各个方法各有优缺点,毕竟我们也要考虑到以后的扩展,不能哪个方便用哪个,不然以后需求变更的话就要修改很多代码了。最后各方法的总结对比如下:

\ 优点 缺点
InputFilter 可以检测文本输入、删除 不能检测按键输入
TextWatcher 可以检测文本输入、删除 不能检测按键输入,只能在输入变更后检测,导致回调方法可能被多次执行
InputConnection 可以检测文本输入、删除,可以拦截按键输入,比InputFilter、TextWatcher先执行 实现时必须自定义EditText,比较麻烦

代码已经上传 Github 地址,欢迎star。

参考