美文网首页
InputFilter 和 TextWatcher

InputFilter 和 TextWatcher

作者: 笨鱼天阳 | 来源:发表于2019-07-15 10:56 被阅读0次

    Android 成长在于积累和分享

    版权声明:本文为参考博主编写的原创文章,原文: https://blog.csdn.net/u014606081/article/details/53101629

    [TOC]

    UCS 移动端技术分享

    技术形式的一种分享,发现在写过程中参考别人博客的同时发现确实他写的更完善点 ,所以在他博客的基础上添加了一部分自己理解的东西。也是给大家看一下,如果有不对的地方,希望我们一起修正和学习。

    导读

    InputFilter源码解析、TextWatcher源码解析

    前言

    Android中 InputFilterTextWatcher 的功能和作用非常相似,都可以做到对 EditText 输入内容的监听及控制。那两者具体有什么区别,又是如何实现对输入内容进行监听的。下面我们就从源码的角度一起分析一下。

    分析源码之前先打一下基础,EditText是继承自TextView,90%的功能跟TextView是一致的,只有4个私有方法,剩下8个是重写TextView的方法。所以EditText的大部分功能都是在TextView中完成的,具体逻辑也都是在TextView中。

    InputFilter

    InputFilter里面只有一个方法filter(),返回值为CharSequence,用于过滤或者输入/插入的字符串, 当返回值不为null时,使用返回结果替换原有输入/插入的字符串

    package android.text;
    
    /**
     * InputFilters can be attached to {@link Editable}s to constrain the
     * changes that can be made to them.
     */
    public interface InputFilter {
    
            /**
             * @param source  将要插入的字符串,来自键盘输入、粘贴
             * @param start  source的起始位置,为0(暂时没有发现其它值的情况)输入-0,删除-0
             * @param end  source的长度: 输入-文字的长度,删除-0
             * @param dest  EditText中已经存在的字符串,原先显示的内容
             * @param dstart  插入点的位置:输入-原光标位置,删除-光标删除结束位置
             * @param dend  输入-原光标位置,删除-光标删除开始位置
            */
           public CharSequence filter(CharSequence source, int start, int end,
                                   Spanned dest, int dstart, int dend);
           ...略...
    }
    
    

    TextView类中的setText()方法,会调用filter()方法,得到过滤后的字符串,部分源码如下:

    setText()方法有很多重载方法,但是最终都会调用下面这个。这个方法很重要,只需要看关键逻辑处的注释。

    /**
     * @param text 将要设置的新内容
     * @param type 内容的设置类型(static text, styleable/spannable text, or editable text)
     * @param notifyBefore 是否需要触发TextWacther的before
     * @param oldlen 新增加内容的长度
    */
    private void setText(CharSequence text, BufferType type,
                             boolean notifyBefore, int oldlen) {
            mTextFromResource = false;
            if (text == null) {
                text = "";
            }
    
            ...略...
    
            // 使用InputFilter处理text
            int n = mFilters.length;
            for (int i = 0; i < n; i++) {
                CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
                if (out != null) {
                    text = out;
                }
            }
    
            ...略...
    
            // 如果是EditText,就new一个Editable
            if (type == BufferType.EDITABLE || getKeyListener() != null ||
                    needEditableForNotification) {
                createEditorIfNeeded();
                Editable t = mEditableFactory.newEditable(text);
                text = t;
                setFilters(t, mFilters); // filter的另外一处过滤调用处
                InputMethodManager imm = InputMethodManager.peekInstance();
                if (imm != null) imm.restartInput(this);
            } else if (type == BufferType.SPANNABLE || mMovement != null) {
                text = mSpannableFactory.newSpannable(text);
            } else if (!(text instanceof CharWrapper)) {
                text = TextUtils.stringOrSpannedString(text);
            }
    
            ... 略 ...
        }
    

    mFilters是一个InputFilter数组,因为有一个或多个过滤器。通过for循环,把text按照所有过滤条件全部过滤一遍,最终得到“合格”的text。

    除了setText方法,在键盘键入的过程中同样会过滤,TextViewsetFilters方法给Editable设置了filters,并在Editablereplace方法中进行过滤。

    private void setFilters(Editable e, InputFilter[] filters) {
            if (mEditor != null) {
                final boolean undoFilter = mEditor.mUndoInputFilter != null;
                final boolean keyFilter = mEditor.mKeyListener instanceof InputFilter;
                int num = 0;
                if (undoFilter) num++;
                if (keyFilter) num++;
                if (num > 0) {
                    InputFilter[] nf = new InputFilter[filters.length + num];
    
                    System.arraycopy(filters, 0, nf, 0, filters.length);
                    num = 0;
                    if (undoFilter) {
                        nf[filters.length] = mEditor.mUndoInputFilter;
                        num++;
                    }
                    if (keyFilter) {
                        nf[filters.length + num] = (InputFilter) mEditor.mKeyListener;
                    }
    
                    e.setFilters(nf);
                    return;
                }
            }
            e.setFilters(filters);
    

    举例SpannableStringBuilderreplace方法的实现。

     // Documentation from interface
        public SpannableStringBuilder replace(final int start, final int end,
                CharSequence tb, int tbstart, int tbend) {
            checkRange("replace", start, end);
    
            int filtercount = mFilters.length;
            for (int i = 0; i < filtercount; i++) {
                CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end);
    
                if (repl != null) {
                    tb = repl;
                    tbstart = 0;
                    tbend = repl.length();
                }
            }
    
         ... 略...
    }
    

    知道了InputFilter是如何起作用的,那么剩下的就是搞清楚filter()方法中的各个参数的含义,写出自己需要的InputFilter。

    SDK提供了两个实现:AllCapsLengthFilter,下面以LengthFilter解读InputFilter的用法,源码片段如下:

    public static class LengthFilter implements InputFilter {
    
            private final int mMax;
    
            public LengthFilter(int max) {
                mMax = max;
            }
    
            //参数source:将要插入的字符串,来自键盘输入、粘贴
            //参数start:source的起始位置,为0(暂时没有发现其它值的情况)输入-0,删除-0
            //参数end:source的长度: 输入-文字的长度,删除-0
            //参数dest:EditText中已经存在的字符串,原先显示的内容
            //参数dstart:插入点的位置:输入-原光标位置,删除-光标删除结束位置
            //参数dend:输入-原光标位置,删除-光标删除开始位置
            public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
                                       int dstart, int dend) {
                int keep = mMax - (dest.length() - (dend - dstart));
                if (keep <= 0) {
                    // 如果超出字数限制,就返回“”
                    return "";
                } else if (keep >= end - start) {
                    // 如果完全满足限制,就返回null(如果返回值为null,TextView中就会使用原始source)
                    return null; // keep original
                } else {
                    keep += start;
                    if (Character.isHighSurrogate(source.charAt(keep - 1))) {
                        // 如果最后一位字符是HighSurrogate(高编码,占2个字符位),就把kepp减1,保证不超出字数限制
                        --keep;
                        if (keep == start) {
                            return "";
                        }
                    }
                    return source.subSequence(start, keep);
                }
            }
    
            /**
             * @return the maximum length enforced by this input filter
             */
            public int getMax() {
                return mMax;
            }
        }
    

    TextWatcher

    类如其名,用于观察Text的输入删除等变化。

    package android.text;
    
    /**
     * When an object of a type is attached to an Editable, its methods will
     * be called when the text is changed.
     */
    public interface TextWatcher extends NoCopySpan {
        /**
         * @param s原内容
         * @param start  被替换内容起点坐标
         * @param count 被替换内容的长度
         * @param after 新增加内容的长度
         */
        public void beforeTextChanged(CharSequence s, int start,
                                      int count, int after);
        /**
         * @param s 发生改变后的内容
         * @param start  被替换内容的起点坐标
         * @param before 被替换内容的长度
         * @param count 新增加的内容的长度
         */
        public void onTextChanged(CharSequence s, int start, int before, int count);
    
        /**
         * @param s 发生改变后的内容(对s编辑同样会触发TextWatcher)
         */
        public void afterTextChanged(Editable s);
    }
    
    

    两种情况会使TextView里面的内容发生变化,从而通知监听器,第一种就是setText()方法,第二种就是从键盘输入。两种都会调用sendBeforeTextChanged方法发送通知,如下(举例beforeTextChanged):

    private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
            if (mListeners != null) {
                final ArrayList<TextWatcher> list = mListeners;
                final int count = list.size();
                for (int i = 0; i < count; i++) {
                    list.get(i).beforeTextChanged(text, start, before, after);
                }
            }
    
            // The spans that are inside or intersect the modified region no longer make sense
            removeIntersectingNonAdjacentSpans(start, start + before, SpellCheckSpan.class);
            removeIntersectingNonAdjacentSpans(start, start + before, SuggestionSpan.class);
        }
    

    首先,给大家介绍下Google的设计理念,说实话,我不知道原文出自哪,这个是从某个大佬博客上摘过来的。


    Google对于“改变字符串”的设计理念就是“替换”。如果是删内容,就是用空字符串替换需要删除的字符串;如果是增加内容,就是用新字符串替换空字符串。所以要先搞清楚下面几个概念:

    1. 原内容:发生改变前TextView中的内容;
    2. 被替换内容起点坐标:编辑一段内容时,有可能是直接添加新内容,也有可能是选中一段原有内容,用新内容把它替换掉;
    3. 被替换内容的长度:如果是直接添加新内容,被替换内容的长度就是0;
    4. 新增加的内容:对于setText()来说,就是方法中的参数,对于键盘输入来说,就是键盘输入的内容

    再来分析这两种情况。

    情况一:setText()

    同样的,先看下TextViewsetText()方法对相关事件的处理

    /**
     * @param text 将要设置的新内容
     * @param type 内容的设置类型(static text, styleable/spannable text, or editable text)
     * @param notifyBefore 是否需要触发TextWacther的before
     * @param oldlen 新增加内容的长度
    */
    private void setText(CharSequence text, BufferType type,
                             boolean notifyBefore, int oldlen) {
            mTextFromResource = false;
            if (text == null) {
                text = "";
            }
    
            ...略...
    
            // 先过滤,再确认是否发送TextChange
            int n = mFilters.length;
            for (int i = 0; i < n; i++) {
                CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
                if (out != null) {
                    text = out;
                }
            }
    
            // char[]类型时会提前发送sendBeforeTextChanged,此处的notifyBefore即为false
            if (notifyBefore) {
                // 通知调用TextWatcher的beforeTextChanged()方法
                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;
            }
    
            if (type == BufferType.EDITABLE || getKeyListener() != null
                    || needEditableForNotification) {
                createEditorIfNeeded();
                mEditor.forgetUndoRedo();
                Editable t = mEditableFactory.newEditable(text); // 创建Editable,后面键盘输入会用到相关监听
                text = t;
                setFilters(t, mFilters);
                InputMethodManager imm = InputMethodManager.peekInstance();
                if (imm != null) imm.restartInput(this);
            } else if (type == BufferType.SPANNABLE || mMovement != null) {
                text = mSpannableFactory.newSpannable(text);
            } else if (!(text instanceof CharWrapper)) {
                text = TextUtils.stringOrSpannedString(text);
            }
    
            ...略...
    
            if (text instanceof Spannable && !mAllowTransformationLengthChange) {
                Spannable sp = (Spannable) text;
    
                // Remove any ChangeWatchers that might have come from other TextViews.
                final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
                final int count = watchers.length;
                for (int i = 0; i < count; i++) {
                    sp.removeSpan(watchers[i]);
                }
    
                if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();
    
                // 设置键盘输入TextWatcher监听(可以看一下ChangeWatcher源码)
                sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
                        | (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
    
                if (mEditor != null) mEditor.addSpanWatchers(sp);
    
                if (mTransformation != null) {
                    sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
                }
    
            ...略...
    
            }
    
            ...略...
    
            // 通知调用TextWatcher的onTextChanged()方法
            sendOnTextChanged(text, 0, oldlen, textLength);
            onTextChanged(text, 0, oldlen, textLength);
    
            // 通知view刷新
            notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
    
            // 通知调用TextWatcher的afterTextChanged()方法
            if (needEditableForNotification) {
                sendAfterTextChanged((Editable) text);
            } else {
                // Always notify AutoFillManager - it will return right away if autofill is disabled.
                notifyAutoFillManagerAfterTextChangedIfNeeded();
            }
    
            // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
            if (mEditor != null) mEditor.prepareCursorControllers();
    
    }
    
    

    这里可以看到,如果调用setText方法是一定会触发TextWatcher相关事件的,所以尽量不要TextWatcheronTextChanged()方法中对文字进行过滤,然后再调用setText()方法重置字符串,
    如果一定要在TextWatcher的onTextChanged()方法中调用setText()方法(某些很难受的需求),注意防止死循环。因为setText()方法又会回调onTextChanged()方法,会形成死循环。

    情况二:键盘输入

    TextView的构造方法中,会获取android:text属性的值,调用setText()方法设置初始内容。其中,就会判断BufferType的类型,如果是EditText,就会创建Editable(此段逻辑见上面setText()源码)。

    最终new出SpannableStringBuilder对象,SpannableStringBuilder实现了Editable、Appendable接口。Appendable提供了一个接口(有三个重载的):append(),用来把新内容(来自键盘输入)添加到原内容中。所以我们去SpannableStringBuilder里看看append()方法的具体实现。

    三个重载的接口,就有三个具体实现,但原理都一样,最终都会调用replace()方法。下面以其中一个append()实现来分析:

    // 键盘输入有两种:一种是正常输入;另一种是先选中一段内容,再从键盘输入,新内容会替换掉选中的内容;
    // 这个方法是正常输入时调用
    public SpannableStringBuilder append(CharSequence text, int start, int end) {
        // length就是插入点的位置
        int length = length();
        // 最终都会调用replace()方法来“增加”内容。从命名可以看出,Google对于字符串改变的设计思路就是“替换”,如果是删内容,就是用空内容替换原内容,如果是增加内容,就是用新内容替换某个内容
        return replace(length, length, text, start, end);
    }
    
    public SpannableStringBuilder replace(final int start, final int end,
                CharSequence tb, int tbstart, int tbend) {
            checkRange("replace", start, end);
    
        // 与setText()一样,都会对新增内容进行过滤
        int filtercount = mFilters.length;
        for (int i = 0; i < filtercount; i++) {
            CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end);
    
            if (repl != null) {
                tb = repl;
                tbstart = 0;
                tbend = repl.length();
            }
        }
    
        // 由于是正常键盘输入,end等于start,所以origLen等于0
        final int origLen = end - start;
        // 新增内容的长度
        final int newLen = tbend - tbstart;
    
        if (origLen == 0 && newLen == 0 && !hasNonExclusiveExclusiveSpanAt(tb, tbstart)) {
            return this;
        }
    
        TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class);
        // 通知TextWatcher调用beforeTextChanged()方法,逻辑跟TextView中的一样,就不再贴代码了
        sendBeforeTextChanged(textWatchers, start, origLen, newLen);
    
        boolean adjustSelection = origLen != 0 && newLen != 0;
        int selectionStart = 0;
        int selectionEnd = 0;
        if (adjustSelection) {
            selectionStart = Selection.getSelectionStart(this);
            selectionEnd = Selection.getSelectionEnd(this);
        }
    
        change(start, end, tb, tbstart, tbend);
    
        if (adjustSelection) {
            if (selectionStart > start && selectionStart < end) {
                final int offset = (selectionStart - start) * newLen / origLen;
                selectionStart = start + offset;
    
                setSpan(false, Selection.SELECTION_START, selectionStart, selectionStart,
                            Spanned.SPAN_POINT_POINT);
                }
            if (selectionEnd > start && selectionEnd < end) {
                final int offset = (selectionEnd - start) * newLen / origLen;
                selectionEnd = start + offset;
    
                setSpan(false, Selection.SELECTION_END, selectionEnd, selectionEnd,
                            Spanned.SPAN_POINT_POINT);
            }
        }
    
        // 通知TextWatcher调用onTextChanged()、afterTextChanged()方法。可以看到,这两个方法是一起调用的,这点跟setText()有点细微差别,总体来说是一样的
        sendTextChanged(textWatchers, start, origLen, newLen);
        sendAfterTextChanged(textWatchers);
    
        sendToSpanWatchers(start, end, newLen - origLen);
    
        return this;
    }
    

    通过分析,大概可以得出如下结论:(通过键盘输入的源码分析可以确认该结论)

    // s:原内容
    // start:被替换内容起点坐标,因为setText()是将原内容全部替换掉,所以起点是0
    // count:被替换内容的长度,因为setText()是将原内容全部替换掉,所以就是mText.length()
    // after:新增加内容的长度
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }
    
    // s:发生改变后的内容
    // start:被替换内容的起点坐标
    // before:被替换内容的长度
    // count:新增加的内容的长度
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    }
    
    // s:发生改变后的内容
    public void afterTextChanged(Editable s) {
    }
    

    总结

    • 最好使用InputFilter对字符串进行控制、过滤。

    • 尽量不要在TextWatcheronTextChanged()方法中对文字进行过滤,然后再调用setText()方法重置字符串,效率明显比InputFilter低

    • 如果一定要在TextWatcheronTextChanged()方法中调用setText()方法,注意防止死循环。因为setText()方法又会回调onTextChanged()方法,会形成死循环。

    • TextWatcher主要功能是进行监听,从Google对该类的命名就可以看出来。。


    鸣谢

    thinkreduce
    原文链接: https://blog.csdn.net/u014606081/article/details/53101629

    相关文章

      网友评论

          本文标题:InputFilter 和 TextWatcher

          本文链接:https://www.haomeiwen.com/subject/rtqvcqtx.html