TextView源码解析-----绘制过程
简介
看段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是如何完成绘制,排版。
更多相关文章
- android 文字转化为语音TextToSpeech
- 拍照(连续拍照 焦距 压缩图像)
- Android(安卓)Dialog 自定义宽度
- 2013.12.05(4)——— android ViewPagerIndicator之SampleLinesDef
- 自定义开关控件(ToggleView)继承View实现
- Android(安卓)人脸识别 ERROR: Return 0 faces because error ex
- listview使用BaseAdapter显示图片和文字
- 简单有效的ItemDecoration--分割线
- miui卸载爆炸效果