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

5,095 阅读6分钟
原文链接: zhooker.github.io

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

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

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

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

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

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

/**
 * 计算字符串的长度,中文加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的方法。

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

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

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监听即可实现功能 :

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中的各个参数含义相近。实现这几个函数:

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监听即可实现功能 :

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

3、使用InputConnection 限制字数

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

//当输入法输入了字符,包括表情,字母、文字、数字和符号等内容,会回调该方法
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类 :

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 :

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。

参考