简介


看段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的分析也从这三个地方展开

  1. 绘制过程

    onMeasure()
    onLayout()
    onDraw()

  2. 事件接收处理

    由于TextView继承于View,所以主要分析onTouchEvent()方法就好了

  3. 一些和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是如何完成绘制,排版。

更多相关文章

  1. android 文字转化为语音TextToSpeech
  2. 拍照(连续拍照 焦距 压缩图像)
  3. Android(安卓)Dialog 自定义宽度
  4. 2013.12.05(4)——— android ViewPagerIndicator之SampleLinesDef
  5. 自定义开关控件(ToggleView)继承View实现
  6. Android(安卓)人脸识别 ERROR: Return 0 faces because error ex
  7. listview使用BaseAdapter显示图片和文字
  8. 简单有效的ItemDecoration--分割线
  9. miui卸载爆炸效果

随机推荐

  1. Android下得到Home键按下的消息
  2. Android AutoCompleteTextView控件实现类
  3. Android Gradle Study
  4. 绘图机制
  5. 【Android Demo】图片之滑动效果(Gallery
  6. onAttachToWindow() 调用
  7. Android中AlarmManager的使用
  8. Android MD5加密算法
  9. android踩坑日记
  10. Android JNI开发工具篇(1)-开发环境搭建