Layout是一个抽象类,负责管理文字布局。Layout有三个实现类:BoringLayout、StaticLayout、DynamicLayout。BoringLayout布局用于显示一行文字并且字符从左至右。StaticLayout用于文字布局之后不会再改变。DynamicLayout用于文本编辑时可以自己更新显示效果。并且这三个类均说明了最好不要直接使用这些类,除非你想实现自己显示的控件。
控件的布局受到父布局和自身设置的共同限制。下面分情况讨论:
- 如果TextView的高度和宽度都是定尺寸,那么不管setText传入的什么内容,都不会重新布局,因为尺寸定死了。
- 如果TextView的宽度是定尺寸,而高度是wrap_content,那么如果在setText重新设置内容之前,高度是两行高度,那么当重新设置的内容如果高于两行或者低于两行,都应该重新布局的,这时会要求父布局重新布局。
- 如果TextView的宽度wrap_content,高度是定尺寸,那么一旦宽度发生变化,就会要求父布局重新布局。
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);
}
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);
}
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;
}
if (type == BufferType.EDITABLE || getKeyListener() != null ||
needEditableForNotification) {
createEditorIfNeeded();
mEditor.forgetUndoRedo();
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;
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;
// 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) {
sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
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);
notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
}
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
if (mEditor != null) mEditor.prepareCursorControllers();
}
setText负责将text进行转换,并且如果设置了TextWatch监听器,那么需要调用接口。其中70-97行的代码是用于XML中如果设置了autoLink,那么需要对text进行处理,可以看到首先将text转换为Spannable,然后再调用Linkify.addLinks方法为text添加效果,最后会自动调用setMovementMethod,所以一个autoLink属性就解决所有问题了。下面是addLinks的方法实现:
public static final boolean addLinks(Spannable text, int mask) {
if (mask == 0) {
return false;
}
URLSpan[] old = text.getSpans(0, text.length(), URLSpan.class);
for (int i = old.length - 1; i >= 0; i--) {
text.removeSpan(old[i]);
}
ArrayList links = new ArrayList();
if ((mask & WEB_URLS) != 0) {
gatherLinks(links, text, Patterns.WEB_URL,
new String[] { "http://", "https://", "rtsp://" },
sUrlMatchFilter, null);
}
if ((mask & EMAIL_ADDRESSES) != 0) {
gatherLinks(links, text, Patterns.EMAIL_ADDRESS,
new String[] { "mailto:" },
null, null);
}
if ((mask & PHONE_NUMBERS) != 0) {
gatherTelLinks(links, text);
}
if ((mask & MAP_ADDRESSES) != 0) {
gatherMapLinks(links, text);
}
pruneOverlaps(links);
if (links.size() == 0) {
return false;
}
for (LinkSpec link: links) {
applyLink(link.url, link.start, link.end, text);
}
return true;
}
该方法扫描文本然后将其中的链接更改显示效果并添加点击事件。从方法中可以看出,首先移除text中已经设置的URLSpan,也就是说如果即通过了代码添加了URLSpan,也在属性中使用了autoLink属性,那么代码中的添加的URLSpan全部失效。接下来就是判断autoLink的类型,扫描出符合条件的LinkSpec,再加入到列表中;然后再调用pruneOverlapas用于删除重复的效果;最后调用applyLink产生效果。下面是gatherLinks方法实现:
private static final void gatherLinks(ArrayList links,
Spannable s, Pattern pattern, String[] schemes,
MatchFilter matchFilter, TransformFilter transformFilter) {
Matcher m = pattern.matcher(s);
while (m.find()) {
int start = m.start();
int end = m.end();
if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) {
LinkSpec spec = new LinkSpec();
String url = makeUrl(m.group(0), schemes, m, transformFilter);
spec.url = url;
spec.start = start;
spec.end = end;
links.add(spec);
}
}
}
可以看到该方法就是调用正则表达式匹配符合条件的链接,然后加入到列表中。其中LinkSpec类的定义如下:
class LinkSpec {
String url;
int start;
int end;
}
LinkSpec就是封装了URL、以及text中的开启位置和结束位置。下面是applyLink方法的实现,该方法用于将找到的LinkSpec真正产生效果:
private static final void applyLink(String url, int start, int end, Spannable text) {
URLSpan span = new URLSpan(url);
text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
可以看到该方法也是通过给text设置URLSpan实现效果的,所以最终的效果都是依赖URLSpan实现的。
Spannable的各种效果,最终都是通过onDraw方法绘制出来的,下面是onDraw()方法的实现。
@Override
protected void onDraw(Canvas canvas) {
restartMarqueeIfNeeded();
// Draw the background for this view
super.onDraw(canvas);
final int compoundPaddingLeft = getCompoundPaddingLeft();
final int compoundPaddingTop = getCompoundPaddingTop();
final int compoundPaddingRight = getCompoundPaddingRight();
final int compoundPaddingBottom = getCompoundPaddingBottom();
final int scrollX = mScrollX;
final int scrollY = mScrollY;
final int right = mRight;
final int left = mLeft;
final int bottom = mBottom;
final int top = mTop;
final boolean isLayoutRtl = isLayoutRtl();
final int offset = getHorizontalOffsetForDrawables();
final int leftOffset = isLayoutRtl ? 0 : offset;
final int rightOffset = isLayoutRtl ? offset : 0 ;
final Drawables dr = mDrawables;
if (dr != null) {
/*
* Compound, not extended, because the icon is not clipped
* if the text height is smaller.
*/
int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mShowing[Drawables.LEFT] != null) {
canvas.save();
canvas.translate(scrollX + mPaddingLeft + leftOffset,
scrollY + compoundPaddingTop +
(vspace - dr.mDrawableHeightLeft) / 2);
dr.mShowing[Drawables.LEFT].draw(canvas);
canvas.restore();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mShowing[Drawables.RIGHT] != null) {
canvas.save();
canvas.translate(scrollX + right - left - mPaddingRight
- dr.mDrawableSizeRight - rightOffset,
scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
dr.mShowing[Drawables.RIGHT].draw(canvas);
canvas.restore();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mShowing[Drawables.TOP] != null) {
canvas.save();
canvas.translate(scrollX + compoundPaddingLeft +
(hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);
dr.mShowing[Drawables.TOP].draw(canvas);
canvas.restore();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mShowing[Drawables.BOTTOM] != null) {
canvas.save();
canvas.translate(scrollX + compoundPaddingLeft +
(hspace - dr.mDrawableWidthBottom) / 2,
scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
dr.mShowing[Drawables.BOTTOM].draw(canvas);
canvas.restore();
}
}
int color = mCurTextColor;
if (mLayout == null) {
assumeLayout();
}
Layout layout = mLayout;
if (mHint != null && mText.length() == 0) {
if (mHintTextColor != null) {
color = mCurHintTextColor;
}
layout = mHintLayout;
}
mTextPaint.setColor(color);
mTextPaint.drawableState = getDrawableState();
canvas.save();
/* Would be faster if we didn't have to do this. Can we chop the
(displayable) text so that we don't need to do this ever?
*/
int extendedPaddingTop = getExtendedPaddingTop();
int extendedPaddingBottom = getExtendedPaddingBottom();
final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
final int maxScrollY = mLayout.getHeight() - vspace;
float clipLeft = compoundPaddingLeft + scrollX;
float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
float clipRight = right - left - getFudgedPaddingRight() + scrollX;
float clipBottom = bottom - top + scrollY -
((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
if (mShadowRadius != 0) {
clipLeft += Math.min(0, mShadowDx - mShadowRadius);
clipRight += Math.max(0, mShadowDx + mShadowRadius);
clipTop += Math.min(0, mShadowDy - mShadowRadius);
clipBottom += Math.max(0, mShadowDy + mShadowRadius);
}
canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
int voffsetText = 0;
int voffsetCursor = 0;
// translate in by our padding
/* shortcircuit calling getVerticaOffset() */
if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
voffsetText = getVerticalOffset(false);
voffsetCursor = getVerticalOffset(true);
}
canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
(absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
final int width = mRight - mLeft;
final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
final float dx = mLayout.getLineRight(0) - (width - padding);
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
if (mMarquee != null && mMarquee.isRunning()) {
final float dx = -mMarquee.getScroll();
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
}
final int cursorOffsetVertical = voffsetCursor - voffsetText;
Path highlight = getUpdatedHighlightPath();
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
if (mMarquee != null && mMarquee.shouldDrawGhost()) {
final float dx = mMarquee.getGhostOffset();
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
canvas.restore();
}
TextView是可以设置Drawable,上述代码首先先绘制Drawable。然后获取Layout对象,该对象用于绘制文本。接下来就是各种控制属性的计算,最后在409-413行会绘制文本,并且通过区分mEditor是不是为null来判断,TextView中mEditor是为null的,而EditText中mEditor是不为空的,所以TextView的绘制最终是通过Layout的draw方法绘制的,而EditText是通过Editor的绘制形成的。最终是判断是不是处于“跑马灯”的设置(暂且不考虑)。下面是assumeLayout方法,该方法根据文本内容返回Layout对象。
/**
* Make a new Layout based on the already-measured size of the view,
* on the assumption that it was measured correctly at some point.
*/
private void assumeLayout() {
int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
if (width < 1) {
width = 0;
}
int physicalWidth = width;
if (mHorizontallyScrolling) {
width = VERY_WIDE;
}
makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
physicalWidth, false);
}
可以看到该方法又是调用了makeNewLayout方法,而makeNewLayout的关键方法是makeSingleLayout,下面是makeSingleLayout方法的实现:
private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
boolean useSaved) {
Layout result = null;
if (mText instanceof Spannable) {
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
mBreakStrategy, mHyphenationFrequency,
getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
} else {
if (boring == UNKNOWN_BORING) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
}
if (boring != null) {
if (boring.width <= wantWidth &&
(effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
}
if (useSaved) {
mSavedLayout = (BoringLayout) result;
}
} else if (shouldEllipsize && boring.width <= wantWidth) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
}
}
}
}
if (result == null) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
0, mTransformed.length(), mTextPaint, wantWidth)
.setAlignment(alignment)
.setTextDirection(mTextDir)
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)
.setEllipsizedWidth(ellipsisWidth)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
}
// TODO: explore always setting maxLines
result = builder.build();
}
return result;
}
可以看到根据不同的情况,返回不同的Layout具体实现。如果TextView目前显示的文本是Spannable类型的话,那么返回DynamicLayout;如果文本不是Spannable类型的话,并且是boring的话,那么返回BoringLayout;如果文本既不是Spannable类型的话也不是boring的话,那么返回StaticLayout。那么什么样的文本是boring呢?根据BoringLayout文档来说,就是可以在一行内显示的文本并且文本方向是从左向右的,那么就是Boring的,否则就不是。那么下面总结一下,什么情况下会返回什么Layout。
- 如果文本属于Spannable类型,那么返回DynamicLayout;
- 如果文本不属于Spannable类型,并且文本可以在一行内显示完并且顺序从左至右,那么返回BoringLayout;
- 如果文本不属于Spannable类型,文本需多行显示或文本方向是从右至左,那么返回StaticLayout。
从前面知道TextView的绘制最终是通过调用layout的draw方法,因为我们考虑的文本是Spannbale类型的,所以直接看DynamicLayout的draw方法,而DynamicLayout没有覆写父类的draw方法,Layout有几个draw方法,但最终都是调用下面的draw方法,下面是Layout的draw方法实现:
/**
* Draw this Layout on the specified canvas, with the highlight path drawn
* between the background and the text.
*
* @param canvas the canvas
* @param highlight the path of the highlight or cursor; can be null
* @param highlightPaint the paint for the highlight
* @param cursorOffsetVertical the amount to temporarily translate the
* canvas while rendering the highlight
*/
public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
int cursorOffsetVertical) {
final long lineRange = getLineRangeForDraw(canvas);
int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
if (lastLine < 0) return;
drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
firstLine, lastLine);
drawText(canvas, firstLine, lastLine);
}
可以看到draw方法就是获取绘制文本的行数,然后得到第一行和最后一行,先去画背景,再去画文字。下面首先看画背景的drawBackground方法:
/**
* @hide
*/
public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
int cursorOffsetVertical, int firstLine, int lastLine) {
// First, draw LineBackgroundSpans.
// LineBackgroundSpans know nothing about the alignment, margins, or
// direction of the layout or line. XXX: Should they?
// They are evaluated at each line.
if (mSpannedText) {
if (mLineBackgroundSpans == null) {
mLineBackgroundSpans = new SpanSet(LineBackgroundSpan.class);
}
Spanned buffer = (Spanned) mText;
int textLength = buffer.length();
mLineBackgroundSpans.init(buffer, 0, textLength);
if (mLineBackgroundSpans.numberOfSpans > 0) {
int previousLineBottom = getLineTop(firstLine);
int previousLineEnd = getLineStart(firstLine);
ParagraphStyle[] spans = NO_PARA_SPANS;
int spansLength = 0;
TextPaint paint = mPaint;
int spanEnd = 0;
final int width = mWidth;
for (int i = firstLine; i <= lastLine; i++) {
int start = previousLineEnd;
int end = getLineStart(i + 1);
previousLineEnd = end;
int ltop = previousLineBottom;
int lbottom = getLineTop(i + 1);
previousLineBottom = lbottom;
int lbaseline = lbottom - getLineDescent(i);
if (start >= spanEnd) {
// These should be infrequent, so we'll use this so that
// we don't have to check as often.
spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
// All LineBackgroundSpans on a line contribute to its background.
spansLength = 0;
// Duplication of the logic of getParagraphSpans
if (start != end || start == 0) {
// Equivalent to a getSpans(start, end), but filling the 'spans' local
// array instead to reduce memory allocation
for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
// equal test is valid since both intervals are not empty by
// construction
if (mLineBackgroundSpans.spanStarts[j] >= end ||
mLineBackgroundSpans.spanEnds[j] <= start) continue;
spans = GrowingArrayUtils.append(
spans, spansLength, mLineBackgroundSpans.spans[j]);
spansLength++;
}
}
}
for (int n = 0; n < spansLength; n++) {
LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
ltop, lbaseline, lbottom,
buffer, start, end, i);
}
}
}
mLineBackgroundSpans.recycle();
}
// There can be a highlight even without spans if we are drawing
// a non-spanned transformation of a spanned editing buffer.
if (highlight != null) {
if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
canvas.drawPath(highlight, highlightPaint);
if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
}
}
从注释中可以看到,首先绘制LineBackgrounSpan。首先判断是否是Spannable类型,如果是,那么绘制LineBackgrounSpan。而具体的绘制工作又是LineBackgroundSpan的drawBackgroun方法,在TextView显示丰富多彩的文字(二)——如何使用ParagraphStyle格式化段落中,我们知道Android并没有提供LineBackgroundSpan的实现类,所以这段代码基本是不会做什么的。下面再判断是否有高亮,绘制高亮。总结一下,在现有的代码中,drawBackground基本是没有绘制任何东西的。下面是drawText方法的实现,drawText用于绘制文字:
/**
* @hide
*/
public void drawText(Canvas canvas, int firstLine, int lastLine) {
int previousLineBottom = getLineTop(firstLine);
int previousLineEnd = getLineStart(firstLine);
ParagraphStyle[] spans = NO_PARA_SPANS;
int spanEnd = 0;
TextPaint paint = mPaint;
CharSequence buf = mText;
Alignment paraAlign = mAlignment;
TabStops tabStops = null;
boolean tabStopsIsInitialized = false;
TextLine tl = TextLine.obtain();
// Draw the lines, one at a time.
// The baseline is the top of the following line minus the current line's descent.
for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
int start = previousLineEnd;
previousLineEnd = getLineStart(lineNum + 1);
int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
int ltop = previousLineBottom;
int lbottom = getLineTop(lineNum + 1);
previousLineBottom = lbottom;
int lbaseline = lbottom - getLineDescent(lineNum);
int dir = getParagraphDirection(lineNum);
int left = 0;
int right = mWidth;
if (mSpannedText) {
Spanned sp = (Spanned) buf;
int textLength = buf.length();
boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
// New batch of paragraph styles, collect into spans array.
// Compute the alignment, last alignment style wins.
// Reset tabStops, we'll rebuild if we encounter a line with
// tabs.
// We expect paragraph spans to be relatively infrequent, use
// spanEnd so that we can check less frequently. Since
// paragraph styles ought to apply to entire paragraphs, we can
// just collect the ones present at the start of the paragraph.
// If spanEnd is before the end of the paragraph, that's not
// our problem.
if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
spanEnd = sp.nextSpanTransition(start, textLength,
ParagraphStyle.class);
spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);
paraAlign = mAlignment;
for (int n = spans.length - 1; n >= 0; n--) {
if (spans[n] instanceof AlignmentSpan) {
paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
break;
}
}
tabStopsIsInitialized = false;
}
// Draw all leading margin spans. Adjust left or right according
// to the paragraph direction of the line.
final int length = spans.length;
boolean useFirstLineMargin = isFirstParaLine;
for (int n = 0; n < length; n++) {
if (spans[n] instanceof LeadingMarginSpan2) {
int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
// if there is more than one LeadingMarginSpan2, use
// the count that is greatest
if (lineNum < startLine + count) {
useFirstLineMargin = true;
break;
}
}
}
for (int n = 0; n < length; n++) {
if (spans[n] instanceof LeadingMarginSpan) {
LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
if (dir == DIR_RIGHT_TO_LEFT) {
margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
lbaseline, lbottom, buf,
start, end, isFirstParaLine, this);
right -= margin.getLeadingMargin(useFirstLineMargin);
} else {
margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
lbaseline, lbottom, buf,
start, end, isFirstParaLine, this);
left += margin.getLeadingMargin(useFirstLineMargin);
}
}
}
}
boolean hasTabOrEmoji = getLineContainsTab(lineNum);
// Can't tell if we have tabs for sure, currently
if (hasTabOrEmoji && !tabStopsIsInitialized) {
if (tabStops == null) {
tabStops = new TabStops(TAB_INCREMENT, spans);
} else {
tabStops.reset(TAB_INCREMENT, spans);
}
tabStopsIsInitialized = true;
}
// Determine whether the line aligns to normal, opposite, or center.
Alignment align = paraAlign;
if (align == Alignment.ALIGN_LEFT) {
align = (dir == DIR_LEFT_TO_RIGHT) ?
Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
} else if (align == Alignment.ALIGN_RIGHT) {
align = (dir == DIR_LEFT_TO_RIGHT) ?
Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
}
int x;
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
} else {
x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
}
} else {
int max = (int)getLineExtent(lineNum, tabStops, false);
if (align == Alignment.ALIGN_OPPOSITE) {
if (dir == DIR_LEFT_TO_RIGHT) {
x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
} else {
x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
}
} else { // Alignment.ALIGN_CENTER
max = max & ~1;
x = ((right + left - max) >> 1) +
getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
}
}
paint.setHyphenEdit(getHyphen(lineNum));
Directions directions = getLineDirections(lineNum);
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
// XXX: assumes there's nothing additional to be done
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
paint.setHyphenEdit(0);
}
TextLine.recycle(tl);
}
关键部分是for循环,一次绘制一行,内部如果是Spannable类型的文本,需要进行特别处理。具体的操作是首先获取所有ParagraphStyle对象,然后绘制。首先获取LeadingMarginSpan2对象,判断当前行是否具备第一行的属性,然后再根据是否是第一行,调用LeadingMarginSpan计算文本绘制的起始位置。不了解这两个对象的可以参考TextView显示丰富多彩的文字(三)——自定义CharacterStyle和ParagraphStyle显示效果。接下来判断有没有tab或者emoji。如果有,就会创建TabStops对象,接下来再决定这一行是靠右对齐还是居中对齐,还是靠左对齐。最后是绘制一行,又分为两种情况,如果方向是从左至右并且不是Spannable类型且没有tab或者emoji,那么直接调用canvas的drawText方法,如果不是上述情况,就会调用TextLine的draw方法。
总结一下:如果该行文本从左至右且没有特殊格式,那么就可以调用drawText绘制纯文本;如果该行由特殊格式,那么就会调用TextLine的draw方法。再使用TextLine的draw方法之前,需要先调用set方法将各种参数设置进入。下面看TextLine的draw方法:
/**
* Renders the TextLine.
*
* @param c the canvas to render on
* @param x the leading margin position
* @param top the top of the line
* @param y the baseline
* @param bottom the bottom of the line
*/
void draw(Canvas c, float x, int top, int y, int bottom) {
if (!mHasTabs) {
if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) {
drawRun(c, 0, mLen, false, x, top, y, bottom, false);
return;
}
if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) {
drawRun(c, 0, mLen, true, x, top, y, bottom, false);
return;
}
}
float h = 0;
int[] runs = mDirections.mDirections;
RectF emojiRect = null;
int lastRunIndex = runs.length - 2;
for (int i = 0; i < runs.length; i += 2) {
int runStart = runs[i];
int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK);
if (runLimit > mLen) {
runLimit = mLen;
}
boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0;
int segstart = runStart;
for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
int codept = 0;
Bitmap bm = null;
if (mHasTabs && j < runLimit) {
codept = mChars[j];
if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) {
codept = Character.codePointAt(mChars, j);
if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) {
bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept);
} else if (codept > 0xffff) {
++j;
continue;
}
}
}
if (j == runLimit || codept == '\t' || bm != null) {
h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom,
i != lastRunIndex || j != mLen);
if (codept == '\t') {
h = mDir * nextTab(h * mDir);
} else if (bm != null) {
float bmAscent = ascent(j);
float bitmapHeight = bm.getHeight();
float scale = -bmAscent / bitmapHeight;
float width = bm.getWidth() * scale;
if (emojiRect == null) {
emojiRect = new RectF();
}
emojiRect.set(x + h, y + bmAscent,
x + h + width, y);
c.drawBitmap(bm, null, emojiRect, mPaint);
h += width;
j++;
}
segstart = j + 1;
}
}
}
}
可以看到,如果没有tab或者emoji,就会调用drawRun方法。而如果有tab或者emoji,如果有emoji,那么会获取Bitmap,再绘制Bitmap,其中也会调用drawRun方法。所以如果绘制文本,那么都会调用drawRun方法,如果有图片,那么就会调用Canvas的drawBitmap方法。下面是drawRun方法,
/**
* Draws a unidirectional (but possibly multi-styled) run of text.
*
*
* @param c the canvas to draw on
* @param start the line-relative start
* @param limit the line-relative limit
* @param runIsRtl true if the run is right-to-left
* @param x the position of the run that is closest to the leading margin
* @param top the top of the line
* @param y the baseline
* @param bottom the bottom of the line
* @param needWidth true if the width value is required.
* @return the signed width of the run, based on the paragraph direction.
* Only valid if needWidth is true.
*/
private float drawRun(Canvas c, int start,
int limit, boolean runIsRtl, float x, int top, int y, int bottom,
boolean needWidth) {
if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
float w = -measureRun(start, limit, limit, runIsRtl, null);
handleRun(start, limit, limit, runIsRtl, c, x + w, top,
y, bottom, null, false);
return w;
}
return handleRun(start, limit, limit, runIsRtl, c, x, top,
y, bottom, null, needWidth);
}
可以看到上面的方法会根据方向调用handleRun方法,下面是handleRun方法:
= & <= tests
if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
(mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
if (span instanceof ReplacementSpan) {
replacement = (ReplacementSpan)span;
} else {
// We might have a replacement that uses the draw
// state, otherwise measure state would suffice.
span.updateDrawState(wp);
}
}
if (replacement != null) {
x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
bottom, fmi, needWidth || mlimit < measureLimit);
continue;
}
for (int j = i, jnext; j < mlimit; j = jnext) {
jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) -
mStart;
wp.set(mPaint);
for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
// Intentionally using >= and <= as explained above
if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) ||
(mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
CharacterStyle span = mCharacterStyleSpanSet.spans[k];
span.updateDrawState(wp);
}
// Only draw hyphen on last run in line
if (jnext < mLen) {
wp.setHyphenEdit(0);
}
x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
top, y, bottom, fmi, needWidth || jnext < measureLimit);
}
}
return x - originalX;
}" data-snippet-id="ext.243646ebd8d3c454518a70e3769518ee" data-snippet-saved="false" data-codota-status="done">/**
* Utility function for handling a unidirectional run. The run must not
* contain tabs or emoji but can contain styles.
*
*
* @param start the line-relative start of the run
* @param measureLimit the offset to measure to, between start and limit inclusive
* @param limit the limit of the run
* @param runIsRtl true if the run is right-to-left
* @param c the canvas, can be null
* @param x the end of the run closest to the leading margin
* @param top the top of the line
* @param y the baseline
* @param bottom the bottom of the line
* @param fmi receives metrics information, can be null
* @param needWidth true if the width is required
* @return the signed width of the run based on the run direction; only
* valid if needWidth is true
*/
private float handleRun(int start, int measureLimit,
int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
int bottom, FontMetricsInt fmi, boolean needWidth) {
// Case of an empty line, make sure we update fmi according to mPaint
if (start == measureLimit) {
TextPaint wp = mWorkPaint;
wp.set(mPaint);
if (fmi != null) {
expandMetricsFromPaint(fmi, wp);
}
return 0f;
}
if (mSpanned == null) {
TextPaint wp = mWorkPaint;
wp.set(mPaint);
final int mlimit = measureLimit;
return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,
y, bottom, fmi, needWidth || mlimit < measureLimit);
}
mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
// Shaping needs to take into account context up to metric boundaries,
// but rendering needs to take into account character style boundaries.
// So we iterate through metric runs to get metric bounds,
// then within each metric run iterate through character style runs
// for the run bounds.
final float originalX = x;
for (int i = start, inext; i < measureLimit; i = inext) {
TextPaint wp = mWorkPaint;
wp.set(mPaint);
inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
mStart;
int mlimit = Math.min(inext, measureLimit);
ReplacementSpan replacement = null;
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)) continue;
MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
if (span instanceof ReplacementSpan) {
replacement = (ReplacementSpan)span;
} else {
// We might have a replacement that uses the draw
// state, otherwise measure state would suffice.
span.updateDrawState(wp);
}
}
if (replacement != null) {
x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
bottom, fmi, needWidth || mlimit < measureLimit);
continue;
}
for (int j = i, jnext; j < mlimit; j = jnext) {
jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + mlimit) -
mStart;
wp.set(mPaint);
for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
// Intentionally using >= and <= as explained above
if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + jnext) ||
(mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
CharacterStyle span = mCharacterStyleSpanSet.spans[k];
span.updateDrawState(wp);
}
// Only draw hyphen on last run in line
if (jnext < mLen) {
wp.setHyphenEdit(0);
}
x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
top, y, bottom, fmi, needWidth || jnext < measureLimit);
}
}
return x - originalX;
}
从注释中可以看到该方法用于绘制单方向的文字,并且文字中不能包含制表符或者图片,但是可以包含其他Span。首先看一下TextLine的set方法:
/**
* Initializes a TextLine and prepares it for use.
*
* @param paint the base paint for the line
* @param text the text, can be Styled
* @param start the start of the line relative to the text
* @param limit the limit of the line relative to the text
* @param dir the paragraph direction of this line
* @param directions the directions information of this line
* @param hasTabs true if the line might contain tabs or emoji
* @param tabStops the tabStops. Can be null.
*/
void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
Directions directions, boolean hasTabs, TabStops tabStops) {
mPaint = paint;
mText = text;
mStart = start;
mLen = limit - start;
mDir = dir;
mDirections = directions;
if (mDirections == null) {
throw new IllegalArgumentException("Directions cannot be null");
}
mHasTabs = hasTabs;
mSpanned = null;
boolean hasReplacement = false;
if (text instanceof Spanned) {
mSpanned = (Spanned) text;
mReplacementSpanSpanSet.init(mSpanned, start, limit);
hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
}
mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
if (mCharsValid) {
if (mChars == null || mChars.length < mLen) {
mChars = ArrayUtils.newUnpaddedCharArray(mLen);
}
TextUtils.getChars(text, start, limit, mChars, 0);
if (hasReplacement) {
// Handle these all at once so we don't have to do it as we go.
// Replace the first character of each replacement run with the
// object-replacement character and the remainder with zero width
// non-break space aka BOM. Cursor movement code skips these
// zero-width characters.
char[] chars = mChars;
for (int i = start, inext; i < limit; i = inext) {
inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)) {
// transition into a span
chars[i - start] = '\ufffc';
for (int j = i - start + 1, e = inext - start; j < e; ++j) {
chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
}
}
}
}
}
mTabs = tabStops;
}
可以看到该方法根据传入的参数做了一些初始化工作,包含判断文字是否是Spanned对象,如果是的话,还判断其中有没有ReplacementSpan。然后再根据当前是否属于特殊情况(有ReplacementSpan或者有支付表或者方向不是从左至右),那么需要需要获取文字的mChars数组类型,如果有ReplacementSpan,还需要mChars进行转换。
下面再看之前的handleRun方法,可以看到如果文字不是Spanned类型的话,那么会调用handleText方法;如果是Spanned的话,那么首先获取文本中所有的MetricAffectingSpan和CharacterStyle对象,在TextView显示丰富多彩的文字(二)——如何使用ParagraphStyle格式化段落中我们知道MetricAffectingSpan会影响文字的外观,接下来会调用每个MetricAffectingSpan的updateDrawState方法
设置TextPaint对象,并且由于set方法中已经替换了Replacement的字符并且在handleRun方法中已经将Replacement的图片绘制出来了,所以这儿就忽略了ReplacementSpan。接下来,就是如果ReplacementSpan不为null,那么需要把位置预留出来。接下来再对所有CharacterStyle调用updateDrawState设置TextPaint。然后调用handleText方法。handleText方法读者测量和渲染文本,文本必须不包含制表符和emoji。handleText方法如下:
/**
* Utility function for measuring and rendering text. The text must
* not include a tab or emoji.
*
* @param wp the working paint
* @param start the start of the text
* @param end the end of the text
* @param runIsRtl true if the run is right-to-left
* @param c the canvas, can be null if rendering is not needed
* @param x the edge of the run closest to the leading margin
* @param top the top of the line
* @param y the baseline
* @param bottom the bottom of the line
* @param fmi receives metrics information, can be null
* @param needWidth true if the width of the run is needed
* @return the signed width of the run based on the run direction; only
* valid if needWidth is true
*/
private float handleText(TextPaint wp, int start, int end,
int contextStart, int contextEnd, boolean runIsRtl,
Canvas c, float x, int top, int y, int bottom,
FontMetricsInt fmi, boolean needWidth) {
// Get metrics first (even for empty strings or "0" width runs)
if (fmi != null) {
expandMetricsFromPaint(fmi, wp);
}
int runLen = end - start;
// No need to do anything if the run width is "0"
if (runLen == 0) {
return 0f;
}
float ret = 0;
if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor != 0 || runIsRtl))) {
if (mCharsValid) {
ret = wp.getRunAdvance(mChars, start, end, contextStart, contextEnd,
runIsRtl, end);
} else {
int delta = mStart;
ret = wp.getRunAdvance(mText, delta + start, delta + end,
delta + contextStart, delta + contextEnd, runIsRtl, delta + end);
}
}
if (c != null) {
if (runIsRtl) {
x -= ret;
}
if (wp.bgColor != 0) {
int previousColor = wp.getColor();
Paint.Style previousStyle = wp.getStyle();
wp.setColor(wp.bgColor);
wp.setStyle(Paint.Style.FILL);
c.drawRect(x, top, x + ret, bottom, wp);
wp.setStyle(previousStyle);
wp.setColor(previousColor);
}
if (wp.underlineColor != 0) {
// kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
int previousColor = wp.getColor();
Paint.Style previousStyle = wp.getStyle();
boolean previousAntiAlias = wp.isAntiAlias();
wp.setStyle(Paint.Style.FILL);
wp.setAntiAlias(true);
wp.setColor(wp.underlineColor);
c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);
wp.setStyle(previousStyle);
wp.setColor(previousColor);
wp.setAntiAlias(previousAntiAlias);
}
drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
x, y + wp.baselineShift);
}
return runIsRtl ? -ret : ret;
}
如果Canvas不为空,那么首先画背景色,再画下划线,最后调用drawTextRun方法。drawTextRun方法用已经设置号的TextPaint渲染文本.下面是drawTextRun方法的实现:
/**
* Render a text run with the set-up paint.
*
* @param c the canvas
* @param wp the paint used to render the text
* @param start the start of the run
* @param end the end of the run
* @param contextStart the start of context for the run
* @param contextEnd the end of the context for the run
* @param runIsRtl true if the run is right-to-left
* @param x the x position of the left edge of the run
* @param y the baseline of the run
*/
private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
if (mCharsValid) {
int count = end - start;
int contextCount = contextEnd - contextStart;
c.drawTextRun(mChars, start, count, contextStart, contextCount,
x, y, runIsRtl, wp);
} else {
int delta = mStart;
c.drawTextRun(mText, delta + start, delta + end,
delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
}
}
从上述方法可以看到最终调用的是Canvas的drawTextRun方法,
/**
* Draw a run of text, all in a single direction, with optional context for complex text
* shaping.
*
* See {@link #drawTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint)}
* for more details. This method uses a character array rather than CharSequence to
* represent the string. Also, to be consistent with the pattern established in
* {@link #drawText}, in this method {@code count} and {@code contextCount} are used rather
* than offsets of the end position; {@code count = end - start, contextCount = contextEnd -
* contextStart}.
*
* @param text the text to render
* @param index the start of the text to render
* @param count the count of chars to render
* @param contextIndex the start of the context for shaping. Must be
* no greater than index.
* @param contextCount the number of characters in the context for shaping.
* contexIndex + contextCount must be no less than index + count.
* @param x the x position at which to draw the text
* @param y the y position at which to draw the text
* @param isRtl whether the run is in RTL direction
* @param paint the paint
*/
public void drawTextRun(@NonNull char[] text, int index, int count, int contextIndex,
int contextCount, float x, float y, boolean isRtl, @NonNull Paint paint) {
if (text == null) {
throw new NullPointerException("text is null");
}
if (paint == null) {
throw new NullPointerException("paint is null");
}
if ((index | count | contextIndex | contextCount | index - contextIndex
| (contextIndex + contextCount) - (index + count)
| text.length - (contextIndex + contextCount)) < 0) {
throw new IndexOutOfBoundsException();
}
native_drawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount,
x, y, isRtl, paint.getNativeInstance(), paint.mNativeTypeface);
}
先经过一系列的判定,最后调用native_drawTextRun方法,该方法是native的。上面还有一个handleReplacement方法,用于测量和渲染一个replacement。下面是handleReplacement方法的实现:
/**
* Utility function for measuring and rendering a replacement.
*
*
* @param replacement the replacement
* @param wp the work paint
* @param start the start of the run
* @param limit the limit of the run
* @param runIsRtl true if the run is right-to-left
* @param c the canvas, can be null if not rendering
* @param x the edge of the replacement closest to the leading margin
* @param top the top of the line
* @param y the baseline
* @param bottom the bottom of the line
* @param fmi receives metrics information, can be null
* @param needWidth true if the width of the replacement is needed
* @return the signed width of the run based on the run direction; only
* valid if needWidth is true
*/
private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
int start, int limit, boolean runIsRtl, Canvas c,
float x, int top, int y, int bottom, FontMetricsInt fmi,
boolean needWidth) {
float ret = 0;
int textStart = mStart + start;
int textLimit = mStart + limit;
if (needWidth || (c != null && runIsRtl)) {
int previousTop = 0;
int previousAscent = 0;
int previousDescent = 0;
int previousBottom = 0;
int previousLeading = 0;
boolean needUpdateMetrics = (fmi != null);
if (needUpdateMetrics) {
previousTop = fmi.top;
previousAscent = fmi.ascent;
previousDescent = fmi.descent;
previousBottom = fmi.bottom;
previousLeading = fmi.leading;
}
ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
if (needUpdateMetrics) {
updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
previousLeading);
}
}
if (c != null) {
if (runIsRtl) {
x -= ret;
}
replacement.draw(c, mText, textStart, textLimit,
x, top, y, bottom, wp);
}
return runIsRtl ? -ret : ret;
}
该方法调用replcement的draw方法进行绘画,并返回宽度。按照Android目前的类库,ReplacementSpan目前只有一个具体的实现类,就是ImageSpan,ImageSpan的draw方法就是绘画设置的图片。
从上可以看到,当TextView设置的文本样式是Spannable类型时,会使用DynamicLayout负责文本的布局,而DynamicLayout会首先绘制背景,绘制背景过程中会调用设置的LineBackgrounSpan绘制背景;接着绘制文本,绘制文本中会首先获取所有的ParagraphStyle,重点就是LeadingMarginSpan,然后逐个绘制Margin,急着就是根据是否是纯文本,如果是纯文本,就直接使用Canvas的drawText方法,如果不是的话,就需要使用TextLine的draw方法。由此可以看出来,Layout绘制文本会将ParagraphStyle绘制,除了TabStopSpan。TextLine负责绘制每一行,会首先获取所有的CharacterStyle对象,再设置TextPaint属性,最后再调用canvas的native的draw方法。
下面看DynamicLayout的构造方法,DynamicLayout有几个构造方法,但最终都会调用下面这一个:
/**
* Make a layout for the transformed text (password transformation
* being the primary example of a transformation)
* that will be updated as the base text is changed.
* If ellipsize is non-null, the Layout will ellipsize the text
* down to ellipsizedWidth.
* *
* *@hide
*/
public DynamicLayout(CharSequence base, CharSequence display,
TextPaint paint,
int width, Alignment align, TextDirectionHeuristic textDir,
float spacingmult, float spacingadd,
boolean includepad, int breakStrategy, int hyphenationFrequency,
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
super((ellipsize == null)
? display
: (display instanceof Spanned)
? new SpannedEllipsizer(display)
: new Ellipsizer(display),
paint, width, align, textDir, spacingmult, spacingadd);
mBase = base;
mDisplay = display;
if (ellipsize != null) {
mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
mEllipsizedWidth = ellipsizedWidth;
mEllipsizeAt = ellipsize;
} else {
mInts = new PackedIntVector(COLUMNS_NORMAL);
mEllipsizedWidth = width;
mEllipsizeAt = null;
}
mObjects = new PackedObjectVector(1);
mIncludePad = includepad;
mBreakStrategy = breakStrategy;
mHyphenationFrequency = hyphenationFrequency;
/*
* This is annoying, but we can't refer to the layout until
* superclass construction is finished, and the superclass
* constructor wants the reference to the display text.
*
* This will break if the superclass constructor ever actually
* cares about the content instead of just holding the reference.
*/
if (ellipsize != null) {
Ellipsizer e = (Ellipsizer) getText();
e.mLayout = this;
e.mWidth = ellipsizedWidth;
e.mMethod = ellipsize;
mEllipsize = true;
}
// Initial state is a single line with 0 characters (0 to 0),
// with top at 0 and bottom at whatever is natural, and
// undefined ellipsis.
int[] start;
if (ellipsize != null) {
start = new int[COLUMNS_ELLIPSIZE];
start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
} else {
start = new int[COLUMNS_NORMAL];
}
Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int asc = fm.ascent;
int desc = fm.descent;
start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
start[TOP] = 0;
start[DESCENT] = desc;
mInts.insertAt(0, start);
start[TOP] = desc - asc;
mInts.insertAt(1, start);
mObjects.insertAt(0, dirs);
// Update from 0 characters to whatever the real text is
reflow(base, 0, 0, base.length());
if (base instanceof Spannable) {
if (mWatcher == null)
mWatcher = new ChangeWatcher(this);
// Strip out any watchers for other DynamicLayouts.
Spannable sp = (Spannable) base;
ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
for (int i = 0; i < spans.length; i++)
sp.removeSpan(spans[i]);
sp.setSpan(mWatcher, 0, base.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE |
(PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
}
}
从上面的代码可以看出,首先调用了父类Layout的构造方法,接着就是自己的处理。构造方法内部会调用reflow方法更新。下面是reflow方法
private void reflow(CharSequence s, int where, int before, int after) {
if (s != mBase)
return;
CharSequence text = mDisplay;
int len = text.length();
// seek back to the start of the paragraph
int find = TextUtils.lastIndexOf(text, '\n', where - 1);
if (find < 0)
find = 0;
else
find = find + 1;
{
int diff = where - find;
before += diff;
after += diff;
where -= diff;
}
// seek forward to the end of the paragraph
int look = TextUtils.indexOf(text, '\n', where + after);
if (look < 0)
look = len;
else
look++; // we want the index after the \n
int change = look - (where + after);
before += change;
after += change;
// seek further out to cover anything that is forced to wrap together
if (text instanceof Spanned) {
Spanned sp = (Spanned) text;
boolean again;
do {
again = false;
Object[] force = sp.getSpans(where, where + after,
WrapTogetherSpan.class);
for (int i = 0; i < force.length; i++) {
int st = sp.getSpanStart(force[i]);
int en = sp.getSpanEnd(force[i]);
if (st < where) {
again = true;
int diff = where - st;
before += diff;
after += diff;
where -= diff;
}
if (en > where + after) {
again = true;
int diff = en - (where + after);
before += diff;
after += diff;
}
}
} while (again);
}
// find affected region of old layout
int startline = getLineForOffset(where);
int startv = getLineTop(startline);
int endline = getLineForOffset(where + before);
if (where + after == len)
endline = getLineCount();
int endv = getLineTop(endline);
boolean islast = (endline == getLineCount());
// generate new layout for affected text
StaticLayout reflowed;
StaticLayout.Builder b;
synchronized (sLock) {
reflowed = sStaticLayout;
b = sBuilder;
sStaticLayout = null;
sBuilder = null;
}
if (reflowed == null) {
reflowed = new StaticLayout(null);
b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
}
b.setText(text, where, where + after)
.setPaint(getPaint())
.setWidth(getWidth())
.setTextDirection(getTextDirectionHeuristic())
.setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
.setEllipsizedWidth(mEllipsizedWidth)
.setEllipsize(mEllipsizeAt)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
reflowed.generate(b, false, true);
int n = reflowed.getLineCount();
// If the new layout has a blank line at the end, but it is not
// the very end of the buffer, then we already have a line that
// starts there, so disregard the blank line.
if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
n--;
// remove affected lines from old layout
mInts.deleteAt(startline, endline - startline);
mObjects.deleteAt(startline, endline - startline);
// adjust offsets in layout for new height and offsets
int ht = reflowed.getLineTop(n);
int toppad = 0, botpad = 0;
if (mIncludePad && startline == 0) {
toppad = reflowed.getTopPadding();
mTopPadding = toppad;
ht -= toppad;
}
if (mIncludePad && islast) {
botpad = reflowed.getBottomPadding();
mBottomPadding = botpad;
ht += botpad;
}
mInts.adjustValuesBelow(startline, START, after - before);
mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
// insert new layout
int[] ints;
if (mEllipsize) {
ints = new int[COLUMNS_ELLIPSIZE];
ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
} else {
ints = new int[COLUMNS_NORMAL];
}
Directions[] objects = new Directions[1];
for (int i = 0; i < n; i++) {
ints[START] = reflowed.getLineStart(i) |
(reflowed.getParagraphDirection(i) << DIR_SHIFT) |
(reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
int top = reflowed.getLineTop(i) + startv;
if (i > 0)
top -= toppad;
ints[TOP] = top;
int desc = reflowed.getLineDescent(i);
if (i == n - 1)
desc += botpad;
ints[DESCENT] = desc;
objects[0] = reflowed.getLineDirections(i);
ints[HYPHEN] = reflowed.getHyphen(i);
if (mEllipsize) {
ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
}
mInts.insertAt(startline + i, ints);
mObjects.insertAt(startline + i, objects);
}
updateBlocks(startline, endline - 1, n);
b.finish();
synchronized (sLock) {
sStaticLayout = reflowed;
sBuilder = b;
}
}
reflow方法会首先将前去查找段落的开始,然后再向后查找段落的结尾。然后如果文本是Spanned类型的,则查找所有WrapTogetherSpan类型,然后再找出影响布局的部分,再根据影响的文本创建新的布局,可以看到接下来是使用StaticLayout的generate方法,StaticLayout布局的generate方法中有下面一段:
// tab stop locations
int[] variableTabStops = null;
if (spanned != null) {
TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
paraEnd, TabStopSpan.class);
if (spans.length > 0) {
int[] stops = new int[spans.length];
for (int i = 0; i < spans.length; i++) {
stops[i] = spans[i].getTabStop();
}
Arrays.sort(stops, 0, stops.length);
variableTabStops = stops;
}
}
在TextView显示丰富多彩的文字(一)——如何使用CharacterStyle格式化字符中留有一个问题,就是对同一位置的文字设置两个URLSpan,点击事件响应的是第一个设置的URLSpan的URL,这与我们平常想的覆盖有所不同,而具体的解释可以看onTouchEvent方法中的这段代码:
if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
&& mText instanceof Spannable && mLayout != null) {
boolean handled = false;
if (mMovement != null) {
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
}
final boolean textIsSelectable = isTextSelectable();
if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
// The LinkMovementMethod which should handle taps on links has not been installed
// on non editable text that support text selection.
// We reproduce its behavior here to open links for these.
ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
getSelectionEnd(), ClickableSpan.class);
if (links.length > 0) {
links[0].onClick(this);
handled = true;
}
}
if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
// Show the IME, except when selecting in read-only text.
final InputMethodManager imm = InputMethodManager.peekInstance();
viewClicked(imm);
if (!textIsSelectable && mEditor.mShowSoftInputOnFocus) {
handled |= imm != null && imm.showSoftInput(this, 0);
}
// The above condition ensures that the mEditor is not null
mEditor.onTouchUpEvent(event);
handled = true;
}
if (handled) {
return true;
}
}
上述代码会获取所有的ClickableSpan对象,然后会调用第一个ClickableSpan对象的click方法,这也就解释了如果设置多个URLSpan的话,为什么点击事件会显示第一个设置的URL。
上面基本就是从TextView的源码解释为什么可以显示丰富多彩的文字,第一次分析源码,有很多看的不太懂,可能会有错误,欢迎讨论。