Android 最大输入x个汉字,2x的英文

前言

终所周知, editText想要监听文本输入,有两种方式:InputFilter与TextWatcher。为什么不建议TextWatcher来修改文本, 我们一起来探讨一下。

1: InputFilter

老规矩,要看作用,先看代码注释. 一言以蔽之: InputFilter可以约束Editable的变化。

**
 * InputFilters can be attached to {@link Editable}s to constrain the
 * changes that can be made to them.
 */
public interface InputFilter
{
    /**
     * This method is called when the buffer is going to replace the
     * range <code>dstart &hellip; dend</code> of <code>dest</code>
     * with the new text from the range <code>start &hellip; end</code>
     * of <code>source</code>.  Return the CharSequence that you would
     * like to have placed there instead, including an empty string
     * if appropriate, or <code>null</code> to accept the original
     * replacement.  Be careful to not to reject 0-length replacements,
     * as this is what happens when you delete text.  Also beware that
     * you should not attempt to make any changes to <code>dest</code>
     * from this method; you may only examine it for context.
     *
     * Note: If <var>source</var> is an instance of {@link Spanned} or
     * {@link Spannable}, the span objects in the <var>source</var> should be
     * copied into the filtered result (i.e. the non-null return value).
     * {@link TextUtils#copySpansFrom} can be used for convenience if the
     * span boundary indices would be remaining identical relative to the source.
     */
    public CharSequence filter(CharSequence source, int start, int end,
                               Spanned dest, int dstart, int dend);

由此可以得知InputFilter是专门用来限制输入的(毕竟从名字就可以看出)。那么它究竟是怎么用的呢? 看看TextView.setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen)的源码即可:

private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
       /* 省略部分代码 */

        int n = mFilters.length;
        for (int i = 0; i < n; i++) {
            CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
          //关键代码, 根据filter返回值,决定是否要替换text。
            if (out != null) {
                text = out;
            }
        }

      // 看见了我们的老朋友TextWatcher.BeforeTextChanged
        if (notifyBefore) {
            if (mText != null) {
                oldlen = mText.length();
                sendBeforeTextChanged(mText, 0, oldlen, text.length());
            } else {
                sendBeforeTextChanged("", 0, 0, text.length());
            }
        }

        boolean needEditableForNotification = false;

        if (mListeners != null && mListeners.size() != 0) {
            needEditableForNotification = true;
        }

        PrecomputedText precomputed =
                (text instanceof PrecomputedText) ? (PrecomputedText) text : null;
        if (type == BufferType.EDITABLE || getKeyListener() != null
                || needEditableForNotification) {
            createEditorIfNeeded();
            mEditor.forgetUndoRedo();
            Editable t = mEditableFactory.newEditable(text);
            text = t;
            setFilters(t, mFilters);
            InputMethodManager imm = InputMethodManager.peekInstance();
            if (imm != null) imm.restartInput(this);
        } 


            if (Linkify.addLinks(s2, mAutoLinkMask)) {
                text = s2;
                type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;

                /*
                 * We must go ahead and set the text before changing the
                 * movement method, because setMovementMethod() may call
                 * setText() again to try to upgrade the buffer type.
                 */
                setTextInternal(text);

                // Do not change the movement method for text that support text selection as it
                // would prevent an arbitrary cursor displacement.
                if (mLinksClickable && !textCanBeSelected()) {
                    setMovementMethod(LinkMovementMethod.getInstance());
                }
            }
        }

        mBufferType = type;
        setTextInternal(text);

       // 看见了我们的老朋友TextWatcher.onTextChanged
        sendOnTextChanged(text, 0, oldlen, textLength);
        onTextChanged(text, 0, oldlen, textLength);

        notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);

       // 看见了我们的老朋友TextWatcher.AfterTextChanged
        if (needEditableForNotification) {
            sendAfterTextChanged((Editable) text);
        } else {
            notifyAutoFillManagerAfterTextChangedIfNeeded();
        }

        // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
        if (mEditor != null) mEditor.prepareCursorControllers();
    }

从setText源码中可以看出其InputerFilter参数与返回值的作用:

  • source 代表此次输入的文本
  • dest 代表控件已有的文本
  • [start,end],[dstart,dend]分别表示source跟dest的文本位置(tips: 当del键被按下时,dend可能小于dstart)
  • 返回值 CharSequence。这里的返回值有三种情况:
    • 仅当满足条件时,返回null. (即不修改此次text,直接将其添加到textView中)
    • 当不满足条件时, 返回空字符串.(用户输入的text被替换为"",添加到textView中)
    • 当部分满足条件时, 取满足条件的部分字符串。

既然InputFilter可以约束变化,那么当产品同学提出这个输入框的最大限制是"128个汉字,或者256的英文字符"的要求时,我们应该怎么做呢?

1.1 自定义InputFilter

产品同学的需求是一个汉字占两个字符,其中标点符号(中英文),其他语言字符(日文,韩文),数字(1,2,3)等等需要占多少个字符呢? 作为一个合格的程序员,这里就需要跟产品同学对齐信息之后再进行编写。
既然有长度限制,我们肯定需要外部传入最大长度限制(Max),那么我们就需要考虑三种情况:

  • Max - 控件已有字符长度 > 此次输入长度。(OK, 不会突破限制,直接append)
  • Max - 控件已有字符长度 == 0。(Can't append)
  • 0 < Max - 控件已有字符串 < 此次输入长度. (部分满足, 取部分source append)

1.1.2 MaxCharInputFilter

让我们假设仅英文字符([A-Za-z])占一个字符,其他都占两个字符,那么该如何实现呢?

/**
 *  @Author: koller
 *  @Date: 2020/7/1
 *  仅[A-Za-z]算一个字符
 *  其他全部算两个字符(包括英文标点符号,数字等)。
 *  计算最大字符数
 */
class MaxCharLengthInputFilter(private val maxCharLength: Int) : BaseInputFilter() {
    override val filterType: FilterType
        get() = FilterType.MaxCharFilter

    override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
        var currentCharLength = 0
        var dMinusCharLength = 0
        dest.forEachIndexed { index, c ->
            if (index in dstart until dend) {
                dMinusCharLength += c.letterSize
            }
            currentCharLength += c.letterSize
        }
        var sourceCharLength = 0
        var keep = maxCharLength - currentCharLength - dMinusCharLength
        if (keep <= 0) {
            // 超出字数限制,返回EMPTY_STRING
            return EMPTY_STRING
        }
        source.forEach { c ->
            sourceCharLength += c.letterSize
        }
        if (keep >= sourceCharLength) {
            // 如果完全满足限制,就返回null
            return null
        }
        val buffer = StringBuffer()
        source.forEach { c ->
            keep -= c.letterSize
            if (keep <= 0) {
                if (!c.isLetter() && keep < 0) {
                    // 不允许超过keep
                    return buffer
                }
                buffer.append(c)
                return buffer
            }
            buffer.append(c)
        }
        return buffer
    }

    private val Char.letterSize: Int
        get() {
            return if (isLetter()) {
                SINGLE_LETTER_SIZE
            } else {
                DOUBLE_LETTER_SIZE
            }
        }

    private fun Char.isLetter() = this in 'a'..'z' || this in 'A'..'Z'

    companion object {
        const val SINGLE_LETTER_SIZE = 1
        const val DOUBLE_LETTER_SIZE = 2
    }
}

1.1.3 MaxBytesInputFilter (UTF-8编码)

已经有最大字符限制了,那我们实现一个最大字节限制,也是so easy

class MaxBytesInputFilter(private val maxBytes: Int) : BaseInputFilter() {
    override val filterType: FilterType
        get() = FilterType.MaxBytesFilter

    override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
        var currentBytesLength = 0
        dest.forEachIndexed { index, c ->
            if (index in dstart until dend) {
                currentBytesLength -= c.byteSize
            } else {
                currentBytesLength += c.byteSize
            }
        }
        var sourceByteLength = 0
        var keep = maxBytes - currentBytesLength
        if (keep <= 0) {
            // 超出字数限制,返回EMPTY_STRING
            return EMPTY_STRING
        }
        source.forEach { c ->
            sourceByteLength += c.byteSize
        }
        // 如果完全满足限制,不改source
        if (keep >= sourceByteLength) {
            return null
        }

        val buffer = StringBuffer()
        source.forEach { c ->
            keep -= c.byteSize
            if (keep <= 0) {
                // 剩下2个字节的空间,最后一位是3字节的汉字
                if (c.byteSize > 1 && keep < 0) {
                    // 不允许超过keep
                    return buffer
                }
                return buffer.append(c)
            }
            buffer.append(c)
        }
        return buffer
    }

    private val Char.byteSize
        get() = toString().toByteArray().size
}

1.1.4 MaxLengthInputFilter.

最大字符跟字节都有了,那么实现一个最大长度的限制,应该是最简单的了。最大长度顾名思义,直接按照CharSequence.length()来粗暴的进行筛选.

class MaxLengthInputFilter(private val max: Int) : BaseInputFilter() {
    override val filterType: FilterType
        get() = FilterType.MaxLengthFilter

    override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
        var keep = max - (dest.length - (dend - dstart))
        return when {
            keep <= 0 -> {
                // 超出字数限制,返回EMPTY_STRING
                EMPTY_STRING
            }
            keep >= end - start -> {
                // 如果完全满足限制,就返回null(如果返回值为null,TextView中就会使用原始source)
                null
            }
            else -> {
                // 部分满足限制, 如还剩2个长度,粘贴5个长度的字符串
                keep += start
                if (Character.isHighSurrogate(source[keep - 1])) {
                    // 如果最后一位字符是HighSurrogate(高编码,占2个字符位),就把keep减1,保证不超出字数限制
                    --keep
                    if (keep == start) {
                        return EMPTY_STRING
                    }
                }
                source.subSequence(start, keep)
            }
        }
    }
}

留个作业:
如果要实现一个emojiInputFilter应该怎么做?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容