本文为转载&地址: https://github.com/itgoyo/AndroidSource-Analysis

简介: Android源码分析,让你更清楚的理解每一个组件的功能与用法。

TextView源码解析

1.简介

TextView作为Android系统上显示和排版文字以及提供对文字的增删改查、图文混排等功能的控件,内部是相对比较复杂的。这么一个复杂的控件自然需要依赖于一些其他的辅助类,例如:Layout以及Layout的相关子类、Span相关的类、MovementMethod接口、TransformationMethod接口等。这篇文章主要介绍TextView的结构和内部处理文字的流程以及TextView相关的辅助类在TextView处理文字过程中的作用。

2.TextView的内部结构和辅助类

TextView内部除了继承自View的相关属性和measure、layout、draw步骤,还包括:

  • Layout: TextView的文字排版、折行策略以及文本绘制都是在Layout里面完成的,TextView的自身测量也受Layout的影响。Layout是TextView执行setText方法后,由TextView内部创建的实例,并不能由外部提供。可以用getLayout()方法获取。
  • TransformationMethod: 用来处理最终的显示结果的类,例如显示密码的时候把密码转换成圆点。这个类并不直接影响TextView内部储存的Text,只影响显示的结果。
  • MovementMethod: 用来处理TextView内部事件响应的类,可以针对TextView内文本的某一个区域做软键盘输入或者触摸事件的响应。
  • Drawables: TextView的静态内部类,用来处理和储存TextView的CompoundDrawables,包括TextView的上下左右的Drawable以及错误提示的Drawable。
  • Spans: Spans并不是特定的某一个类或者实现了某一个接口的类。它可以是任意类型,Spans实际上做的事情是在TextView的内部的text的某一个区域做标记。其中有部分Spans可以影响TextView的绘制和测量,如ImageSpan、BackgroundColorSpan、AbsoluteSizeSpan。还有可以响应点击事件的ClickableSpan。
  • Editor: TextView作为可编辑文本控件的时候(EditText),使用Editor来处理文本的区域选择处理和判断、拼写检查、弹出文本菜单等。
  • InputConnection: EditText的文本输入部分是在TextView中完成的。而InputConnection是软键盘和TextView之间的桥梁,所有的软键盘的输入文字、修改文字和删除文字都是通过InputConnection传递给TextView的。

3.TextView的onTouchEvent处理

TextView内部能处理触摸事件的,包括自身的触摸处理、Editor的onTouchEvent、MovementMethod的onTouchEvent。Editor的onTouchEvent主要处理出于编辑状态下的触摸事件,比如点击选中、长按等。MovementMethod则主要负责文本内部有Span的时候的相关处理,比较常见的就是LinkMovementMethod处理ClickableSpan的点击事件。我们来看一下TextView内部对这些触摸事件的处理和优先级的分配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
//当Editor不为空的时候,给Editor的双击事件预设值
if (mEditor != null && action == MotionEvent.ACTION_DOWN) {
if (mFirstTouch && (SystemClock.uptimeMillis() - mLastTouchUpTime) <=
ViewConfiguration.getDoubleTapTimeout()) {
mEditor.mDoubleTap = true;
mFirstTouch = false;
} else {
mEditor.mDoubleTap = false;
mFirstTouch = true;
}
}
if (action == MotionEvent.ACTION_UP) {
mLastTouchUpTime = SystemClock.uptimeMillis();
}
//当Editor不为空,优先处理Editor的触摸事件
if (mEditor != null) {
mEditor.onTouchEvent(event);
//由于Editor内部onTouchEvent实际上交给了mSelectionModifierCursorController处理,所以这边判断mSelectionModifierCursorController是否需要处理接下来的一系列事件,如果是则直接返回跳过下面的步骤
if (mEditor.mSelectionModifierCursorController != null &&
mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
return true;
}
}
final boolean superResult = super.onTouchEvent(event);
//处理API 23新加入的InsertionActinoMode
if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
mEditor.mDiscardNextActionUp = false;
if (mEditor.mIsInsertionActionModeStartPending) {
mEditor.startInsertionActionMode();
mEditor.mIsInsertionActionModeStartPending = false;
}
return superResult;
}
final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) &&
(mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
&& mText instanceof Spannable && mLayout != null) {
boolean handled = false;
//MovementMethod的触摸时间处理,如果MovementMethod类型是LinkMovementMethod则会处理文本内的所有ClickableSpan的点击
if (mMovement != null) {
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
}
final boolean textIsSelectable = isTextSelectable();
if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
//在文本可选择的情况下,默认是没有LinkMovementMethod来处理ClickableSpan相关的点击的,所以在文本可选择情况,TextView对所有的ClickableSpan进行统一处理
ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
getSelectionEnd(), ClickableSpan.class);
if (links.length > 0) {
links[0].onClick(this);
handled = true;
}
}
if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
final InputMethodManager imm = InputMethodManager.peekInstance();
viewClicked(imm);
if (!textIsSelectable && mEditor.mShowSoftInputOnFocus) {
handled |= imm != null && imm.showSoftInput(this, 0);
}
mEditor.onTouchUpEvent(event);
handled = true;
}
if (handled) {
return true;
}
}
return superResult;
}

4.TextView的创建Layout的过程

TextView内部并不仅仅只有一个用来显示文本内容的Layout,在设置了hint的时候,还需要有一个mHintLayout来处理hint的内容。如果设置了Ellipsize类型为Marquee时,还会有一个mSavedMarqueeModeLayout专门用来显示marquee效果。这些Layout都是通过内部的makeNewLayout方法来创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
protected void makeNewLayout(int wantWidth, int hintWidth
BoringLayout.Metrics boring,
BoringLayout.Metrics hintBoring,
int ellipsisWidth, boolean bringIntoView) {
//如果当前有marquee动画,则先停止动画
stopMarquee();
mOldMaximum = mMaximum;
mOldMaxMode = mMaxMode;
mHighlightPathBogus = true;
if (wantWidth < 0) {
wantWidth = 0;
}
if (hintWidth < 0) {
hintWidth = 0;
}
//文本对齐方式
Layout.Alignment alignment = getLayoutAlignment();
final boolean testDirChange = mSingleLine && mLayout != null &&
(alignment == Layout.Alignment.ALIGN_NORMAL ||
alignment == Layout.Alignment.ALIGN_OPPOSITE);
int oldDir = 0;
if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
//检测是否设置了ellipsize
boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
TruncateAt effectiveEllipsize = mEllipsize;
if (mEllipsize == TruncateAt.MARQUEE &&
mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
effectiveEllipsize = TruncateAt.END_SMALL;
}
//文本方向
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}
//创建主Layout
mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
effectiveEllipsize, effectiveEllipsize == mEllipsize);
//非常规的Marquee模式下,需要创建mSavedMarqueeModeLayout来保存marquee动画时所用的Layout,并且在动画期间把它和TextView的主Layout对换
if (switchEllipsize) {
TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
TruncateAt.END : TruncateAt.MARQUEE;
mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
}
shouldEllipsize = mEllipsize != null;
mHintLayout = null;
//判断是否需要创建hintLayout
if (mHint != null) {
if (shouldEllipsize) hintWidth = wantWidth;
if (hintBoring == UNKNOWN_BORING) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}
//判断是否为boring,如果是则创建BoringLayout
if (hintBoring != null) {
if (hintBoring.width <= hintWidth &&
(!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
if (mSavedHintLayout != null) {
mHintLayout = mSavedHintLayout.
replaceOrMake(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad);
} else {
mHintLayout = BoringLayout.make(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad);
}
mSavedHintLayout = (BoringLayout) mHintLayout;
} else if (shouldEllipsize && hintBoring.width <= hintWidth) {
if (mSavedHintLayout != null) {
mHintLayout = mSavedHintLayout.
replaceOrMake(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad, mEllipsize,
ellipsisWidth);
} else {
mHintLayout = BoringLayout.make(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad, mEllipsize,
ellipsisWidth);
}
}
}
//不是boring的状态下,用StaticLayout来创建
if (mHintLayout == null) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
mHint.length(), mTextPaint, hintWidth)
.setAlignment(alignment)
.setTextDirection(mTextDir)
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
if (shouldEllipsize) {
builder.setEllipsize(mEllipsize)
.setEllipsizedWidth(ellipsisWidth)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
}
mHintLayout = builder.build();
}
}
if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
registerForPreDraw();
}
//判断是否需要开始Marquee动画
if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
if (!compressText(ellipsisWidth)) {
final int height = mLayoutParams.height;
if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
startMarquee();
} else {
mRestartMarquee = true;
}
}
}
if (mEditor != null) mEditor.prepareCursorControllers();
}

TextView的布局创建过程涉及到一个boring的概念,boring是指布局所用的文本里面不包含任何Span,所有的文本方向都是从左到右的布局,并且仅需一行就能显示完全的布局。这种情况下,TextView会使用BoringLayout类来创建相关的布局,以节省不必要的文本测量以及文本折行、Span宽度、文本方向等的计算。下面我们来看一下makeNewLayout中使用频率比较高的makeSingleLayout的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
boolean useSaved) {
Layout result = null;
//判断是否Spannable,如果是则用DynamicLayout类来创建布局,DynamicLayout内部实际也是使用StaticLayout来做文本的测量绘制,并在StaticLayout的基础上增加了文本或者Span改变时的监听,及时对文本或者Span的变化做出反应。
if (mText instanceof Spannable) {
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
mBreakStrategy, mHyphenationFrequency,
getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
} else {
//如果boring是未知状态,则重新判断一次是否boring
if (boring == UNKNOWN_BORING) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
}
//根据boring的属性来创建对应的布局,如果有mSavedLayout则从mSavedLayout创建
if (boring != null) {
if (boring.width <= wantWidth &&
(effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
if (useSaved && mSavedLayout != null) {
//从之前保存的Layout中创建
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
} else {
//创建新的Layout
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);
}
}
}
}
//如果没有创建BoringLayout, 则使用StaticLayout类来创建布局
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);
}
result = builder.build();
}
return result;
}

5.TextView的文字处理和绘制

TextView主要的文字排版和渲染并不是在TextView里面完成的,而是由Layout类来处理文字排版工作。在单纯地使用TextView来展示静态文本的时候,这件事情则是由Layout的子类StaticLayout来完成的。

StaticLayout接收到字符串后,首先做的事情是根据字符串里面的换行符对字符串进行拆分。

1
2
3
4
5
6
for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
if (paraEnd < 0)
paraEnd = bufEnd;
else
paraEnd++;

拆分后的段落(Paragraph)被分配给辅助类MeasuredText进行测量得到每个字符的宽度以及每个段落的FontMetric。并通过LineBreaker进行折行的判断

1
2
3
4
5
6
7
//把段落载入到MeasuredText中,并分配对应的缓存空间
measured.setPara(source, paraStart, paraEnd, textDir, b);
char[] chs = measured.mChars;
float[] widths = measured.mWidths;
byte[] chdirs = measured.mLevels;
int dir = measured.mDir;
boolean easy = measured.mEasy;
//把相关属性传给JNI层的LineBreaker
nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
        firstWidth, firstWidthLineCount, restWidth,
        variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int fmCacheCount = 0;
int spanEndCacheCount = 0;
for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
if (fmCacheCount * 4 >= fmCache.length) {
int[] grow = new int[fmCacheCount * 4 * 2];
System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
fmCache = grow;
}
if (spanEndCacheCount >= spanEndCache.length) {
int[] grow = new int[spanEndCacheCount * 2];
System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
spanEndCache = grow;
}
if (spanned == null) {
spanEnd = paraEnd;
int spanLen = spanEnd - spanStart;
//段落没有Span的情况下,把整个段落交给MeasuredText计算每个字符的宽度和FontMetric
measured.addStyleRun(paint, spanLen, fm);
} else {
spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
MetricAffectingSpan.class);
int spanLen = spanEnd - spanStart;
MetricAffectingSpan[] spans =
spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
//把对排版有影响的Span交给MeasuredText测量宽度并计算FontMetric
measured.addStyleRun(paint, spans, spanLen, fm);
}
//把测量后的FontMetric缓存下来方便后面使用
fmCache[fmCacheCount * 4 + 0] = fm.top;
fmCache[fmCacheCount * 4 + 1] = fm.bottom;
fmCache[fmCacheCount * 4 + 2] = fm.ascent;
fmCache[fmCacheCount * 4 + 3] = fm.descent;
fmCacheCount++;
spanEndCache[spanEndCacheCount] = spanEnd;
spanEndCacheCount++;
}
nGetWidths(b.mNativePtr, widths);
//计算段落中需要折行的位置,并返回折行的数量
int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length);

计算完每一行的测量相关信息、Span宽高以及折行位置,就可以开始按照最终的行数一行一行地保存下来,以供后面绘制和获取对应文本信息的时候使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
spanEnd = spanEndCache[spanEndCacheIndex++];
// 获取之前缓存的FontMetric信息
fm.top = fmCache[fmCacheIndex * 4 + 0];
fm.bottom = fmCache[fmCacheIndex * 4 + 1];
fm.ascent = fmCache[fmCacheIndex * 4 + 2];
fm.descent = fmCache[fmCacheIndex * 4 + 3];
fmCacheIndex++;
if (fm.top < fmTop) {
fmTop = fm.top;
}
if (fm.ascent < fmAscent) {
fmAscent = fm.ascent;
}
if (fm.descent > fmDescent) {
fmDescent = fm.descent;
}
if (fm.bottom > fmBottom) {
fmBottom = fm.bottom;
}
while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
breakIndex++;
}
while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
int endPos = paraStart + breaks[breakIndex];
boolean moreChars = (endPos < bufEnd);
//逐行把相关信息储存下来
v = out(source, here, endPos,
fmAscent, fmDescent, fmTop, fmBottom,
v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex],
needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
chs, widths, paraStart, ellipsize, ellipsizedWidth,
lineWidths[breakIndex], paint, moreChars);
if (endPos < spanEnd) {
fmTop = fm.top;
fmBottom = fm.bottom;
fmAscent = fm.ascent;
fmDescent = fm.descent;
} else {
fmTop = fmBottom = fmAscent = fmDescent = 0;
}
here = endPos;
breakIndex++;
if (mLineCount >= mMaximumVisibleLineCount) {
return;
}
}
}

这样StaticLayout的排版过程就完成了。文本的绘制则是交给父类Layout来做的,Layout的绘制分为两大部分,drawBackground和drawText。drawBackground做的事情是如果文本内有LineBackgroundSpan则绘制所有的LineBackgroundSpan,然后判断是否有高亮背景(文本选中的背景),如果有则绘制高亮背景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
int cursorOffsetVertical, int firstLine, int lastLine) {
//判断并绘制LineBackgroundSpan
if (mSpannedText) {
if (mLineBackgroundSpans == null) {
mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(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;
//逐行绘制LineBackgroundSpan
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) {
spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength);
spansLength = 0;
if (start != end || start == 0) {
//排除不在绘制范围内的LineBackgroundSpan
for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) {
if (mLineBackgroundSpans.spanStarts[j] >= end ||
mLineBackgroundSpans.spanEnds[j] <= start) continue;
spans = GrowingArrayUtils.append(
spans, spansLength, mLineBackgroundSpans.spans[j]);
spansLength++;
}
}
}
//对当前行内的LineBackgroundSpan进行绘制
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();
}
//判断并绘制高亮背景(即选中的文本)
if (highlight != null) {
if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical);
canvas.drawPath(highlight, highlightPaint);
if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical);
}
}

drawText用来逐行绘制Layout的文本、影响显示效果的Span、以及Emoji表情等。当有Emoji或者Span的时候,实际绘制工作交给TextLine类来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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实例
TextLine tl = TextLine.obtain();
//逐行绘制文本
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');
//获得所有的段落风格相关的Span
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;
}
//获取影响行缩进的Span
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 (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);
if (hasTabOrEmoji && !tabStopsIsInitialized) {
if (tabStops == null) {
tabStops = new TabStops(TAB_INCREMENT, spans);
} else {
tabStops.reset(TAB_INCREMENT, spans);
}
tabStopsIsInitialized = true;
}
//判断当前行的第五方式
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) {
//没有任何Emoji或者span的时候,直接调用Canvas来绘制文本
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
//当有Emoji或者Span的时候,交给TextLine类来绘制
tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
paint.setHyphenEdit(0);
}
TextLine.recycle(tl);
}

我们下面再来看看TextLine是如何绘制有特殊情况的文本的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
void draw(Canvas c, float x, int top, int y, int bottom) {
//判断是否有Tab或者Emoji
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) {
//获取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();
}
//调整emoji图像绘制矩形
emojiRect.set(x + h, y + bmAscent,
x + h + width, y);
//绘制Emoji图像
c.drawBitmap(bm, null, emojiRect, mPaint);
h += width;
j++;
}
segstart = j + 1;
}
}
}
}

这样就完成了文本的绘制工作,简单地总结就是:分析整体文本—>拆分为段落—>计算整体段落的文本包括Span的测量信息—>对文本进行折行—>根据最终行数把文本测量信息保存—>绘制文本的行背景—>判断并获取文本种的Span和Emoji图像—>绘制最终的文本和图像。当然我们省略了一部分内容,比如段落文本方向,单行的文本排版方向的计算,实际的处理要更为复杂。

接下来我们来看一下在测量过程中出现的FontMetrics,这是一个Paint的静态内部类。主要用来储存文字排版的Y轴相关信息。内部仅包含ascent、descent、top、bottom、leading五个数值。如下图:

1339061786_4121

除了leading以外,其他的数值都是相对于每一行的baseline的,也就是说其他的数值需要加上对应行的baseline才能得到最终真实的坐标。

6.TextView接收软键盘输入

Android上的标准文本编辑控件是EditText,而EditText对软键盘输入的处理,却是在TextView内部实现的。Android为所有的View预留了一个接收软键盘输入的接口类,叫InputConnection。软键盘以InputConnection为桥梁把文字输入、文字修改、文字删除等传递给View。任意View只要重写onCheckIsTextEditor()并返回true,然后重写onCreateInputConnection(EditorInfo outAttrs)返回一个InputConnection的实例,便可以接收软键盘的输入。TextView的软键盘输入接收,是通过EditableInputConnection类来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
//判断是否处于可编辑状态
if (onCheckIsTextEditor() && isEnabled()) {
mEditor.createInputMethodStateIfNeeded();
//设置输入法相关的信息
outAttrs.inputType = getInputType();
if (mEditor.mInputContentType != null) {
outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
outAttrs.actionId = mEditor.mInputContentType.imeActionId;
outAttrs.extras = mEditor.mInputContentType.extras;
} else {
outAttrs.imeOptions = EditorInfo.IME_NULL;
}
if (focusSearch(FOCUS_DOWN) != null) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
}
if (focusSearch(FOCUS_UP) != null) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
}
if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
== EditorInfo.IME_ACTION_UNSPECIFIED) {
if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
//把软键盘的enter设为下一步
outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
} else {
//把软键盘的enter设为完成
outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
}
if (!shouldAdvanceFocusOnEnter()) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
}
if (isMultilineInputType(outAttrs.inputType)) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
outAttrs.hintText = mHint;
//判断TextView内部文本是否可编辑
if (mText instanceof Editable) {
//返回EditableInputConnection实例
InputConnection ic = new EditableInputConnection(this);
outAttrs.initialSelStart = getSelectionStart();
outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
return ic;
}
}
return null;
}

我们再来看一下EditableInputConnection里面的几个主要的方法:

首先是commitText方法,这个方法接收输入法输入的字符并提交给TextView。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean commitText(CharSequence text, int newCursorPosition) {
//判断TextView是否为空
if (mTextView == null) {
return super.commitText(text, newCursorPosition);
}
//判断文本是否Span,来自输入法的Span一般只有SuggestionSpan,SuggestionSpan携带了输入法的错别字修正的词
if (text instanceof Spanned) {
Spanned spanned = ((Spanned) text);
SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
mIMM.registerSuggestionSpansForNotification(spans);
}
mTextView.resetErrorChangedFlag();
//提交字符
boolean success = super.commitText(text, newCursorPosition);
mTextView.hideErrorIfUnchanged();
//返回是否成功
return success;
}

getEditable方法,这个方法并不是InputConnection接口的一部分,而是EditableInputConnection的父类BaseInputConnection的方法,用来获取一个可编辑对象,EditableInputConnection里面的所有修改都针对这个可编辑对象来做。

1
2
3
4
5
6
7
8
public Editable getEditable() {
TextView tv = mTextView;
if (tv != null) {
//返回TextView的可编辑对象
return tv.getEditableText();
}
return null;
}

deleteSurroundingText方法,这个方法用来删除光标前后的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength
+ " / " + afterLength);
final Editable content = getEditable();
if (content == null) return false;
//批量删除标记
beginBatchEdit();
//获取当前已选择的文本的位置
int a = Selection.getSelectionStart(content);
int b = Selection.getSelectionEnd(content);
if (a > b) {
int tmp = a;
a = b;
b = tmp;
}
int ca = getComposingSpanStart(content);
int cb = getComposingSpanEnd(content);
if (cb < ca) {
int tmp = ca;
ca = cb;
cb = tmp;
}
if (ca != -1 && cb != -1) {
if (ca < a) a = ca;
if (cb > b) b = cb;
}
int deleted = 0;
//删除光标之前的文本
if (beforeLength > 0) {
int start = a - beforeLength;
if (start < 0) start = 0;
content.delete(start, a);
deleted = a - start;
}
//删除光标之后的文本
if (afterLength > 0) {
b = b - deleted;
int end = b + afterLength;
if (end > content.length()) end = content.length();
content.delete(b, end);
}
//结束批量编辑
endBatchEdit();
return true;
}

commitCompletion和commitCorrection方法,即是用来补全单词和修正错别字的方法,这两个方法内部都是调用TextView对应的方法来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean commitCompletion(CompletionInfo text) {
if (DEBUG) Log.v(TAG, "commitCompletion " + text);
mTextView.beginBatchEdit();
mTextView.onCommitCompletion(text);
mTextView.endBatchEdit();
return true;
}
@Override
public boolean commitCorrection(CorrectionInfo correctionInfo) {
if (DEBUG) Log.v(TAG, "commitCorrection" + correctionInfo);
mTextView.beginBatchEdit();
mTextView.onCommitCorrection(correctionInfo);
mTextView.endBatchEdit();
return true;
}

8.总结

一个展示文本+文本编辑器功能的控件需要做的事情很多,要对文本进行排版、处理不同的段落风格、处理段落内的不同emoji和span、进行折行计算,然后还需要做文本编辑、文本选择等。而TextView把这些事情明确分工给不同的类。这样不仅仅把复杂问题拆分成了一个个简单的小功能,同时也大大增加了可扩展性。


发现文章,代码有误、对内容有疑问,请在Issue上面提单,技术讨论可以给我Email,欢迎关注左边微信公众号或加QQ群获取更多信息。