简介
看段Android官方的简介
Class Overview
Displays text to the user and optionally allows them to edit it. A TextView is a complete text editor, however the basic class is configured to not allow editing; see EditText for a subclass that configures the text view for editing.
To allow users to copy some or all of the TextView’s value and paste it somewhere else, set the XML attribute android:textIsSelectable to “true” or call setTextIsSelectable(true). The textIsSelectable flag allows users to make selection gestures in the TextView, which in turn triggers the system’s built-in copy/paste controls.
TextView主要用于给用户展示文字,并且让用户随意的可以对文字进行编辑。但是普通的TextView是不允许用来编辑的,只有EditText才可以。
如果在XML中设置了android:textIsSelectable 或者在Java代码中调用了setTextIsSelectable(true)方法,就可以允许对TextView的部分或者全部文字进行复制,然后粘贴到其他地方。textIsSelectable 标签是允许用户在TextView上使用选择手势。
顺便提下,大家如果想看API文档的话,可以在
file:///E:/AndroidEnvironment/SDK/docs/reference/android/widget/TextView.html
你安装SDK的目录下/docs/reference/android/widget/TextView.html找到你想要查看控件的API
分析思路
一般自定义view都需要满足2个条件,展示我们期望的UI,正确传递或者接收处理点击或者触摸事件。
所以对于TextView的分析也从这三个地方展开
-
绘制过程
onMeasure()
onLayout()
onDraw() -
事件接收处理
由于TextView继承于View,所以主要分析onTouchEvent()方法就好了
一些和TextView有关的类如何实现,比如Spans,Layout,接收输入的InputConnection
本文基于Android SDK API-19的基础上分析
在分析之前,我们先来看个小彩蛋
不知道这个//TODO是某个哥们自问自答呢,还是别人在对他的代码review的时候给注上的
再分析之前,顺便抛出一个问题供大家思考下,maxEms这个属性到底是用来做什么的?
网上的答案五花八门,在下面的源码中我们可以一窥究竟。
绘制过程
首先来看onMeasure()部分代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//首先接收到父容器传递过来的MeasureSpec
//关于MeasureSpec是如果计算的,可以查看之前的博文
//[LinearLayout源码解析](http://blog.csdn.net/wz249863091/article/details/51702980)
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
//这里解释下什么叫作boring
//A BoringLayout is a very simple Layout implementation for text that
//fits on a single line and is all left-to-right characters.
//boring就是指布局所用的文本里面不包含任何Span,所有的文本方向都是从左到右的布局,
//并且仅需一行就能显示完全的布局
//这里将TextView和Hint的boring初始化
BoringLayout.Metrics boring = UNKNOWN_BORING;
BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
//获得文字的排序方式。一共有6种
//FIRSTSTRONG_RTL,FIRSTSTRONG_LTR Unicode双向算法
//ANYRTL_LTR
//LTR,RTL 左到右或者右到左排序
//LOCALE
//first strong算法 有兴趣的同学可以自行研究下,一般情况下都是左到右排序
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}
int des = -1;
boolean fromexisting = false;
//如果宽度是精确模式了,那就那父容器给的宽度当作当前TextView的宽度
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
if (mLayout != null && mEllipsize == null) {
//首先计算下期望值,如果行数大于1就返回-1,否则返回单行宽度
//具体代码贴在下面
des = desired(mLayout);
}
//如果小于0,即行数大于1行,就去判断是否是boring
//isBoring()这个方法也在下面有详细分析,大家可以阅读后
//再回过头来看看
if (des < 0) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
//阅读过下面的方法,就知道boring是一个Metrics矩阵,
//包含了文本样式 width, ascent, and descen等
if (boring != null) {
mBoring = boring;
}
} else {
fromexisting = true;
}
//再次判断boring是否为null
//这里有2种情况会为null
//1.des>0,即Textview只显示一行文字,就不会去计算boring的值了
//2.Textview包含的内容不是boring的,多行,有缩进或者包含spann
if (boring == null || boring == UNKNOWN_BORING) {
//如果是多行文字的
if (des < 0) {
des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
}
width = des;
} else {
//如果是boring模式的就很简单了,把boring刚测量得到的width赋给TextView
//即文字的宽度
width = boring.width;
}
//这里就是加上Drawable的宽度
final Drawables dr = mDrawables;
if (dr != null) {
width = Math.max(width, dr.mDrawableWidthTop);
width = Math.max(width, dr.mDrawableWidthBottom);
}
//这里会再计算一次hint的宽度,流程和上面的一模一样,就不再重复了
if (mHint != null) {
int hintDes = -1;
int hintWidth;
if (mHintLayout != null && mEllipsize == null) {
hintDes = desired(mHintLayout);
}
if (hintDes < 0) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}
if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
if (hintDes < 0) {
hintDes = (int) FloatMath.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
}
hintWidth = hintDes;
} else {
hintWidth = hintBoring.width;
}
if (hintWidth > width) {
width = hintWidth;
}
}
//这里再加上padding的值
//顺便说一句,padding的值是在子view里自己算的
//margin的值是在父容器里算的
//在自定义view和viewgroup的时候,千万注意
//width += getCompoundPaddingLeft() + getCompoundPaddingRight();
//在这,就能解答之前的疑问,EMS这个属性到底是干嘛的
//如果我们设置了maxEms这个属性
//public void setMaxEms(int maxems) {
// mMaxWidth = maxems;
//mMaxWidthMode = EMS;
//requestLayout();
//invalidate();
//}
//mMaxWidth的值就是EMS的值
//如果设置了maxLength,那么mMaxWidth的值就是maxWidth的值
//然后再来看如果是EMS模式
//Math.min(width, mMaxWidth * getLineHeight())
//我们的最大宽度就是EMS的值乘以lineHeight的值
//而lineHeight的值 官方是这么解释的
//return the height of one standard line in pixels
//public int getLineHeight() {
// return FastMath.round(mTextPaint.getFontMetricsInt(null) *
//mSpacingMult + mSpacingAdd);
//就是行间距乘以字体大小
//所以在不同行间距和字体大小下,EMS所产生的mMaxWidth也是不同的
}
if (mMaxWidthMode == EMS) {
width = Math.min(width, mMaxWidth * getLineHeight());
} else {
width = Math.min(width, mMaxWidth);
}
if (mMinWidthMode == EMS) {
width = Math.max(width, mMinWidth * getLineHeight());
} else {
width = Math.max(width, mMinWidth);
}
// Check against our minimum width
width = Math.max(width, getSuggestedMinimumWidth());
//如果是Wrap的,会在父容器给的size和实际最大size中取小的
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
}
//最后根据上面计算得到的size-padding的值就是我们单行text实际可以展示的大小
int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
int unpaddedWidth = want;
//如果是水平方向可以scroll的,那么宽度就是无限大了,因为可以滑嘛
if (mHorizontallyScrolling) want = VERY_WIDE;
int hintWant = want;
int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
//这里会牵扯到makeNewLayout(...)这个方法,也会在下面得到详细分析
if (mLayout == null) {
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
} else {
final boolean layoutChanged = (mLayout.getWidth() != want) ||
(hintWidth != hintWant) ||
(mLayout.getEllipsizedWidth() !=
width - getCompoundPaddingLeft() - getCompoundPaddingRight());
final boolean widthChanged = (mHint == null) &&
(mEllipsize == null) &&
(want > mLayout.getWidth()) &&
(mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));
final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
if (layoutChanged || maximumChanged) {
if (!maximumChanged && widthChanged) {
mLayout.increaseWidthTo(want);
} else {
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
}
} else {
// Nothing has changed
}
}
//然后开始计算高度,这部分代码相对于宽度,就简单的多了
//如果是精确模式,那么高度就等于TextView要求的高度
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
//计算下想要的高度
//这里逻辑比较简单
//只需要比较下文字高度和hint的高度,取大的那个值就可以了
//至于文字高度和hint高度的计算:
//1.当行高度*行数
//如果设置了Drawable的话,比较2个值得大小,取大的
//如果设置了maxLines或者maxHeight计算下当前高度有没超过最大高度,超过的话取最大高度
//如果设置了minLines或者minHeight的话,比较下当前高度和最小高度,取小的
int desired = getDesiredHeight();
height = desired;
mDesiredHeightAtMeasure = desired;
//如果是warp模式,就取父容器算的和实际需要小的值
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
}
int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
}
/*
* We didn't let makeNewLayout() register to bring the cursor into view,
* so do it here if there is any possibility that it is needed.
*/
//这里就是处理下滚动条
if (mMovement != null ||
mLayout.getWidth() > unpaddedWidth ||
mLayout.getHeight() > unpaddedHeight) {
registerForPreDraw();
} else {
scrollTo(0, 0);
}
setMeasuredDimension(width, height);
}
desire()方法
private static int desired(Layout layout) {
//首先获得行数
int n = layout.getLineCount();
CharSequence text = layout.getText();
float max = 0;
// if any line was wrapped, we can't use it.
// but it's ok for the last line not to have a newline
//如果行数大于1,就返回-1
for (int i = 0; i < n - 1; i++) {
if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
return -1;
}
//将宽度和0比较,如果大于0,就取宽度
for (int i = 0; i < n; i++) {
max = Math.max(max, layout.getLineWidth(i));
}
return (int) FloatMath.ceil(max);
}
isBoring()实现
/**
* Returns null if not boring; the width, ascent, and descent in the
* provided Metrics object (or a new one if the provided one was null)
* if boring.
* @hide
*/
//如果是boring模式的就返回Metrics object,不是就返回null
//什么是boring模式 开头已经讲过了,根据他的定义也不难猜到这个方法有几个条件判断
public static Metrics isBoring(CharSequence text, TextPaint paint,
TextDirectionHeuristic textDir, Metrics metrics) {
//首先获得一个char型数组,这里值得一提的是TextUtils.obtain(500)这个方法
//在我们自己写代码的时候也可以借鉴,减少内存的交换
//这里把字符串分成了以500个字符为一组
char[] temp = TextUtils.obtain(500);
int length = text.length();
boolean boring = true;
outer:
for (int i = 0; i < length; i += 500) {
int j = i + 500;
//首先判断是否当前组的字符串是否有500个
//没有就取实际长度
if (j > length)
j = length;
//根据长度取出字符串的子串
TextUtils.getChars(text, i, j, temp, 0);
//子串的长度
int n = j - i;
//遍历整个子串
for (int a = 0; a < n; a++) {
char c = temp[a];
//这里有3个条件
//1.如果有换行 \n
//2.如果有缩进 \t
//3.如果不是LTR 左到右模式
//如果有其中1种情况,就视为不是boring模式
if (c == '\n' || c == '\t' || c >= FIRST_RIGHT_TO_LEFT) {
boring = false;
break outer;
}
}
if (textDir != null && textDir.isRtl(temp, 0, n)) {
boring = false;
break outer;
}
}
//把temp回收
TextUtils.recycle(temp);
//如果包含了span,那么也视为不是boring模式
if (boring && text instanceof Spanned) {
Spanned sp = (Spanned) text;
Object[] styles = sp.getSpans(0, length, ParagraphStyle.class);
if (styles.length > 0) {
boring = false;
}
}
//如果是boring模式,那就返回Metrics对象
if (boring) {
Metrics fm = metrics;
//首先判断传进来的Metrics是否为空,如果为空,就新建一个对象
if (fm == null) {
fm = new Metrics();
}
//设置TextLine,文本样式
TextLine line = TextLine.obtain();
line.set(paint, text, 0, length, Layout.DIR_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
fm.width = (int) FloatMath.ceil(line.metrics(fm));
TextLine.recycle(line);
return fm;
} else {
return null;
}
}
makeNewLayout(…)方法
/**
* The width passed in is now the desired layout width,
* not the full view width with padding.
* {@hide}
*/
protected void makeNewLayout(int wantWidth, int hintWidth,
BoringLayout.Metrics boring,
BoringLayout.Metrics hintBoring,
int ellipsisWidth, boolean bringIntoView) {
//首先,如果有跑马灯效果,先把跑马灯停了
stopMarquee();
// Update "old" cached values
//把最大宽度和最大行数先保存起来
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);
//是否需要省略号,这个值是根据我们在XML中写的Ellipsize来定的
boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
//省略号主要分开始位置,中间位置和结束位置3个常规位置
//还有跑马灯这种非常规位置
//mMarqueeFadeMode分为3种效果
//MARQUEE_FADE_NORMAL
//MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS
//MARQUEE_FADE_SWITCH_SHOW_FADE
//这里判断是否为常规的marquee
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;
}
//获得排序方向,一般是LTR 左到右
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}
//获得一个singleLayout,这个方法在下面分析
mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
effectiveEllipsize, effectiveEllipsize == mEllipsize);
//如果是非常规的跑马灯,需要再创建一个mSavedMarqueeModeLayout,在播放跑马灯的时候
//把这个layout作为mLayout
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;
//这段代码看着眼熟不?就是刚在onLayout()里分析的那段
if (hintBoring == UNKNOWN_BORING) {
//如果hint只是单行,无缩进,无spann那么就是把boring的矩阵赋值hintBoring
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}
//这里有3层if嵌套,如果不仔细看,很容易晕
//主要分为3层逻辑
//最外层当前的hint是否是boring的,即单行无span
//第二层主要判断是否需要省略号,当前hint实际需要宽度是否大于计算得到的宽度
//如果仔细看了前面分析可以知道wantWidth,hintWidth和ellipsisWidth
//其实都是一个值 width-paddingleft-paddingRight
//即实际可以提供的宽度
//然后最里面那层就是判断mSavedHintLayout是否为null
//如果为null就make一个新的,不为null就把老的值更新
//先看如果是boring模式
if (hintBoring != null) {
//首先判断hint需要宽度是否小于实际给的宽度
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;
//如果不满足上面的要求
//再判断是否需要省略号,并且需要宽度是否小于实际给的宽度
//如果进入这个if条件的,都是设置了省略号,但是不需要显示的
} 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);
}
//如果还是不满足
//到了这就应该是需要省略号,但是需要宽度是大于实际给的宽度
//那么就应该显示省略号了
} else if (shouldEllipsize) {
mHintLayout = new StaticLayout(mHint,
0, mHint.length(),
mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, mEllipsize,
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
} else {
mHintLayout = new StaticLayout(mHint, mTextPaint,
hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
//到了这开始处理不是boring的hint
//如果不是boring的,需要省略号
} else if (shouldEllipsize) {
mHintLayout = new StaticLayout(mHint,
0, mHint.length(),
mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, mEllipsize,
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
//如果不是boring的,不需要省略号
} else {
mHintLayout = new StaticLayout(mHint, mTextPaint,
hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
}
//如果文字方向发生变化了,就重新注册OnPreDrawListener
//OnPreDrawListener回调的时机是
//即将绘制视图树时执行的回调函数。这时所有的视图都测量完成并确定了框架。 客户端可以
//使用该方法来调整滚动边框,甚至可以在绘制之前请求新的布局。
if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
registerForPreDraw();
}
//这里开始处理跑马灯
//如果需要播放跑马灯
if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
if (!compressText(ellipsisWidth)) {
final int height = mLayoutParams.height;
// If the size of the view does not depend on the size of the text, try to
// start the marquee immediately
//这里值得稍微留意是
//如果当前TextView的宽度不需要依赖内部文字的话
//直接就可以播放跑马灯了
if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
startMarquee();
} else {
// Defer the start of the marquee until we know our width (see setFrame())
mRestartMarquee = true;
}
}
}
// CursorControllers need a non-null mLayout
if (mEditor != null) mEditor.prepareCursorControllers();
}
分析到这onMeasure()就结束了
让我们来看下onLayout()是如何实现的
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//这里主要的逻辑就是bringPointIntoView
if (mDeferScroll >= 0) {
int curs = mDeferScroll;
mDeferScroll = -1;
bringPointIntoView(Math.min(curs, mText.length()));
}
}
最后我们看下onDraw()的实现,看看文字是如何被绘制到屏幕上的
@Override
protected void onDraw(Canvas canvas) {
restartMarqueeIfNeeded();
// Draw the background for this view
super.onDraw(canvas);
//首先先计算padding和scorll的值
//还有判断是LTR还是RTS方向
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 ;
//如果有drawable,那么先绘制draw
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.
//开始绘制DrawableLeft
if (dr.mDrawableLeft != null) {
//这里简单介绍下canvas.save()和canvas.restore()
//调用save之后,可以对canvas进行平移和旋转,确定新的原点然后绘制
//等绘制完了之后,可以把原点恢复原状
canvas.save();
canvas.translate(scrollX + mPaddingLeft + leftOffset,
scrollY + compoundPaddingTop +
(vspace - dr.mDrawableHeightLeft) / 2);
//个人认为TextView整个控件写了十分出彩
//TextView需要绘制背景,文字,Drawable
//谷歌在处理这个控件的时候,把不同的事交给不同的类去完成,充分解耦
//文字部分用Editor和layout处理
//图片部分用Drawable自行绘制
//整个TextView其实只是充当了容器作用
dr.mDrawableLeft.draw(canvas);
canvas.restore();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mDrawableRight != null) {
canvas.save();
canvas.translate(scrollX + right - left - mPaddingRight
- dr.mDrawableSizeRight - rightOffset,
scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
dr.mDrawableRight.draw(canvas);
canvas.restore();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mDrawableTop != null) {
canvas.save();
canvas.translate(scrollX + compoundPaddingLeft +
(hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);
dr.mDrawableTop.draw(canvas);
canvas.restore();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mDrawableBottom != null) {
canvas.save();
canvas.translate(scrollX + compoundPaddingLeft +
(hspace - dr.mDrawableWidthBottom) / 2,
scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
dr.mDrawableBottom.draw(canvas);
canvas.restore();
}
}
int color = mCurTextColor;
//如果layout为null,通过刚分析的makeNewLayout()方法,再去获得一个Layout
if (mLayout == null) {
assumeLayout();
}
Layout layout = mLayout;
//如果当前没有文字,并且设置了hint,那么就显示hint
if (mHint != null && mText.length() == 0) {
if (mHintTextColor != null) {
color = mCurHintTextColor;
}
layout = mHintLayout;
}
mTextPaint.setColor(color);
mTextPaint.drawableState = getDrawableState();
canvas.save();
//感觉写TextView控件这位工程师对自己写的代码不是很自信,留下了很多疑问
//也许是Review之后忘了删除了,自己看的时候有时候会和有代入感,感觉在给别人review代码
/* 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;
//计算矩阵的上下左右4个坐标值
float clipLeft = compoundPaddingLeft + scrollX;
float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
float clipRight = right - left - compoundPaddingRight + 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);
//如果有跑马灯,并且不是MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS模式的话
if (mEllipsize == TextUtils.TruncateAt.MARQUEE &&
mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
//如果当前只有1行显示,并且不是SingleLine的,也不是Gravity.LEFT
if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
(absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
final int width = mRight - mLeft;
final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
//dx值就是layout的宽度减去实际宽度再减去padding的值,主要是给RTL模式计算偏移量
final float dx = mLayout.getLineRight(0) - (width - padding);
canvas.translate(isLayoutRtl ? -dx : +dx, 0.0f);
}
if (mMarquee != null && mMarquee.isRunning()) {
final float dx = -mMarquee.getScroll();
canvas.translate(isLayoutRtl ? -dx : +dx, 0.0f);
}
}
final int cursorOffsetVertical = voffsetCursor - voffsetText;
//这里终于开始绘制文字了
Path highlight = getUpdatedHighlightPath();
//如果是EditText的就交给mEditor绘制,普通TextView,就交给layout处理
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
if (mMarquee != null && mMarquee.shouldDrawGhost()) {
final int dx = (int) mMarquee.getGhostOffset();
canvas.translate(isLayoutRtl ? -dx : dx, 0.0f);
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
canvas.restore();
}
总结
上文中主要分析了TextView的整个绘制流程,主要是从过程的角度分析了几个比较重要的阶段。
在下一篇TextView源码分析(二)中会具体分析Layout,Editor和Drawable是如何完成绘制,排版。