导读
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;
}
通过上面的分析,即可确认前面的结论。
总结
- 使用InputFilter对字符串进行控制、过滤。
- 尽量不要在TextWatcher的onTextChanged()方法中对文字进行过滤,然后再调用setText()方法重置字符串,效率明显比InputFilter低。
- 如果一定要在TextWatcher的onTextChanged()方法中调用setText()方法,注意防止死循环。因为setText()方法又会回调onTextChanged()方法,会形成死循环。
- TextWatcher主要功能是进行监听,从Google对该类的命名就可以看出来。