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 dstart … dend of dest
     * with the new text from the range start … end
     * of source.  Return the CharSequence that you would
     * like to have placed there instead, including an empty string
     * if appropriate, or null 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 dest
     * from this method; you may only examine it for context.
     *
     * Note: If source is an instance of {@link Spanned} or
     * {@link Spannable}, the span objects in the source 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应该怎么做?

你可能感兴趣的:(Android 最大输入x个汉字,2x的英文)