使用TextView,如果在代码里添加了maxLength属性,那么有一点需要注意:
如果TextView的文案在显示时发生了截断,那么这时调用TextView.getText()方法返回的字符串,是截断后的子串,而不是之前TextView.setText()方法传入的字符串!
也就是说,使用了maxLength属性后,不能保证setText()和getText()方法的字符串守恒定律。
原因都在代码里,先去看TextView.getText()源码:
public CharSequence getText() {
return mText;
}
很简洁,那就去追查mText被赋值的地方吧!于是来到TextView.setText()方法:
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
if (text == null) {
text = "";
}
// If suggestions are not enabled, remove the suggestion spans from the text
if (!isSuggestionsEnabled()) {
text = removeSuggestionSpans(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 (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;
/*
* We must go ahead and set the text before changing the
* movement method, because setMovementMethod() may call
* setText() again to try to upgrade the buffer type.
*/
mText = text;
// Do not change the movement method for text that support text selection as it
// would prevent an arbitrary cursor displacement.
if (mLinksClickable && !textCanBeSelected()) {
setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
mBufferType = type;
mText = text; ...... }
上面的方法重载了TextView其他几个public的方法,其他的public方法最终都是调用此方法,我省略了一些和mText赋值无关的代码片段(剩下的还是很多)。可以看到这个方法的大致逻辑是根据mBufferType来生成不同的对象(均实现CharSequence接口),例如String、Spanned、Editable或是CharWrapper,并赋值给mText。
除了上述的主逻辑,修改mText变量的地方就剩下setText()中应用filter的代码块了:
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;
}
}
由于我当时并没有设置任何过滤器,所以一开始根本没有怀疑到这里,直到我不小心看到TextView.setFilters()方法的注释:
/**
* Sets the list of input filters that will be used if the buffer is
* Editable. Has no effect otherwise.
*
* @attr ref android.R.styleable#TextView_maxLength
*/
public void setFilters(InputFilter[] filters) {
if (filters == null) {
throw new IllegalArgumentException();
}
mFilters = filters;
if (mText instanceof Editable) {
setFilters((Editable) mText, filters);
}
}
没错,这个方法对应的resource 属性居然就是看似无辜的maxLength,赶快去看TextView构造器中读取并应用maxLength的代码:
if (maxlength >= 0) {
setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
} else {
setFilters(NO_FILTERS);
}
真相大白,添加maxLength属性,就相当于设置了InputFilter,超过maxLength的直接被截断,根本不会保存。
思考:maxLength属性一般给开发者的感觉和layout_width类似,是一个用于约束UI控件布局的属性,但是真没想到它却是会直接修改mText本身的。不知是谷歌设计的瑕疵,还是我自己的思维定势,不过看来以后还是要多读代码,不可望文生义。
有趣的是,当时碰到这个问题在SO上搜时,发现好多人在求问如何让getText()方法返回截断后的子串,而不是返回原始字符串(可以通过TextUtils.ellipsize()方法),当时很诧异:真是身在福中不知福啊!后来才发现,逆行的是自己,哈哈。