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

  1. 绘制过程

    onMeasure()
    onLayout()
    onDraw()

  2. 事件接收处理

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

  3. 一些和TextView有关的类如何实现,比如Spans,Layout,接收输入的InputConnection

本文基于Android SDK API-19的基础上分析

在分析之前,我们先来看个小彩蛋

TextView源码解析-----绘制过程_第1张图片

不知道这个//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源码学习,Android源码分析)