InputFilter詳解、TextWatcher詳解

導讀

InputFilter源碼解析、TextWatcher源碼解析

前言

Android中控制EditText輸入內容、長度的方法有三種:
1. 通過添加TextWatcher來監聽變化,實現控制;
2. 通過setFilter()方法設置過濾器;
3. 通過佈局文件中,控件的屬性來控制,比如maxLength、inputType等;
Google爲什麼要提供三種方法來做同樣的事呢?它們之間到底有什麼區別呢?下面就從源碼的角度來分析一下。

InPutFilter

分析源碼之前先打一下基礎,EditText是繼承自TextView,90%的功能跟TextView是一致的,只有4個私有方法,剩下8個是重寫TextView的方法。所以EditText的大部分功能都是在TextView中完成的,具體邏輯也都是在TextView中。

InputFilter是系統提供的一個接口,裏面只有一個方法filter(),用於過濾輸入/插入的字符串,返回值爲CharSequence。
TextView類中的setText()方法中,會調用filter()方法,得到過濾後的字符串,setText()方法源碼如下:

注:setText()方法有很多重載方法,但是最終都會調用下面這個。這個方法很重要,下面所有的分析都會經過這裏,只需要看關鍵邏輯處的註釋。

// mText:發生變化前TextView中的內容
// text:將要設置的新內容
private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        if (text == null) {
            text = "";
        }

        if (!isSuggestionsEnabled()) {
            text = removeSuggestionSpans(text);
        }

        if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);

        if (text instanceof Spanned &&
            ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
            if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
                setHorizontalFadingEdgeEnabled(true);
                mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
            } else {
                setHorizontalFadingEdgeEnabled(false);
                mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
            }
            setEllipsize(TextUtils.TruncateAt.MARQUEE);
        }

        // 使用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;
            }
        }

        if (notifyBefore) {
            if (mText != null) {
                oldlen = mText.length();
                // 通知調用TextWatcher的beforeTextChanged()方法
                sendBeforeTextChanged(mText, 0, oldlen, text.length());
            } else {
                sendBeforeTextChanged("", 0, 0, text.length());
            }
        }

        boolean needEditableForNotification = false;

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

        // 如果是EditText,就new一個Editable
        if (type == BufferType.EDITABLE || getKeyListener() != null ||
                needEditableForNotification) {
            createEditorIfNeeded();
            Editable t = mEditableFactory.newEditable(text);
            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 (mAutoLinkMask != 0) {
            Spannable s2;

            if (type == BufferType.EDITABLE || text instanceof Spannable) {
                s2 = (Spannable) text;
            } else {
                s2 = mSpannableFactory.newSpannable(text);
            }

            if (Linkify.addLinks(s2, mAutoLinkMask)) {
                text = s2;
                type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
                mText = text;
                if (mLinksClickable && !textCanBeSelected()) {
                    setMovementMethod(LinkMovementMethod.getInstance());
                }
            }
        }

        mBufferType = type;
        // 用新內容替換舊內容
        mText = text;

        if (mTransformation == null) {
            mTransformed = text;
        } else {
            mTransformed = mTransformation.getTransformation(text, this);
        }

        final int textLength = text.length();

        if (text instanceof Spannable && !mAllowTransformationLengthChange) {
            Spannable sp = (Spannable) text;

            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();

            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);
            }

            if (mMovement != null) {
                mMovement.initialize(this, (Spannable) text);
                if (mEditor != null) mEditor.mSelectionMoved = false;
            }
        }

        if (mLayout != null) {
            checkForRelayout();
        }

        // 通知調用TextWatcher的onTextChanged()方法
        sendOnTextChanged(text, 0, oldlen, textLength);
        onTextChanged(text, 0, oldlen, textLength);

        // 通知view刷新 
      notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);

        if (needEditableForNotification) {
            // 通知調用TextWatcher的afterTextChanged()方法
            sendAfterTextChanged((Editable) text);
        }

        if (mEditor != null) mEditor.prepareCursorControllers();
    }

mFilters是一個InputFilter數組,很好理解,因爲需要設置一個或多個過濾器。通過for循環,把text按照所有過濾條件全部過濾一遍,最終得到“合格”的text。

知道了InputFilter是如何起作用的,那麼剩下的就是搞清楚filter()方法中的各個參數的含義,寫出自己需要的InputFilter。

SDK提供了兩個實現:AllCaps和LengthFilter,下面以LengthFilter解讀InputFilter的用法,源碼片段如下:

public static class LengthFilter implements InputFilter {

        private final int mMax;

        public LengthFilter(int max) {
            mMax = max;
        }

        //參數source:將要插入的字符串,來自鍵盤輸入、粘貼
        //參數start:source的起始位置,爲0(暫時沒有發現其它值的情況)
        //參數end:source的長度
        //參數dest:EditText中已經存在的字符串
        //參數dstart:插入點的位置
        //參數dend:插入點的結束位置,一般情況下等於dstart;如果選中一段字符串(這段字符串將會被替換),dstart的值就插入點的結束位置
        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;
        }
    }

maxLength、inputType屬性

TextView的構造方法中,會通過TypedArray獲取各種屬性,用於初始化。下面爲構造方法源碼片段(只貼出主要邏輯):

// 讀取控件參數
switch (attr) {
    case com.android.internal.R.styleable.TextView_maxLength:
        maxlength = a.getInt(attr, -1);
    break;
}

.................
.............
.................

// 該方法的主要邏輯在下面貼出
setInputType(inputType, true);

.................
.............
.................

// 設置過濾器(注意:不管有沒有設置maxLength屬性,都會調用setFilters()方法)
if (maxlength >= 0) {
    setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
} else {
    setFilters(NO_FILTERS);
}

setInputType()方法源碼片段如下(只貼出主要邏輯):

if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
    // 如果inputType = number,就創建DigitsKeyListener
    // DigitsKeyListener繼承自NumberKeyListener,而NumberKeyListener實現了InputFilter接口
    input = DigitsKeyListener.getInstance(
                    (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
                    (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
}

// 把上面創建的DigitsKeyListener賦值給成員變量mKeyListener
mEditor.mKeyListener = input;

maxLength屬性

可以看到,對於android:maxLength屬性來說,就是直接通過setFilters()方法設置LengthFilter。如果代碼中調用了setFilters()方法重新設置過濾器,android:maxLength屬性就會失效。

inputType屬性

那inputType屬性又是怎樣生效的呢?
上面說過,不管有沒有設置maxLength屬性,都會調用setFilters()方法,關鍵就在setFilters()方法中:

private void setFilters(Editable e, InputFilter[] filters) {
        if (mEditor != null) {
            final boolean undoFilter = mEditor.mUndoInputFilter != null;
            // mKeyListener繼承自NumberKeyListener,實現了InputFilter接口,所以這裏爲true
            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) {
                    // 將mKeyListener添加到filters集合中。setText()、鍵盤輸入時都會遍歷filters集合進行過濾
                    nf[filters.length + num] = (InputFilter) mEditor.mKeyListener;
                }

                e.setFilters(nf);
                return;
            }
        }
        e.setFilters(filters);
    }

可以看到,inputType是一定能生效的,而且不會與maxLength、setFilters()衝突。

TextWatcher

網上關於TextWatcher問的最多的就是裏面的各個參數各代表什麼意思?onTextChaned()和afterTextChanged()有什麼區別?下面就分析源碼來搞清楚這幾個問題。

先看TextView源碼中的addTextChangedListener():

    public void addTextChangedListener(TextWatcher watcher) {
        if (mListeners == null) {
            mListeners = new ArrayList<TextWatcher>();
        }
        mListeners.add(watcher);
    }

這裏可看到,是可以添加多個TextWatcher的,典型的觀察者模式,TextView會在文字發生變化時,遍歷集合中的TextWatcher,發送通知,如下:

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);
    }

只列舉sendBeforeTextChanged(),另外兩個方法sendOnTextChanged()、sendAfterTextChanged()類似。

那什麼時候會調用sendBeforeTextChanged()方法呢?
有兩種情況會使TextView裏面的內容發生變化,從而通知監聽器,第一種就是setText()方法,第二種就是從鍵盤輸入。

Google對於“改變字符串”的設計理念就是“替換”。如果是刪內容,就是用空字符串替換需要刪除的字符串;如果是增加內容,就是用新字符串替換空字符串。所以要先搞清楚下面幾個概念:
1. 原內容:發生改變前TextView中的內容;
2. 被替換內容起點座標:編輯一段內容時,有可能是直接添加新內容,也有可能是選中一段原有內容,用新內容把它替換掉;
3. 被替換內容的長度:如果是直接添加新內容,被替換內容的長度就是0;
4. 新增加的內容:對於setText()來說,就是方法中的參數,對於鍵盤輸入來說,就是鍵盤輸入的內容

再來分析這兩種情況。

情況一:setText()

setText()的源碼在上面已經貼出,並寫了註釋。

通過分析,大概可以得出如下結論:(通過鍵盤輸入的源碼分析可以確認該結論)

// 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) {
}

情況二:鍵盤輸入

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;
}

通過上面的分析,即可確認前面的結論。

總結

  1. 使用InputFilter對字符串進行控制、過濾。
  2. 儘量不要在TextWatcher的onTextChanged()方法中對文字進行過濾,然後再調用setText()方法重置字符串,效率明顯比InputFilter低。
  3. 如果一定要在TextWatcher的onTextChanged()方法中調用setText()方法,注意防止死循環。因爲setText()方法又會回調onTextChanged()方法,會形成死循環。
  4. TextWatcher主要功能是進行監聽,從Google對該類的命名就可以看出來。
發佈了35 篇原創文章 · 獲贊 52 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章