InputFilter 和 TextWatcher
Android 成長在于積累和分享
原文:https://www.jianshu.com/p/223dcbac3e4d
版權聲明:本文為參考博主編寫的原創文章,參考原文: https://blog.csdn.net/u014606081/article/details/53101629
文章目錄
- UCS 移動端技術分享
- 導讀
- 前言
- InputFilter
- TextWatcher
- 情況一:setText()
- 情況二:鍵盤輸入
- 總結
- 鳴謝
UCS 移動端技術分享
技術形式的一種分享,發現在寫過程中參考別人博客的同時發現確實他寫的更完善點 ,所以在他博客的基礎上添加了一部分自己理解的東西。也是給大家看一下,如果有不對的地方,希望我們一起修正和學習。
導讀
InputFilter源碼解析、TextWatcher源碼解析
前言
Android中 InputFilter 和 TextWatcher 的功能和作用非常相似,都可以做到對 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處理textint 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一個Editableif (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方法,在鍵盤鍵入的過程中同樣會過濾,TextView的setFilters方法給Editable設置了filters,并在Editable的replace方法中進行過濾。
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);舉例SpannableStringBuilder中replace方法的實現。
// Documentation from interfacepublic 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提供了兩個實現:AllCaps和LengthFilter,下面以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 senseremoveIntersectingNonAdjacentSpans(start, start + before, SpellCheckSpan.class);removeIntersectingNonAdjacentSpans(start, start + before, SuggestionSpan.class);}首先,給大家介紹下Google的設計理念,說實話,我不知道原文出自哪,這個是從某個大佬博客上摘過來的。
摘
Google對于**“改變字符串”的設計理念就是“替換”**。如果是刪內容,就是用空字符串替換需要刪除的字符串;如果是增加內容,就是用新字符串替換空字符串。所以要先搞清楚下面幾個概念:
再來分析這兩種情況。
情況一:setText()
同樣的,先看下TextView中**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 = "";}...略...// 先過濾,再確認是否發送TextChangeint 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即為falseif (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 textif (mEditor != null) mEditor.prepareCursorControllers();}這里可以看到,如果調用setText方法是一定會觸發TextWatcher相關事件的,所以盡量不要在TextWatcher的**onTextChanged()方法中對文字進行過濾,然后再調用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等于0final 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對字符串進行控制、過濾。
-
盡量不要在TextWatcher的**onTextChanged()方法中對文字進行過濾,然后再調用setText()**方法重置字符串,效率明顯比InputFilter低。
-
如果一定要在TextWatcher的**onTextChanged()方法中調用setText()方法,注意防止死循環。因為setText()方法又會回調onTextChanged()**方法,會形成死循環。
-
TextWatcher主要功能是進行監聽,從Google對該類的命名就可以看出來。。
鳴謝
thinkreduce
原文鏈接: https://blog.csdn.net/u014606081/article/details/53101629
總結
以上是生活随笔為你收集整理的InputFilter 和 TextWatcher的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 泰文Unicode编码表及排版规则
- 下一篇: [js] 写一个获取页面中所有chec