Android 成長在于積累和分享
版權聲明:本文為參考博主編寫的原創(chuàng)文章馁启,原文: https://blog.csdn.net/u014606081/article/details/53101629
[TOC]
UCS 移動端技術分享
技術形式的一種分享蚓让,發(fā)現(xiàn)在寫過程中參考別人博客的同時發(fā)現(xiàn)確實他寫的更完善點 ,所以在他博客的基礎上添加了一部分自己理解的東西鉴嗤。也是給大家看一下盟劫,如果有不對的地方航缀,希望我們一起修正和學習商架。
導讀
InputFilter源碼解析、TextWatcher源碼解析
前言
Android中 InputFilter 和 TextWatcher 的功能和作用非常相似芥玉,都可以做到對 EditText 輸入內(nèi)容的監(jiān)聽及控制蛇摸。那兩者具體有什么區(qū)別,又是如何實現(xiàn)對輸入內(nèi)容進行監(jiān)聽的灿巧。下面我們就從源碼的角度一起分析一下赶袄。
分析源碼之前先打一下基礎,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(暫時沒有發(fā)現(xiàn)其它值的情況)輸入-0髓梅,刪除-0
* @param end source的長度: 輸入-文字的長度拟蜻,刪除-0
* @param dest EditText中已經(jīng)存在的字符串绎签,原先顯示的內(nèi)容
* @param dstart 插入點的位置:輸入-原光標位置,刪除-光標刪除結束位置
* @param dend 輸入-原光標位置酝锅,刪除-光標刪除開始位置
*/
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend);
...略...
}
在TextView類中的setText()方法诡必,會調(diào)用filter()方法,得到過濾后的字符串搔扁,部分源碼如下:
注:setText()方法有很多重載方法爸舒,但是最終都會調(diào)用下面這個。這個方法很重要稿蹲,只需要看關鍵邏輯處的注釋扭勉。
/**
* @param text 將要設置的新內(nèi)容
* @param type 內(nèi)容的設置類型(static text, styleable/spannable text, or editable text)
* @param notifyBefore 是否需要觸發(fā)TextWacther的before
* @param oldlen 新增加內(nèi)容的長度
*/
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的另外一處過濾調(diào)用處
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數(shù)組苛聘,因為有一個或多個過濾器涂炎。通過for循環(huán),把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方法的實現(xiàn)战虏。
// 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()方法中的各個參數(shù)的含義,寫出自己需要的InputFilter烦感。
SDK提供了兩個實現(xiàn):AllCaps和LengthFilter巡社,下面以LengthFilter解讀InputFilter的用法,源碼片段如下:
public static class LengthFilter implements InputFilter {
private final int mMax;
public LengthFilter(int max) {
mMax = max;
}
//參數(shù)source:將要插入的字符串啸盏,來自鍵盤輸入重贺、粘貼
//參數(shù)start:source的起始位置,為0(暫時沒有發(fā)現(xiàn)其它值的情況)輸入-0,刪除-0
//參數(shù)end:source的長度: 輸入-文字的長度气笙,刪除-0
//參數(shù)dest:EditText中已經(jīng)存在的字符串次企,原先顯示的內(nèi)容
//參數(shù)dstart:插入點的位置:輸入-原光標位置,刪除-光標刪除結束位置
//參數(shù)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) {
// 如果超出字數(shù)限制缸棵,就返回“”
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隧出,保證不超出字數(shù)限制
--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原內(nèi)容
* @param start 被替換內(nèi)容起點坐標
* @param count 被替換內(nèi)容的長度
* @param after 新增加內(nèi)容的長度
*/
public void beforeTextChanged(CharSequence s, int start,
int count, int after);
/**
* @param s 發(fā)生改變后的內(nèi)容
* @param start 被替換內(nèi)容的起點坐標
* @param before 被替換內(nèi)容的長度
* @param count 新增加的內(nèi)容的長度
*/
public void onTextChanged(CharSequence s, int start, int before, int count);
/**
* @param s 發(fā)生改變后的內(nèi)容(對s編輯同樣會觸發(fā)TextWatcher)
*/
public void afterTextChanged(Editable s);
}
有兩種情況會使TextView里面的內(nèi)容發(fā)生變化胀瞪,從而通知監(jiān)聽器针余,第一種就是setText()方法,第二種就是從鍵盤輸入凄诞。兩種都會調(diào)用sendBeforeTextChanged方法發(fā)送通知圆雁,如下(舉例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對于“改變字符串”的設計理念就是“替換”烈涮。如果是刪內(nèi)容,就是用空字符串替換需要刪除的字符串患雇;如果是增加內(nèi)容跃脊,就是用新字符串替換空字符串。所以要先搞清楚下面幾個概念:
- 原內(nèi)容:發(fā)生改變前TextView中的內(nèi)容苛吱;
- 被替換內(nèi)容起點坐標:編輯一段內(nèi)容時酪术,有可能是直接添加新內(nèi)容,也有可能是選中一段原有內(nèi)容翠储,用新內(nèi)容把它替換掉绘雁;
- 被替換內(nèi)容的長度:如果是直接添加新內(nèi)容,被替換內(nèi)容的長度就是0援所;
- 新增加的內(nèi)容:對于setText()來說庐舟,就是方法中的參數(shù),對于鍵盤輸入來說住拭,就是鍵盤輸入的內(nèi)容
再來分析這兩種情況挪略。
情況一:setText()
同樣的历帚,先看下TextView中setText()方法對相關事件的處理
/**
* @param text 將要設置的新內(nèi)容
* @param type 內(nèi)容的設置類型(static text, styleable/spannable text, or editable text)
* @param notifyBefore 是否需要觸發(fā)TextWacther的before
* @param oldlen 新增加內(nèi)容的長度
*/
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
mTextFromResource = false;
if (text == null) {
text = "";
}
...略...
// 先過濾,再確認是否發(fā)送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[]類型時會提前發(fā)送sendBeforeTextChanged杠娱,此處的notifyBefore即為false
if (notifyBefore) {
// 通知調(diào)用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); // 創(chuàng)建Editable挽牢,后面鍵盤輸入會用到相關監(jiān)聽
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監(jiān)聽(可以看一下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);
}
...略...
}
...略...
// 通知調(diào)用TextWatcher的onTextChanged()方法
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
// 通知view刷新
notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
// 通知調(diào)用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();
}
這里可以看到,如果調(diào)用setText方法是一定會觸發(fā)TextWatcher相關事件的摊求,所以盡量不要在TextWatcher的onTextChanged()方法中對文字進行過濾禽拔,然后再調(diào)用setText()方法重置字符串,
如果一定要在TextWatcher的onTextChanged()方法中調(diào)用setText()方法(某些很難受的需求)室叉,注意防止死循環(huán)睹栖。因為setText()方法又會回調(diào)onTextChanged()方法,會形成死循環(huán)茧痕。
情況二:鍵盤輸入
TextView的構造方法中野来,會獲取android:text屬性的值,調(diào)用setText()方法設置初始內(nèi)容凿渊。其中梁只,就會判斷BufferType的類型缚柳,如果是EditText埃脏,就會創(chuàng)建Editable(此段邏輯見上面setText()源碼)。
最終new出SpannableStringBuilder對象秋忙,SpannableStringBuilder實現(xiàn)了Editable彩掐、Appendable接口。Appendable提供了一個接口(有三個重載的):append()灰追,用來把新內(nèi)容(來自鍵盤輸入)添加到原內(nèi)容中堵幽。所以我們?nèi)pannableStringBuilder里看看append()方法的具體實現(xiàn)。
三個重載的接口弹澎,就有三個具體實現(xiàn)朴下,但原理都一樣,最終都會調(diào)用replace()方法苦蒿。下面以其中一個append()實現(xiàn)來分析:
// 鍵盤輸入有兩種:一種是正常輸入殴胧;另一種是先選中一段內(nèi)容,再從鍵盤輸入佩迟,新內(nèi)容會替換掉選中的內(nèi)容团滥;
// 這個方法是正常輸入時調(diào)用
public SpannableStringBuilder append(CharSequence text, int start, int end) {
// length就是插入點的位置
int length = length();
// 最終都會調(diào)用replace()方法來“增加”內(nèi)容。從命名可以看出报强,Google對于字符串改變的設計思路就是“替換”灸姊,如果是刪內(nèi)容,就是用空內(nèi)容替換原內(nèi)容秉溉,如果是增加內(nèi)容力惯,就是用新內(nèi)容替換某個內(nèi)容
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()一樣碗誉,都會對新增內(nèi)容進行過濾
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;
// 新增內(nèi)容的長度
final int newLen = tbend - tbstart;
if (origLen == 0 && newLen == 0 && !hasNonExclusiveExclusiveSpanAt(tb, tbstart)) {
return this;
}
TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class);
// 通知TextWatcher調(diào)用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調(diào)用onTextChanged()诱建、afterTextChanged()方法蝴蜓。可以看到俺猿,這兩個方法是一起調(diào)用的茎匠,這點跟setText()有點細微差別,總體來說是一樣的
sendTextChanged(textWatchers, start, origLen, newLen);
sendAfterTextChanged(textWatchers);
sendToSpanWatchers(start, end, newLen - origLen);
return this;
}
通過分析押袍,大概可以得出如下結論:(通過鍵盤輸入的源碼分析可以確認該結論)
// s:原內(nèi)容
// start:被替換內(nèi)容起點坐標诵冒,因為setText()是將原內(nèi)容全部替換掉,所以起點是0
// count:被替換內(nèi)容的長度谊惭,因為setText()是將原內(nèi)容全部替換掉汽馋,所以就是mText.length()
// after:新增加內(nèi)容的長度
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
// s:發(fā)生改變后的內(nèi)容
// start:被替換內(nèi)容的起點坐標
// before:被替換內(nèi)容的長度
// count:新增加的內(nèi)容的長度
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
// s:發(fā)生改變后的內(nèi)容
public void afterTextChanged(Editable s) {
}
總結
最好使用InputFilter對字符串進行控制、過濾圈盔。
盡量不要在TextWatcher的onTextChanged()方法中對文字進行過濾豹芯,然后再調(diào)用setText()方法重置字符串,效率明顯比InputFilter低驱敲。
如果一定要在TextWatcher的onTextChanged()方法中調(diào)用setText()方法铁蹈,注意防止死循環(huán)。因為setText()方法又會回調(diào)onTextChanged()方法众眨,會形成死循環(huán)握牧。
TextWatcher主要功能是進行監(jiān)聽,從Google對該類的命名就可以看出來娩梨。沿腰。
鳴謝
thinkreduce
原文鏈接: https://blog.csdn.net/u014606081/article/details/53101629