TextView源码解析

1,930 阅读10分钟

一、前置知识

(一)Span

文档引导

Span 是功能强大的标记对象,可用于在字符或段落级别为文本设置样式。通过将 Span 附加到文本对象,您能够以各种方式更改文本,包括添加颜色、使文本可点击、缩放文本大小以及以自定义方式绘制文本。Span 还可以更改 TextPaint 属性、在 Canvas 上绘制,以及更改文本布局。

Android为我们提供的Span非常多,可大致分为以下几类:

  • 影响文本外观的 Span:如更改文本或背景颜色以及添加下划线或删除线,会触发重新绘制文本,而不会触发重新计算布局。这些 Span 会实现 UpdateAppearance并扩展 CharacterStyleCharacterStyle 子类通过提供更新 TextPaint 的访问权限来定义如何绘制文本。
  • 影响文本指标的 Span:如行高和文本大小,会导致观察对象重新测量文本,以实现正确的布局和渲染。这些 Span 通常会扩展 MetricAffectingSpan类,该类为抽象类,它允许子类通过提供对 TextPaint的访问权限来定义 Span 影响文本测量的方式。由于 MetricAffectingSpan 会扩展 CharacterSpan,因此子类会影响字符级别的文本外观。
  • 影响单个字符的Span:如更新背景颜色、样式或大小等字符元素。影响单个字符的 Span 会扩展 CharacterStyle
  • 影响段落的Span:如更改整个文本块的对齐方式或边距。影响整个段落的 Span 会实现 ParagraphStyle。使用这些 Span 时,必须将其附加到整个段落,不包括末尾换行符【通过换行符分割段落】。如果您尝试将段落 Span 应用于除整个段落以外的其他内容,Android 根本不会应用该 Span。

我们可以根据Span子类实现/继承的类来区分是上述哪种类型:

  • Span会影响文本外观,需要实现UpdateAppearance
  • Span 会影响文本指标/尺寸,需要实现UpdateLayout
  • Span 会在字符级别影响文本,需要实现 CharacterStyle
  • Span 会在段落级别影响文本,需要实现ParagraphStyle

如何将span附加到塞入TextView中的string上呢?

Android中定义了SpannedStringSpannableStringSpannableStringBuilder类将CharSequenceSpan建立关联,即存储每一个char对应的Span标记。

  • SpannedString:实现Spanned接口,仅读取而不设置文本和 span;继承自SpannableStringInternal内部使用线性数组实现
  • SpannableString:实现Spannable接口,设置少量的 Span,而不设置文本;继承自SpannableStringInternal内部使用线性数组实现
  • SpannableStringBuilder:实现Spannable接口,需要设置文本和span;设置大量的Span;内部使用区间数实现

(二)FontMetrics

  • BaseLine:在 TextView 中每一行都有一条基线,叫 BaseLine ,文本的绘制是从这里开始的,这是以当前行为坐标系,y 方向为 0 的一条线,也就是说,BaseLine 以上是负数,以下是正数。
  • ascent:从 BaseLine 向上到字符的最高处。
  • descent:从 BaseLine 向下到字符的最低处。
  • top、bottom:在说 top 和 bottom 之前,需要知道,世界上很多国家,文字书写也是不相同的,有些文字可能带有读音符之类的上标或者下标,比如上图中的 A ,它上面的波浪线就是类似于读音符(具体是啥,我也不知道),Android 为了更好的画出这些上标或者下标,特意在每一行的 ascent 和 descent 外都预留了一点距离,即 top 是 ascent 加上上面预留出来的距离所表示的坐标,bottom 也是一样的。
  • leading:是表示上一行的descent 到当前行的 ascent 之间的距离

二、本文摘要

TextView是Android控件中较为复杂的控件,也是使用频率较高的控件,通过阅读TextView的源码,帮助我们了解View组件的绘制流程。

阅读源码前,我们先思考一下当我们自己去实现一个TextView时,会考虑哪些点?

1、当我们绘制一行简单的文本,在调用canvas#drawText()之前,如何测量view大小、绘制位置?

2、当我们绘制多行文本时,如何确定需要换行?如何在第一个问题的基础上确定后面几行的绘制位置?

3、当一行文本中插入了扩大文本大小的Span时,如何计算整行的高度?如何定位没有被修改大小属性的文本绘制位置?

在总结上述问题前,我们先将TextView#setText()作为TextView的入口,来跟踪一下源码的绘制流程。

(一)源码跟踪

TextView对外提供了几个 setText 的重载方法,但其实最终都会进入内部的 setText 方法,代码如下:

    private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        ......
        if (mLayout != null) {//1
            checkForRelayout();//2
        }
        ......
    }

在这个方法中,主要做一些初始化工作,如为传入的CharSequence重新设置Span,添加触摸事件等,其中最重要的是checkForRelayout()方法。

首先,我们会对mLayout != null这行代码产生如下疑问:

  • mLayout是什么?
    mLayout其实就是Layout的子类,并不是我们View绘制中理解的布局,可以理解为一个 TextView展示在屏幕上的布局相关的管理信息类,也负责处理文本测量与绘制的前置工作。Layout有三个子类:
    • BoringLayout
      是Layout的最简单的实现,主要用于适配单行文字展示,并且只支持从左到右的展示 方向。不建议在自己的开发过程中直接使用,如果需要使用的话,首先使用isBoring 判断文字是否符合要求。
    • DynamicLayout
      支持在排列布局之后修改文字,修改之后会更新text内容。
    • StaticLayout
      在文字被排列布局之后不允许修改。
  • mLayout什么时候初始化的?
    TextView#onMeasure()做布局测量时,会通过TextView#makeNewLayout()方法初始化mLayout。
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        if (mLayout == null) {
          	//初始化mLayout
            makeNewLayout(want, hintWant, boring, hintBoring,
                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
        }
        ......
    }

1、测量布局

由此,我们可以知道,当走到TextView#setText()方法时,mLayout已经被赋值了,因此一定会走到TextView#checkForRelayout()方法,这个方法的主要工作是判断是否需要重新测量布局。

    private void checkForRelayout() {
        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
          	//1.如果TextView的宽度是固定值,而不是WRAP_CONTENT,直接重建一个新的mLayout
            ......

			//关注
            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                          false);

          	//1.1.TextView.mEllipsize == TextUtils.TruncateAt.MARQUEE:设置为跑马灯效果
            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                  	//1.1.1.如果TextView的高度是固定值,则不需要触发重新布局,只要重绘
                    autoSizeText();
                    invalidate();
                    return;
                }

                if (mLayout.getHeight() == oldht
                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                  	//1.1.2.如果TextView的高度是动态的,但是布局高度没有改变,则不需要触发重新布局,只要重绘
                    autoSizeText();
                    invalidate();
                    return;
                }
            }

          	//1.2.如果TextView的高度是动态的,并且高度发生变化了,就需要触发重新布局
            requestLayout();
            invalidate();
        } else {
          	//2.如果TextView的宽度是动态的,就需要触发重新布局
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }

这里的makeNewLayout()方法就是上文所说的初始化mLayout的方法。

    public void makeNewLayout(int wantWidth, int hintWidth,
                                 BoringLayout.Metrics boring,
                                 BoringLayout.Metrics hintBoring,
                                 int ellipsisWidth, boolean bringIntoView) {
        ......

        mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
                effectiveEllipsize, effectiveEllipsize == mEllipsize);
        ......
    }

此方法主要获取能够创建mLayout所需的参数,如对齐方式、省略方式、文字方向等。最后通过TextView#makeSingleLayout()构建出mLayout

    protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
            Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
            boolean useSaved) {
        Layout result = null;

        if (useDynamicLayout()) {
            //1.如果文字被选中,或文字设置了Span,则创建DynamicLayout
            final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(......);
            ......
        } else {
            //2.判断是否符合BoringLayout的创建条件,如果符合,则创建BoringLayout
            if (boring == UNKNOWN_BORING) {
                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
                if (boring != null) {
                    mBoring = boring;
                }
            }

            if (boring != null) {
                if (boring.width <= wantWidth
                        && (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
                    if (useSaved && mSavedLayout != null) {
                        //2.1用缓存的layout
                        result = mSavedLayout.replaceOrMake(......);
                    } else {
                        //2.2创建BoringLayout
                        result = BoringLayout.make(......);
                    }

                    if (useSaved) {
                        mSavedLayout = (BoringLayout) result;
                    }
                } else if (shouldEllipsize && boring.width <= wantWidth) {
                    if (useSaved && mSavedLayout != null) {
                        //2.1用缓存的layout
                        result = mSavedLayout.replaceOrMake(......);
                    } else {
                        //2.2创建BoringLayout
                        result = BoringLayout.make(......);
                    }
                }
            }
        }
        if (result == null) {
            //3.兜底逻辑,如果上述条件都不符合,则默认创建StaticLayout
            StaticLayout.Builder builder = StaticLayout.Builder.obtain(......);
            ......
        }
        return result;
    }

至此,mLayout的创建逻辑就走完了,接下来,我们看一下TextView的布局、绘制流程。当TextView#checkForRelayout()方法中满足了重新布局的条件时,会通过requestLayout()方法通知ViewRootImpl重新布局,根据view的绘制流程,RootView会递归调用子View的onLayout()方法来通知子view进行测量,因此我们来看一下TextView#onLayout()

TextView#onLayout()主要是为了计算当前字体大小,如果允许动态测量字体,则需要通过TextView#makeNewLayout()重新创建mLayout。

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        ......
        // Call auto-size after the width and height have been calculated.
        autoSizeText();
    }
    private void autoSizeText() {
        if (!isAutoSizeEnabled()) {
            return;
        }

        if (mNeedsAutoSizeText) {
            if (getMeasuredWidth() <= 0 || getMeasuredHeight() <= 0) {
                return;
            }

            final int availableWidth = mHorizontallyScrolling
                    ? VERY_WIDE
                    : getMeasuredWidth() - getTotalPaddingLeft() - getTotalPaddingRight();
            final int availableHeight = getMeasuredHeight() - getExtendedPaddingBottom()
                    - getExtendedPaddingTop();

            if (availableWidth <= 0 || availableHeight <= 0) {
                return;
            }

            synchronized (TEMP_RECTF) {
                TEMP_RECTF.setEmpty();
                TEMP_RECTF.right = availableWidth;
                TEMP_RECTF.bottom = availableHeight;
                final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);

                if (optimalTextSize != getTextSize()) {
                    setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize,
                            false /* shouldRequestLayout */);

                    makeNewLayout(availableWidth, 0 /* hintWidth */, UNKNOWN_BORING, UNKNOWN_BORING,
                            mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                            false /* bringIntoView */);
                }
            }
        }
        // Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing
        // after the next layout pass should set this to false.
        mNeedsAutoSizeText = true;
    }

2、绘制

至此,布局测量的流程就完成了。接着,TextView#checkForRelayout()方法中,会通过invalidate()方法通知view绘制,根据view的绘制流程,RootView会递归调用子View的onDraw()方法来通知子view进行绘制,因此我们来看一下TextView#onDraw()

2.1、绘制与文本无关的

首先,会处理与文字本身的绘制无关的绘制工作,如绘制控件背景、drawable、hint、padding、shadow,并不断平移canvas,调整文字绘制的锚点。

    protected void onDraw(Canvas canvas) {
        restartMarqueeIfNeeded();

        // Draw the background for this view
        super.onDraw(canvas);

        ......
        //1、绘制drawable
        final Drawables dr = mDrawables;
        if (dr != null) {
            ......
            
            //如果TextView设置了Drawable,通过drawable#draw(canvas)绘制drawable
            //然后,需要根据drawable的位置(Left、Right、Top、Bottom)来将canvas平移,这样就可以依旧将画布左上角作为文字绘制的锚点
            if (dr.mShowing[Drawables.LEFT] != null) {
                canvas.save();
                canvas.translate(scrollX + mPaddingLeft + leftOffset,
                        scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
                dr.mShowing[Drawables.LEFT].draw(canvas);
                canvas.restore();
            }

            ......
        }

        ......

        //2、绘制hint文本
        if (mHint != null && mText.length() == 0) {
            //如果文本内容为空,并且存在提示文字,则绘制提示文字
            if (mHintTextColor != null) {
                color = mCurHintTextColor;
            }

            layout = mHintLayout;
        }

        mTextPaint.setColor(color);
        mTextPaint.drawableState = getDrawableState();

        canvas.save();

        //3、调整canvas位置,使文本绘制锚点的x、y轴为0
        //3.1、根据TextView设置的padding、shawdow,来对canvas进行裁剪、平移,也是为了能将画布的左上角作为绘制文字的锚点
        int extendedPaddingTop = getExtendedPaddingTop();
        int extendedPaddingBottom = getExtendedPaddingBottom();

        final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
        final int maxScrollY = mLayout.getHeight() - vspace;

        float clipLeft = compoundPaddingLeft + scrollX;
        float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
        float clipRight = right - left - getCompoundPaddingRight() + 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;
        
        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
            voffsetText = getVerticalOffset(false);
            voffsetCursor = getVerticalOffset(true);
        }
        canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);

        //3.2、根据文字direction、gravity来对canvas进行平移,也是为了能将画布的左上角作为绘制文字的锚点
        final int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
        if (isMarqueeFadeEnabled()) {
            if (!mSingleLine && getLineCount() == 1 && canMarquee()
                    && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
                final int width = mRight - mLeft;
                final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
                final float dx = mLayout.getLineRight(0) - (width - padding);
                canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
            }

            if (mMarquee != null && mMarquee.isRunning()) {
                final float dx = -mMarquee.getScroll();
                canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
            }
        }

        final int cursorOffsetVertical = voffsetCursor - voffsetText;

        Path highlight = getUpdatedHighlightPath();
        if (mEditor != null) {
            mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
        } else {
            //4、最后,走到mLayout#draw()来绘制文字
            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
        }

        if (mMarquee != null && mMarquee.shouldDrawGhost()) {
            final float dx = mMarquee.getGhostOffset();
            canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
        }

        canvas.restore();
    }

2.2、绘制与文本相关的

真正开始与文字绘制相关的流程在mLayout#onDraw()中。

Layout的所有子类中,BoringLayout会重写onDraw()方法。

    public void draw(Canvas c, Path highlight, Paint highlightpaint,
                     int cursorOffset) {
        if (mDirect != null && highlight == null) {
            //文字方向确定,并且不需要绘制高亮,直接通过canvas#drawText()绘制文字
            c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
        } else {
            //和其他Layout子类一样走父Layout#draw()方法
            super.draw(c, highlight, highlightpaint, cursorOffset);
        }
    }

因此真正的文字绘制流程还是在父Layout#draw()方法中。

    public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
            int cursorOffsetVertical) {
        final long lineRange = getLineRangeForDraw(canvas);
        int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
        if (lastLine < 0) return;

        //1、绘制背景
        drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
                firstLine, lastLine);
        //2、绘制文字
        drawText(canvas, firstLine, lastLine);
    }

2.2.1、绘制文本背景

首先需要绘制与文本相关的背景,包括:LineBackgroundSpanhighlight

    public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
            int cursorOffsetVertical, int firstLine, int lastLine) {
        //1、处理LineBackgroundSpan
        if (mSpannedText) {
            ......

            if (mLineBackgroundSpans.numberOfSpans > 0) {
                ......
                for (int i = firstLine; i <= lastLine; i++) {
                    //一行一行处理
                    ......

                    for (int n = 0; n < spansLength; n++) {
                        LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
                        //调用lineBackgroundSpan中重写的drawBackground()绘制
                        lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
                                ltop, lbaseline, lbottom,
                                buffer, start, end, i);
                    }
                }
            }
            mLineBackgroundSpans.recycle();
        }

      	//2、绘制highlight
        if (highlight != null) {
            if (cursorOffsetVertical != 0) {
                //纵向上移动canvas,去掉highlight占空间,也是为了能将画布的左上角作为绘制文字的锚点
                canvas.translate(0, cursorOffsetVertical);
            }
            canvas.drawPath(highlight, highlightPaint);
            if (cursorOffsetVertical != 0) {
                canvas.translate(0, -cursorOffsetVertical);
            }
        }
    }

2.2.2、绘制文本

终于到了绘制文本阶段,这里的代码我们详细跟一下。我们可以看到,他会先去查找是否有影响到文本尺寸、位置属性的Span、tab符、缩进,来判断这些文字是否需要特殊处理(即根据这几样来重新确定文字绘制的锚点),如果不需要复杂处理,就会直接调用canvas#drawText(),否则会通过TextLine来处理。

    public void drawText(Canvas canvas, int firstLine, int lastLine) {
       int previousLineBottom = getLineTop(firstLine);
       int previousLineEnd = getLineStart(firstLine);
       ParagraphStyle[] spans = NO_PARA_SPANS;
       int spanEnd = 0;
       final TextPaint paint = mWorkPaint;
       paint.set(mPaint);
       CharSequence buf = mText;

       Alignment paraAlign = mAlignment;
       TabStops tabStops = null;
       boolean tabStopsIsInitialized = false;

       TextLine tl = TextLine.obtain();

       for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
           //一行一行绘制
           int start = previousLineEnd;
           previousLineEnd = getLineStart(lineNum + 1);
           final boolean justify = isJustificationRequired(lineNum);
           //去掉换行符和空格,只保留文字
           int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
           paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
           paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));

           //当前行的top = 上一行的bottom
           int ltop = previousLineBottom;
           //当前行的bottom = 下一行的top
           int lbottom = getLineTop(lineNum + 1);
           previousLineBottom = lbottom;
           //baseline = 当前行的bottom-当前行的top
           int lbaseline = lbottom - getLineDescent(lineNum);

           int dir = getParagraphDirection(lineNum);
           int left = 0;
           int right = mWidth;

           //1、处理span标志
           if (mSpannedText) {
               Spanned sp = (Spanned) buf;
               int textLength = buf.length();
               boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');

               // New batch of paragraph styles, collect into spans array.
               // Compute the alignment, last alignment style wins.
               // Reset tabStops, we'll rebuild if we encounter a line with
               // tabs.
               // We expect paragraph spans to be relatively infrequent, use
               // spanEnd so that we can check less frequently.  Since
               // paragraph styles ought to apply to entire paragraphs, we can
               // just collect the ones present at the start of the paragraph.
               // If spanEnd is before the end of the paragraph, that's not
               // our problem.
               if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) {
                   spanEnd = sp.nextSpanTransition(start, textLength,
                                                   ParagraphStyle.class);
                   spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class);

                   //段落对齐方式
                   paraAlign = mAlignment;
                   for (int n = spans.length - 1; n >= 0; n--) {
                       if (spans[n] instanceof AlignmentSpan) {
                           paraAlign = ((AlignmentSpan) spans[n]).getAlignment();
                           break;
                       }
                   }

                   tabStopsIsInitialized = false;
               }

               //画LeadingMargin
               final int length = spans.length;
               boolean useFirstLineMargin = isFirstParaLine;
               for (int n = 0; n < length; n++) {
                   if (spans[n] instanceof LeadingMarginSpan2) {
                       int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount();
                       int startLine = getLineForOffset(sp.getSpanStart(spans[n]));
                       // if there is more than one LeadingMarginSpan2, use
                       // the count that is greatest
                       if (lineNum < startLine + count) {
                           useFirstLineMargin = true;
                           break;
                       }
                   }
               }
               for (int n = 0; n < length; n++) {
                   if (spans[n] instanceof LeadingMarginSpan) {
                       LeadingMarginSpan margin = (LeadingMarginSpan) spans[n];
                       if (dir == DIR_RIGHT_TO_LEFT) {
                           margin.drawLeadingMargin(canvas, paint, right, dir, ltop,
                                                    lbaseline, lbottom, buf,
                                                    start, end, isFirstParaLine, this);
                           right -= margin.getLeadingMargin(useFirstLineMargin);
                       } else {
                           margin.drawLeadingMargin(canvas, paint, left, dir, ltop,
                                                    lbaseline, lbottom, buf,
                                                    start, end, isFirstParaLine, this);
                           left += margin.getLeadingMargin(useFirstLineMargin);
                       }
                   }
               }
           }

           //2、判断是否有tab
           boolean hasTab = getLineContainsTab(lineNum);
           // Can't tell if we have tabs for sure, currently
           if (hasTab && !tabStopsIsInitialized) {
               if (tabStops == null) {
                   tabStops = new TabStops(TAB_INCREMENT, spans);
               } else {
                   tabStops.reset(TAB_INCREMENT, spans);
               }
               tabStopsIsInitialized = true;
           }

           //3、计算绘制的x坐标
           Alignment align = paraAlign;
           //根据段落的对齐方式判断text绘制的对齐方式
           //左对齐、右对齐、居中
           if (align == Alignment.ALIGN_LEFT) {
               align = (dir == DIR_LEFT_TO_RIGHT) ?
                   Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
           } else if (align == Alignment.ALIGN_RIGHT) {
               align = (dir == DIR_LEFT_TO_RIGHT) ?
                   Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
           }
           
           int x;
           final int indentWidth;
           //根据对齐方式,计算intentWidth缩进
           //结合对齐方式+缩进 计算绘制的x轴坐标
           if (align == Alignment.ALIGN_NORMAL) {
               if (dir == DIR_LEFT_TO_RIGHT) {
                   indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
                   x = left + indentWidth;
               } else {
                   indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
                   x = right - indentWidth;
               }
           } else {
               int max = (int)getLineExtent(lineNum, tabStops, false);
               if (align == Alignment.ALIGN_OPPOSITE) {
                   if (dir == DIR_LEFT_TO_RIGHT) {
                       indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
                       x = right - max - indentWidth;
                   } else {
                       indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
                       x = left - max + indentWidth;
                   }
               } else { // Alignment.ALIGN_CENTER
                   indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
                   max = max & ~1;
                   x = ((right + left - max) >> 1) + indentWidth;
               }
           }

           //4、绘制
           Directions directions = getLineDirections(lineNum);
           if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
               //不需要特殊处理,即正常方向、没有span标志、没有tab、不需要调整
               //直接绘制text
               canvas.drawText(buf, start, end, x, lbaseline, paint);
           } else {
               //否则,通过调用TextLine绘制text
               tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops,
                       getEllipsisStart(lineNum),
                       getEllipsisStart(lineNum) + getEllipsisCount(lineNum));
               if (justify) {
                   tl.justify(right - left - indentWidth);
               }
               tl.draw(canvas, x, ltop, lbaseline, lbottom);
           }
       }

       //5、回收TextLine
       TextLine.recycle(tl);
   }

2.2.2.1、绘制简单文本

对于简单的不需要额外处理的文本,会直接交给canvas#drawText()方法绘制。

    public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
           @NonNull Paint paint) {
       super.drawText(text, start, end, x, y, paint);
   }

canvas#drawText()会直接调用其父类BaseCanvas#drawText()方法

    public void drawText(@NonNull char[] text, int index, int count, float x, float y,
           @NonNull Paint paint) {
       //处理异常情况
       if ((index | count | (index + count) |
               (text.length - index - count)) < 0) {
           throw new IndexOutOfBoundsException();
       }
       throwIfHasHwBitmapInSwMode(paint);
       nDrawText(mNativeCanvasWrapper, text, index, count, x, y, paint.mBidiFlags,
               paint.getNativeInstance());
   }

通过注释可知绘制文本需要知道四个重要信息:

  • 要绘制的文本:text
  • 要绘制的文本区间:通过start、end来确定
  • 绘制的起始锚点:根据x、y坐标确定
  • 画笔:paint

其中:

  • x:绘制的起始x轴坐标,根据对齐方向和缩进来确定
  • y:绘制的起始y轴坐标,即段落的baseLine

BaseCanvas#drawText()方法中通过调用native方法执行绘制。

    private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
            int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint,
            long nativePrecomputedText);

2.2.2.2、绘制复杂文本

对于spantab等需要特殊处理的情况,Layout#draw()都交由TextLine处理了。

    void draw(Canvas c, float x, int top, int y, int bottom) {
        float h = 0;
        final int runCount = mDirections.getRunCount();
        for (int runIndex = 0; runIndex < runCount; runIndex++) {
            final int runStart = mDirections.getRunStart(runIndex);
            if (runStart > mLen) break;
            //这一行能绘制的文字个数 = Math.min(这一行应该绘制的文字个数, 去掉其他因素占用的空间后剩下的空间中所能绘制的文字个数)
            final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
            final boolean runIsRtl = mDirections.isRunRtl(runIndex);

            int segStart = runStart;
            //如果存在空格,就需要减少这一行能够绘制的文字个数
            for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
                if (j == runLimit || charAt(j) == TAB_CHAR) {
                    h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom,
                            runIndex != (runCount - 1) || j != mLen);

                    if (j != runLimit) {  // charAt(j) == TAB_CHAR
                        h = mDir * nextTab(h * mDir);
                    }
                    segStart = j + 1;
                }
            }
        }
    }
    private float drawRun(Canvas c, int start,
            int limit, boolean runIsRtl, float x, int top, int y, int bottom,
            boolean needWidth) {

        if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
            float w = -measureRun(start, limit, limit, runIsRtl, null);
            //如果方向是从右向左的,需要重新计算绘制的x坐标
            handleRun(start, limit, limit, runIsRtl, c, x + w, top,
                    y, bottom, null, false);
            return w;
        }

        return handleRun(start, limit, limit, runIsRtl, c, x, top,
                y, bottom, null, needWidth);
    }
    private float handleRun(int start, int measureLimit,
            int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
            int bottom, FontMetricsInt fmi, boolean needWidth) {

        if (measureLimit < start || measureLimit > limit) {
            throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
                    + "start (" + start + ") and limit (" + limit + ") bounds");
        }

        // Case of an empty line, make sure we update fmi according to mPaint
        if (start == measureLimit) {
            final TextPaint wp = mWorkPaint;
            wp.set(mPaint);
            //根据上述逻辑可知, fmi=null
            if (fmi != null) {
                expandMetricsFromPaint(fmi, wp);
            }
            return 0f;
        }

        //1、处理span
        //判断是否有Span,需要根据span来计算高宽等属性
        ////主要有两种Span:
        ////- MetricAffectingSpan,会影响文字的size属性
        ////- CharacterStyle,会影响文字的显示属性
        final boolean needsSpanMeasurement;
        if (mSpanned == null) {
            needsSpanMeasurement = false;
        } else {
            mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
            mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
            needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
                    || mCharacterStyleSpanSet.numberOfSpans != 0;
        }

        if (!needsSpanMeasurement) {
            //如果不需要计算Span,可以直接去绘制文字
            final TextPaint wp = mWorkPaint;
            wp.set(mPaint);
            //处理连字符
            wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit()));
            wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
            return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
                    y, bottom, fmi, needWidth, measureLimit, null);
        }

        // Shaping needs to take into account context up to metric boundaries,
        // but rendering needs to take into account character style boundaries.
        // So we iterate through metric runs to get metric bounds,
        // then within each metric run iterate through character style runs
        // for the run bounds.
        final float originalX = x;
        for (int i = start, inext; i < measureLimit; i = inext) {//一个字符一个字符处理
            final TextPaint wp = mWorkPaint;
            wp.set(mPaint);

            inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
                    mStart;
            int mlimit = Math.min(inext, measureLimit);

            ReplacementSpan replacement = null;

            //1.1、处理会影响文字的size属性的MetricAffectingSpan
            for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
                // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
                // empty by construction. This special case in getSpans() explains the >= & <= tests
                if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit)
                        || (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;

                boolean insideEllipsis =
                        mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j]
                        && mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd;
                final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
                if (span instanceof ReplacementSpan) {
                    //如果ReplacementSpan所在的位置,是被设置为省略了,就不处理这个ReplacementSpan了
                    replacement = !insideEllipsis ? (ReplacementSpan) span : null;
                } else {
                    //否则,就调用MetricAffectingSpan子类中重写的updateDrawState()方法,修改text的textSize等属性
                    span.updateDrawState(wp);
                }
            }

            //如果需要处理replacement,就调用handleReplacement()方法
            //handleReplacement()方法实际是调用ReplacementSpan子类中重写的draw()方法进行绘制
            if (replacement != null) {
                x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
                        bottom, fmi, needWidth || mlimit < measureLimit);
                continue;
            }

            //1.2、处理影响文本属性的CharacterStyle类型的span
            final TextPaint activePaint = mActivePaint;
            activePaint.set(mPaint);
            int activeStart = i;
            int activeEnd = mlimit;
            final DecorationInfo decorationInfo = mDecorationInfo;
            mDecorations.clear();
            for (int j = i, jnext; j < mlimit; j = jnext) {
                jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
                        mStart;

                final int offset = Math.min(jnext, mlimit);
                wp.set(mPaint);
                for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
                    // Intentionally using >= and <= as explained above
                    if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
                            (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;

                    final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
                    span.updateDrawState(wp);
                }

                extractDecorationInfo(wp, decorationInfo);

                //用activeStart和activeEnd来记录,span属性应用到第几个字符
                if (j == i) {
                    // First chunk of text. We can't handle it yet, since we may need to merge it
                    // with the next chunk. So we just save the TextPaint for future comparisons
                    // and use.
                    activePaint.set(wp);
                } else if (!equalAttributes(wp, activePaint)) {
                    //当遇到span属性与上一个字符的span属性不同时,就先处理上一段属性应用的字符区间
                    //然后更新activeStart到这个不同的属性的字符位置
                    activePaint.setStartHyphenEdit(
                            adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
                    activePaint.setEndHyphenEdit(
                            adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
                    x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
                            top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
                            Math.min(activeEnd, mlimit), mDecorations);

                    activeStart = j;
                    activePaint.set(wp);
                    mDecorations.clear();
                } else {
                    // The present TextPaint is substantially equal to the last TextPaint except
                    // perhaps for decorations. We just need to expand the active piece of text to
                    // include the present chunk, which we always do anyway. We don't need to save
                    // wp to activePaint, since they are already equal.
                }

                activeEnd = jnext;
                if (decorationInfo.hasDecoration()) {
                    final DecorationInfo copy = decorationInfo.copyInfo();
                    copy.start = j;
                    copy.end = jnext;
                    mDecorations.add(copy);
                }
            }
            //2、处理剩下的字符
            activePaint.setStartHyphenEdit(
                    adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
            activePaint.setEndHyphenEdit(
                    adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
            x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
                    top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
                    Math.min(activeEnd, mlimit), mDecorations);
        }

        return x - originalX;
    }

终于到了绘制文本的地方了。

    private float handleText(TextPaint wp, int start, int end,
            int contextStart, int contextEnd, boolean runIsRtl,
            Canvas c, float x, int top, int y, int bottom,
            FontMetricsInt fmi, boolean needWidth, int offset,
            @Nullable ArrayList<DecorationInfo> decorations) {

        if (mIsJustifying) {
            wp.setWordSpacing(mAddedWidthForJustify);
        }

        //获取FontMetrics
        if (fmi != null) {
            expandMetricsFromPaint(fmi, wp);
        }

        // No need to do anything if the run width is "0"
        if (end == start) {
            return 0f;
        }

        float totalWidth = 0;

        final int numDecorations = decorations == null ? 0 : decorations.size();
        if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
            totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
        }

        if (c != null) {
            final float leftX, rightX;
            //再次按照文本方向,计算绘制锚点的x坐标
            if (runIsRtl) {
                leftX = x - totalWidth;
                rightX = x;
            } else {
                leftX = x;
                rightX = x + totalWidth;
            }

            //1、如果有背景色,先绘制背景
            if (wp.bgColor != 0) {
                int previousColor = wp.getColor();
                Paint.Style previousStyle = wp.getStyle();

                wp.setColor(wp.bgColor);
                wp.setStyle(Paint.Style.FILL);
                c.drawRect(leftX, top, rightX, bottom, wp);

                wp.setStyle(previousStyle);
                wp.setColor(previousColor);
            }

            //2、绘制文字
            drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
                    leftX, y + wp.baselineShift);

            if (numDecorations != 0) {
                for (int i = 0; i < numDecorations; i++) {
                    final DecorationInfo info = decorations.get(i);

                    final int decorationStart = Math.max(info.start, start);
                    final int decorationEnd = Math.min(info.end, offset);
                    float decorationStartAdvance = getRunAdvance(
                            wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
                    float decorationEndAdvance = getRunAdvance(
                            wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
                    final float decorationXLeft, decorationXRight;
                    if (runIsRtl) {
                        decorationXLeft = rightX - decorationEndAdvance;
                        decorationXRight = rightX - decorationStartAdvance;
                    } else {
                        decorationXLeft = leftX + decorationStartAdvance;
                        decorationXRight = leftX + decorationEndAdvance;
                    }
                    
                    //3、绘制下划线
                    if (info.underlineColor != 0) {
                        drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
                                info.underlineThickness, decorationXLeft, decorationXRight, y);
                    }
                    if (info.isUnderlineText) {
                        final float thickness =
                                Math.max(wp.getUnderlineThickness(), 1.0f);
                        drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
                                decorationXLeft, decorationXRight, y);
                    }

                    if (info.isStrikeThruText) {
                        final float thickness =
                                Math.max(wp.getStrikeThruThickness(), 1.0f);
                        drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
                                decorationXLeft, decorationXRight, y);
                    }
                }
            }

        }

        return runIsRtl ? -totalWidth : totalWidth;
    }
    private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
            int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {

        if (mCharsValid) {
            int count = end - start;
            int contextCount = contextEnd - contextStart;
            c.drawTextRun(mChars, start, count, contextStart, contextCount,
                    x, y, runIsRtl, wp);
        } else {
            int delta = mStart;
            c.drawTextRun(mText, delta + start, delta + end,
                    delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
        }
    }

可以看到最终,我们还是走到了canvas#drawTextRun()方法,而canvas#drawtext()直接走到其父类BaseCanvas#drawTextRun()方法,对一些异常情况做处理后,走到BaseCanvas#nDrawTextRun()方法,即native方法。

    private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
            int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint,
            long nativePrecomputedText);

3、接受软键盘的输入

  • Android为所有的View预留了一个接收软键盘输入的接口类,叫InputConnection。软键盘以InputConnection为桥梁把文字输入、文字修改、文字删除等传递给View。任意View只要重写onCheckIsTextEditor()并返回true,然后重写onCreateInputConnection(EditorInfo outAttrs)返回一个InputConnection的实例,便可以接收软键盘的输入。
  • TextView的软键盘输入接收,是通过EditableInputConnection类来实现的。
  • EditText继承自TextView

4、OnTouchEvent事件处理

  • TextView内部能处理触摸事件的,包括自身的触摸处理、Editor的onTouchEvent、MovementMethod的onTouchEvent。
  • Editor的onTouchEvent主要处理出于编辑状态下的触摸事件,比如点击选中、长按等。
  • MovementMethod则主要负责文本内部有Span的时候的相关处理,比较常见的就是LinkMovementMethod处理ClickableSpan的点击事件。

(二)总结

至此,从setText作为入口,我们已经跟踪完了TextView绘制的完整流程,总结下来就是:

1、首先判断是否需要重新测量布局

  • 如果需要就先测量

  • 如果不需要就直接进行绘制。

2、绘制流程

  • 先从绘制与TextView控件有关,但与文本无关的,如TextView的背景、hint
  • 接着绘制与文本有关的:
    • 首先绘制文本的背景,如LineBackgroundSpanhighlight

    • 接着开始绘制文本。如果是简单文本,即没有spantab、缩进会影响到文本属性和绘制锚点的,就直接通知canvas绘制。如果是复杂文本,即文本中包含Span标记、tab、缩进,就交给TextLine来处理。

      • 首先,处理Span。对于MetricAffectingSpan,如果是ReplacementSpan,就调用其重写的draw()方法进行绘制;如果是其他Span,就调用其内部重写的updateDrawState更改TextPaint中的属性。对于CharacterStylespan,就调用span内部重写的updateDrawState更改TextPaint中的属性。
      • 接着,绘制文本的背景色
      • 然后,绘制文本,即通知canvas绘制
      • 最后,绘制下划线

接着对我们刚开始提出的三个问题作出解答:

1、当我们绘制一行简单的文本,在调用canvas#drawText()之前,如何测量view大小、绘制位置?
我们再来看一下绘制简单文本的地方,直接调用canvas#drawText()方法进行绘制,而这个方法的入参有:

  • text:文本内容

  • start:要绘制的文本内容的区间的开始位置

  • end:要绘制的文本内容的区间的结束位置

  • x:绘制锚点的x坐标

  • y:绘制锚点的y坐标

  • paint:画笔

1.1、首先,针对view的文本大小,当我们调用TextView#setText()方法时,其内部是通过调用Paint#setText()方法,最终走到native方法。

    public void setTextSize(int unit, float size) {
        if (!isAutoSizeEnabled()) {
            setTextSizeInternal(unit, size, true /* shouldRequestLayout */);
        }
    }
    private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) {
        ......
        setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()),
                shouldRequestLayout);
    }
    private void setRawTextSize(float size, boolean shouldRequestLayout) {
        if (size != mTextPaint.getTextSize()) {
            //调用paint方法
            mTextPaint.setTextSize(size);

            if (shouldRequestLayout && mLayout != null) {
                // Do not auto-size right after setting the text size.
                mNeedsAutoSizeText = false;
                nullLayouts();
                requestLayout();
                invalidate();
            }
        }
    }
    @CriticalNative
    private static native void nSetTextSize(long paintPtr, float textSize);

对于简单的不需要额外处理的文本,会直接交给canvas#drawText()方法绘制。

    public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
           @NonNull Paint paint) {
       super.drawText(text, start, end, x, y, paint);
   }

1.2、调用此方法的入参处,可以看到,x = x,y = lbasekline

canvas.drawText(buf, start, end, x, lbaseline, paint);

从下面的代码可以看出,x是根据对齐方式和缩进来计算的。

            int x;
            final int indentWidth;
            //根据对齐方式,计算intentWidth缩进
            //结合对齐方式+缩进 计算绘制的x轴坐标
            if (align == Alignment.ALIGN_NORMAL) {
                if (dir == DIR_LEFT_TO_RIGHT) {
                    indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
                    x = left + indentWidth;
                } else {
                    indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
                    x = right - indentWidth;
                }
            } else {
                int max = (int)getLineExtent(lineNum, tabStops, false);
                if (align == Alignment.ALIGN_OPPOSITE) {
                    if (dir == DIR_LEFT_TO_RIGHT) {
                        indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
                        x = right - max - indentWidth;
                    } else {
                        indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
                        x = left - max + indentWidth;
                    }
                } else { // Alignment.ALIGN_CENTER
                    indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
                    max = max & ~1;
                    x = ((right + left - max) >> 1) + indentWidth;
                }
            }

而baseLine则是

    //baseline = 当前行的bottom-当前行的top
    int lbaseline = lbottom - getLineDescent(lineNum);

2、当我们绘制多行文本时,如何确定需要换行?如何在第一个问题的基础上确定后面几行的绘制位置?
2.1、首先,Layout#draw()方法中通过getLineRangeForDraw()方法再到getLineCount()方法拿到文本行数

    public long getLineRangeForDraw(Canvas canvas) {
        int dtop, dbottom;

        synchronized (sTempRect) {
            if (!canvas.getClipBounds(sTempRect)) {
                // Negative range end used as a special flag
                return TextUtils.packRangeInLong(0, -1);
            }

            dtop = sTempRect.top;
            dbottom = sTempRect.bottom;
        }

        final int top = Math.max(dtop, 0);
        final int bottom = Math.min(getLineTop(getLineCount()), dbottom);

        if (top >= bottom) return TextUtils.packRangeInLong(0, -1);
        return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom));
    }

在父Layout中,getLineCount()是一个抽象方法,在他的子类中分别实现。

public abstract int getLineCount();

例如,BoringLayout只考虑单行:

    @Override
    public int getLineCount() {
        return 1;
    }

而,StaticLayout中返回了mLineCount,而mLineCount是在StaticLayout#generate()时,通过LineBreaker根据段落信息计算的。【generate()方法在创建Layout时被调用】

    @Override
    public int getLineCount() {
        return mLineCount;
    }
   /* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
        .......

        mLineCount = 0;
        ......

        final LineBreaker lineBreaker = new LineBreaker.Builder()
                .setBreakStrategy(b.mBreakStrategy)
                .setHyphenationFrequency(b.mHyphenationFrequency)
                // TODO: Support more justification mode, e.g. letter spacing, stretching.
                .setJustificationMode(b.mJustificationMode)
                .setIndents(indents)
                .build();

        LineBreaker.ParagraphConstraints constraints =
                new LineBreaker.ParagraphConstraints();

        ......

        for (int paraIndex = 0; paraIndex < paragraphInfo.length; paraIndex++) {
            ......
            //计算行数
            LineBreaker.Result res = lineBreaker.computeLineBreaks(
                    measuredPara.getMeasuredText(), constraints, mLineCount);
            ......
        }

        ......
    }

而LineBreaker中则是调用的native方法

    private static native long nComputeLineBreaks(
            /* non zero */ long nativePtr,

            // Inputs
            @NonNull char[] text,
            /* Non Zero */ long measuredTextPtr,
            @IntRange(from = 0) int length,
            @FloatRange(from = 0.0f) float firstWidth,
            @IntRange(from = 0) int firstWidthLineCount,
            @FloatRange(from = 0.0f) float restWidth,
            @Nullable float[] variableTabStops,
            float defaultTabStop,
            @IntRange(from = 0) int indentsOffset);

2.2、拿到了行数之后,就可以用个for循环来一行一行的处理了。每一行,都需要根据上一行的位置来计算这一行的ltop、lbottom、lbaseline属性,并根据对齐方式、缩进等信息计算x、y锚点。

    @UnsupportedAppUsage
    public void drawText(Canvas canvas, int firstLine, int lastLine) {
        ......

        for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
            //一行一行绘制
            int start = previousLineEnd;
            previousLineEnd = getLineStart(lineNum + 1);
            final boolean justify = isJustificationRequired(lineNum);
            //去掉换行符和空格,只保留文字
            int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
            paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
            paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));

            //当前行的top = 上一行的bottom
            int ltop = previousLineBottom;
            //当前行的bottom = 下一行的top
            int lbottom = getLineTop(lineNum + 1);
            previousLineBottom = lbottom;
            //baseline = 当前行的bottom-当前行的top
            int lbaseline = lbottom - getLineDescent(lineNum);
            
            ......
            canvas.drawText(buf, start, end, x, lbaseline, paint);
            ......
        }

        TextLine.recycle(tl);
    }

3、当一行文本中插入了扩大文本大小的Span时,如何计算整行的高度?如何定位没有被修改大小属性的文本绘制位置?
3.1、我们在源码跟踪中说过,复杂的文本会交给TextLine来处理,在TextLine#handleRun()方法中会处理MetricAffectingSpan、CharacterStyle这种会改变文本属性的Span,而这里会调用Span的子类中的updateDrawState(wp)方法来修改TextPaint中保存的文本大小等绘制属性。例如,AbsoluteSizeSpan中重写的updateDrawState()方法,修改了TextSize。

       @Override
    public void updateDrawState(@NonNull TextPaint ds) {
        if (mDip) {
            ds.setTextSize(mSize * ds.density);
        } else {
            ds.setTextSize(mSize);
        }
    }

3.2、在绘制文本前,会先调用expandMetricsFromPaint()方法更新一下FontMetricsInt信息,

    private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
        final int previousTop     = fmi.top;
        final int previousAscent  = fmi.ascent;
        final int previousDescent = fmi.descent;
        final int previousBottom  = fmi.bottom;
        final int previousLeading = fmi.leading;

        wp.getFontMetricsInt(fmi);

        updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
                previousLeading);
    }

一段文本可能存在有的字体大,有的小,为了不让大的文字发生截断,对于上边界(top、ascent)会取最小值,对于下边界(descent、bottom、leading)会去、取最大值,这样保证最终这一行的高度是按照最大字体的来计算的。

    static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
            int previousDescent, int previousBottom, int previousLeading) {
        fmi.top     = Math.min(fmi.top,     previousTop);
        fmi.ascent  = Math.min(fmi.ascent,  previousAscent);
        fmi.descent = Math.max(fmi.descent, previousDescent);
        fmi.bottom  = Math.max(fmi.bottom,  previousBottom);
        fmi.leading = Math.max(fmi.leading, previousLeading);
    }

3.3、这里我们一定要注意上述规则,在需要处理图文混排,或者需要让文本下对齐等ui时,需要对FontMetrics进行修改。
www.jianshu.com/p/cae5e04cc…


4、如果存在Span修改了文本字体,如何计算混合中的换行?

jaeger.itscoder.com/android/201…

有Span的text,创建的是DynamicLayout,在DynamicLayout#generate()中调用DynamicLayout#reflow()

,对存在span的一行单独创建新的StaticLayout,在StaticLayout#generate()方法里计算换行out

out

三、实践

(一)实现markdown

GitHub - zzhoujay/Markdown: Android平台下的原生Markdown解析器