TextView setText发生了什么?
前置知识
Span
既然是setText,想必你一定听过Span的大名,借助Span和SpannableString就可以让文字不光是文字,文字颜色、背景颜色、文字点击、链接还可以自定义更复杂的效果,
MovementMethod
文字上的点击、光标选中都是由它实现的,详细的之后单独写。
Editor
和编辑文字相关,之后也单独写。
mTransformation
/**
* Sets the transformation that is applied to the text that this
* TextView is displaying.
*
* @attr ref android.R.styleable#TextView_password
* @attr ref android.R.styleable#TextView_singleLine
*/
public final void setTransformationMethod(TransformationMethod method) {
if (mEditor != null) {
mEditor.setTransformationMethod(method);
} else {
setTransformationMethodInternal(method);
}
}
BufferType
// definition
@UnsupportedAppUsage
private BufferType mBufferType = BufferType.NORMAL;
/**
* Type of the text buffer that defines the characteristics of the text such as static,
* styleable, or editable.
*/
public enum BufferType {
NORMAL, SPANNABLE, EDITABLE
}
正文
作为Android中最基本的控件,TextView简直是无处不在,最最常用的用法像是textView.setText("test")
,无论是传入String,还是SpannableString都能显示,为什么能起作用?
从setText
方法找到方法实现:
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
嗯?CharSequence?
public class String : Comparable<String>, CharSequence{
...
}
expect class StringBuilder : Appendable, CharSequence {
...
}
public class SpannableString
extends SpannableStringInternal
implements CharSequence, GetChars, Spannable
{
...
}
Android中String和StringBuilder都是CharSequence的子类。
mBufferType又是什么东西?先不管它,接着往下找。
/**
* Sets the text to be displayed and the {@link android.widget.TextView.BufferType}.
* <p/>
* When required, TextView will use {@link android.text.Spannable.Factory} to create final or
* intermediate {@link Spannable Spannables}. Likewise it will use
* {@link android.text.Editable.Factory} to create final or intermediate
* {@link Editable Editables}.
*
* Subclasses overriding this method should ensure that the following post condition holds,
* in order to guarantee the safety of the view's measurement and layout operations:
* regardless of the input, after calling #setText both {@code mText} and {@code mTransformed}
* will be different from {@code null}.
*
* @param text text to be displayed
* @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
* stored as a static text, styleable/spannable text, or editable text
*
* @see #setText(CharSequence)
* @see android.widget.TextView.BufferType
* @see #setSpannableFactory(Spannable.Factory)
* @see #setEditableFactory(Editable.Factory)
*
* @attr ref android.R.styleable#TextView_text
* @attr ref android.R.styleable#TextView_bufferType
*/
public void setText(CharSequence text, BufferType type) {
setText(text, type, true, 0);
// drop any potential mCharWrappper leaks
mCharWrapper = null;
}
又是嵌套,从上面的代码注释中知道了两点:
- text是传入的文字内容;
- type定义展示文字类型。
private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen)
聪明的你从名字上一定看出了一些什么,notifyBefore大概率是要不要调用callback了,oldlen是长度。
在看代码实现前,你没有疑问吗?即使setText传入SpannableString,type我们可没改,也就是说用的是默认的,里面一定有判断text类型的逻辑。
去代码里找答案。(大量代码预警)
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
mTextSetFromXmlOrResourceId = false;
if (text == null) {
text = "";
}
// 移除SuggestionSpan,类似Word中拼写检查
// If suggestions are not enabled, remove the suggestion spans from the 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);
}
// 按照mFilters过滤文字
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();
sendBeforeTextChanged(mText, 0, oldlen, text.length());
} else {
sendBeforeTextChanged("", 0, 0, text.length());
}
}
boolean needEditableForNotification = false;
if (mListeners != null && mListeners.size() != 0) {
needEditableForNotification = true;
}
// TextView文字排版相关
PrecomputedText precomputed =
(text instanceof PrecomputedText) ? (PrecomputedText) text : null;
// 可以编辑的内容重置状态,deep copy,最重要的是初始化Editor,后面用到。
if (type == BufferType.EDITABLE || getKeyListener() != null
|| needEditableForNotification) {
createEditorIfNeeded();
mEditor.forgetUndoRedo();
mEditor.scheduleRestartInputForSetText();
Editable t = mEditableFactory.newEditable(text);
text = t;
setFilters(t, mFilters);
} else if (precomputed != null) {
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}
final boolean autoPhraseBreaking =
!mUserSpeficiedLineBreakwordStyle && FeatureFlagUtils.isEnabled(mContext,
FeatureFlagUtils.SETTINGS_AUTO_TEXT_WRAPPING);
final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
precomputed.getParams().checkResultUsable(getPaint(), mTextDir, mBreakStrategy,
mHyphenationFrequency, LineBreakConfig.getLineBreakConfig(
mLineBreakStyle, mLineBreakWordStyle, autoPhraseBreaking));
switch (checkResult) {
case PrecomputedText.Params.UNUSABLE:
throw new IllegalArgumentException(
"PrecomputedText's Parameters don't match the parameters of this TextView."
+ "Consider using setTextMetricsParams(precomputedText.getParams()) "
+ "to override the settings of this TextView: "
+ "PrecomputedText: " + precomputed.getParams()
+ "TextView: " + getTextMetricsParams());
case PrecomputedText.Params.NEED_RECOMPUTE:
precomputed = PrecomputedText.create(precomputed, getTextMetricsParams());
break;
case PrecomputedText.Params.USABLE:
// pass through
}
} else if (type == BufferType.SPANNABLE || mMovement != null) {
// SPANNABLE deep copy
text = mSpannableFactory.newSpannable(text);
} else if (!(text instanceof CharWrapper)) {
text = TextUtils.stringOrSpannedString(text);
}
@AccessibilityUtils.A11yTextChangeType int a11yTextChangeType = AccessibilityUtils.NONE;
if (AccessibilityManager.getInstance(mContext).isEnabled()) {
a11yTextChangeType = AccessibilityUtils.textOrSpanChanged(text, mText);
}
// 设置autolink更新type
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.
*/
setTextInternal(text);
if (a11yTextChangeType == AccessibilityUtils.NONE) {
a11yTextChangeType = AccessibilityUtils.PARCELABLE_SPAN;
}
// 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());
}
}
}
// 更新BufferType
mBufferType = type;
// 更新文字
setTextInternal(text);
// 像是把密码替换成*的文字处理类
if (mTransformation == null) {
mTransformed = text;
} else {
mTransformed = mTransformation.getTransformation(text, this);
}
if (mTransformed == null) {
// Should not happen if the transformation method follows the non-null postcondition.
mTransformed = "";
}
final int textLength = text.length();
final boolean isOffsetMapping = mTransformed instanceof OffsetMapping;
// !!!更新Span
if (text instanceof Spannable && (!mAllowTransformationLengthChange || isOffsetMapping)) {
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();
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) {
final int priority = isOffsetMapping ? OFFSET_MAPPING_SPAN_PRIORITY : 0;
sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
| (priority << Spanned.SPAN_PRIORITY_SHIFT));
}
if (mMovement != null) {
mMovement.initialize(this, (Spannable) text);
/*
* Initializing the movement method will have set the
* selection, so reset mSelectionMoved to keep that from
* interfering with the normal on-focus selection-setting.
*/
if (mEditor != null) mEditor.mSelectionMoved = false;
}
}
if (mLayout != null) {
checkForRelayout();
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
mHideHint = false;
if (a11yTextChangeType == AccessibilityUtils.TEXT) {
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
} else if (a11yTextChangeType == AccessibilityUtils.PARCELABLE_SPAN) {
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
} else {
notifyListeningManagersAfterTextChanged();
}
if (mEditor != null) {
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
mEditor.prepareCursorControllers();
mEditor.maybeFireScheduledRestartInputForSetText();
}
}
乍一看setTextInternal像是更新文字的,点进去才发现是更新变量的。
又一看mMovement.initialize(this, (Spannable) text)
也很像,点进去发现又是重置状态的。
大失所望,又一想文字改变的callback还没看呢!
sendOnTextChanged还是onTextChanged呢,一看sendOnTextChanged有内容而onTextChanged是空实现,那就是sendOnTextChanged。很遗憾虽然有callback,但也不是。
从Span入手
ForegroundColorSpan起作用一定要调用什么方法,ForegroundColorSpan和BackgroundColorSpan同时是抽象类CharacterStyle的子类,updateDrawState方法是需要重写的,依我看十有八九是它。
for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
// Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
// empty by construction. This special case in getSpans() explains the >= & <= tests
if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit)
|| (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continu
boolean insideEllipsis =
mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j]
&& mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd;
final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
if (span instanceof ReplacementSpan) {
replacement = !insideEllipsis ? (ReplacementSpan) span : null;
} else {
// We might have a replacement that uses the draw
// state, otherwise measure state would suffice.
span.updateDrawState(wp);
}
}
验证了上面猜测。但是,Span可以点击呢,当然没有忘记在LinkMovementMethod中实现。
case CLICK:
if (selStart == selEnd) {
return false;
ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class
if (links.length != 1) {
return false;
ClickableSpan link = links[0];
if (link instanceof TextLinkSpan) {
((TextLinkSpan) link).onClick(widget, TextLinkSpan.INVOCATION_METHOD_KEYBOARD);
} else {
link.onClick(widget);
}
break;