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;
}

又是嵌套,从上面的代码注释中知道了两点:

  1. text是传入的文字内容;
  2. 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;