TextView is a text display UI control provided by Android. It is also the first Weight component familiar to Android developers. It can display text, display Html and highlight with Html and Spannable. Also through Autolink email, TEL and other functions of the recognition jump, this article will take you from the point of view of the system source code completely done TextView drawing process.

TextView dependenciesTextView itself is a custom View control, so the analysis of TextView can be directly analyzed in accordance with the commonly used custom View drawing process.

  • onMeasure
  • onLayout
  • onDraw

onMeasure

In onMeasure, according to the general process of custom View, we mainly determine how to determine the width and height of the control itself.

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       int widthMode = MeasureSpec.getMode(widthMeasureSpec);
       int heightMode = MeasureSpec.getMode(heightMeasureSpec);
       int widthSize = MeasureSpec.getSize(widthMeasureSpec);
       int heightSize = MeasureSpec.getSize(heightMeasureSpec);

       int width;
       int height;

		//BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
		UNKNOWN_BORING is a metrics object. Metrics objects are used to determine how text is drawn.
		/ / for the Metrics can refer to the article: https://blog.csdn.net/wanggang514260663/article/details/113845402
       BoringLayout.Metrics boring = UNKNOWN_BORING;
       BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
		
		// Return the alignment of the text, such as LTR and RTL
       if (mTextDir == null) {
           mTextDir = getTextDirectionHeuristic();
       }
		
       int des = -1;
       boolean fromexisting = false;
       final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
               ?  (float) widthSize : Float.MAX_VALUE;
		// If the size measurement method is using a definite value, the measured definite value is used
       if (widthMode == MeasureSpec.EXACTLY) {
           width = widthSize;
       } else {
           if(mLayout ! =null && mEllipsize == null) {
               // Return -1 if the number of lines of text >1, otherwise return the length of the line of text
               des = desired(mLayout);
           }
           // If the number of rows >1
           if (des < 0) {
               //BoringLayout is the simplest implementation of Layout, mainly used for single line text display,
               // Only left-to-right display directions are supported. It is not recommended to use directly in your own development process,
               // If necessary, first use isBoring to determine if the text meets the requirements.
               boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
               If you are responsible for BoringLayout requirements, return the Metrics object.
               // Otherwise null is returned
               if(boring ! =null) { mBoring = boring; }}else {
               fromexisting = true;
           }
           //boring == null indicates the number of rows ==0 and does not support boringLayout
           if (boring == null || boring == UNKNOWN_BORING) {
              //des < 0 indicates the number of lines
               if (des < 0) {
                   des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
                           mTransformed.length(), mTextPaint, mTextDir, widthLimit));
               }
               width = des;
           } else {
               // Measure the width of text
               width = boring.width;
           }
			
           final Drawables dr = mDrawables;
           if(dr ! =null) {
               width = Math.max(width, dr.mDrawableWidthTop);
               width = Math.max(width, dr.mDrawableWidthBottom);
           }

           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) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
                               mHint.length(), mTextPaint, mTextDir, widthLimit));
                   }
                   hintWidth = hintDes;
               } else {
                   hintWidth = hintBoring.width;
               }

               if(hintWidth > width) { width = hintWidth; }}// The width needs to be interspaced
           width += getCompoundPaddingLeft() + getCompoundPaddingRight();
			
           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());

           if(widthMode == MeasureSpec.AT_MOST) { width = Math.min(widthSize, width); }}// The actual width of the text, excluding the padding value
       int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
       int unpaddedWidth = want;
	   // If scrolling is supported, the text width is set to VERY_WIDE = 1024 * 1024;
       if (mHorizontallyScrolling) want = VERY_WIDE;
       int hintWant = want;
       int hintWidth = (mHintLayout == null)? hintWant : mHintLayout.getWidth();if (mLayout == null) {
          // If the Layout object is null, construct a Layout object using makeNewLayout
           makeNewLayout(want, hintWant, boring, hintBoring,
                         width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
       } else {
           final booleanlayoutChanged = (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 booleanmaximumChanged = (mMaxMode ! = mOldMaxMode) || (mMaximum ! = mOldMaximum);// If the text changes
           if (layoutChanged || maximumChanged) {
               if(! maximumChanged && widthChanged) {// Set the width of the Layout to the desired width of want
                   mLayout.increaseWidthTo(want);
               } else {
                   // Recalculate the build Layout
                   makeNewLayout(want, hintWant, boring, hintBoring,
                           width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); }}else {
               // Nothing has changed}}if (heightMode == MeasureSpec.EXACTLY) {
           // Parent has told us how big to be. So be it.
           // If the measurement mode is fixed, the measured value is used
           height = heightSize;
           mDesiredHeightAtMeasure = -1;
       } else {
           The getDesiredHeight method calculates a maximum height based on text and hint
           int desired = getDesiredHeight();
           height = desired;
           mDesiredHeightAtMeasure = desired;
			// If the at_most mode is used, the measured and desired values are relatively small
           if(heightMode == MeasureSpec.AT_MOST) { height = Math.min(desired, heightSize); }}// Return the height without the padding
       int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
       // If there are multiple lines
       if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
          // mlayout. getLineTop returns the height of the specified row from the top to the corresponding height of the specified row
           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);
   }
Copy the code
  • TextView#desired
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
	// If the number of rows >1, -1 is returned
    for (int i = 0; i < n - 1; i++) {
        // Determine if each line ends with a \n newline
        if (text.charAt(layout.getLineEnd(i) - 1) != '\n') {
            return -1; }}for (int i = 0; i < n; i++) {
        If the row width is greater than 0, return 0 otherwise
        max = Math.max(max, layout.getLineWidth(i));
    }
    return (int) Math.ceil(max);
}
Copy the code
  • TextView#makeNewLayout
public void makeNewLayout(int wantWidth, int hintWidth,
                                 BoringLayout.Metrics boring,
                                 BoringLayout.Metrics hintBoring,
                                 int ellipsisWidth, boolean bringIntoView) {
    // Stop running off the lantern effect
    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 booleantestDirChange = mSingleLine && mLayout ! =null
            && (alignment == Layout.Alignment.ALIGN_NORMAL
                    || alignment == Layout.Alignment.ALIGN_OPPOSITE);
    int oldDir = 0;
    if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
    // Determine whether to support the display of body names...
    booleanshouldEllipsize = mEllipsize ! =null && getKeyListener() == null;
    // Determine whether to display the horselight effect
    final booleanswitchEllipsize = mEllipsize == TruncateAt.MARQUEE && mMarqueeFadeMode ! = MARQUEE_FADE_NORMAL; TruncateAt effectiveEllipsize = mEllipsize;if (mEllipsize == TruncateAt.MARQUEE
            && mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
        // Ellipsis display..
        effectiveEllipsize = TruncateAt.END_SMALL;
    }
	
	// Get text paragraph direction
    if (mTextDir == null) {
        mTextDir = getTextDirectionHeuristic();
    }
	/ / for layout
    mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
            effectiveEllipsize, effectiveEllipsize == mEllipsize);
    if (switchEllipsize) {
        TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE
                ? TruncateAt.END : TruncateAt.MARQUEE;
        // For the running light effect, save a separate Layout for running light modemSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize, oppositeEllipsize, effectiveEllipsize ! = mEllipsize); } shouldEllipsize = mEllipsize ! =null;
    mHintLayout = null;
	// Prepare to calculate the Layout used by the Hint
    if(mHint ! =null) {
        if (shouldEllipsize) hintWidth = wantWidth;

        if (hintBoring == UNKNOWN_BORING) {
            hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
                                               mHintBoring);
            if(hintBoring ! =null) { mHintBoring = hintBoring; }}if(hintBoring ! =null) {
            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;
            } 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); }}}// TODO: code duplication with makeSingleLayout()
        if (mHintLayout == null) {
            StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
                    mHint.length(), mTextPaint, hintWidth)
                    .setAlignment(alignment)
                    .setTextDirection(mTextDir)
                    .setLineSpacing(mSpacingAdd, mSpacingMult)
                    .setIncludePad(mIncludePad)
                    .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                    .setBreakStrategy(mBreakStrategy)
                    .setHyphenationFrequency(mHyphenationFrequency)
                    .setJustificationMode(mJustificationMode)
                    .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
            if(shouldEllipsize) { builder.setEllipsize(mEllipsize) .setEllipsizedWidth(ellipsisWidth); } mHintLayout = builder.build(); }}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
            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();
}
Copy the code
  • TextView#makeSingleLayout

This method

protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
            Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
            boolean useSaved) {
    Layoutresult = null;
    //userDynamicLayout = isTextSelectable() || (mSpannable ! = null && mPrecomputed == null);
    // If the above conditions are met, DynamicLayout is used
    if (useDynamicLayout()) {
        final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint,
                wantWidth)
                .setDisplayText(mTransformed)
                .setAlignment(alignment)
                .setTextDirection(mTextDir)
                .setLineSpacing(mSpacingAdd, mSpacingMult)
                .setIncludePad(mIncludePad)
                .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                .setBreakStrategy(mBreakStrategy)
                .setHyphenationFrequency(mHyphenationFrequency)
                .setJustificationMode(mJustificationMode)
                .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
                .setEllipsizedWidth(ellipsisWidth);
        result = builder.build();
    } else {
        if (boring == UNKNOWN_BORING) {
            // Determine whether to use BoringLayout
            boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
            if(boring ! =null) { mBoring = boring; }}//bording ! = null indicates that Boringlayout is used
        if(boring ! =null) {
            if (boring.width <= wantWidth
                    && (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
                if(useSaved && mSavedLayout ! =null) {
                    result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
                            wantWidth, alignment, mSpacingMult, mSpacingAdd,
                            boring, mIncludePad);
                } else {
                    result = BoringLayout.make(mTransformed, mTextPaint,
                            wantWidth, alignment, mSpacingMult, mSpacingAdd,
                            boring, mIncludePad);
                }

                if(useSaved) { mSavedLayout = (BoringLayout) result; }}else if (shouldEllipsize && boring.width <= wantWidth) {
                if(useSaved && mSavedLayout ! =null) {
                    result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
                            wantWidth, alignment, mSpacingMult, mSpacingAdd,
                            boring, mIncludePad, effectiveEllipsize,
                            ellipsisWidth);
                } else{ result = BoringLayout.make(mTransformed, mTextPaint, wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, effectiveEllipsize, ellipsisWidth); }}}}// StaticLayout is used if BoringLayout is not satisfied and DynamicLayout is not satisfied
    if (result == null) {
        StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
                0, mTransformed.length(), mTextPaint, wantWidth)
                .setAlignment(alignment)
                .setTextDirection(mTextDir)
                .setLineSpacing(mSpacingAdd, mSpacingMult)
                .setIncludePad(mIncludePad)
                .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
                .setBreakStrategy(mBreakStrategy)
                .setHyphenationFrequency(mHyphenationFrequency)
                .setJustificationMode(mJustificationMode)
                .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
        if (shouldEllipsize) {
            builder.setEllipsize(effectiveEllipsize)
                    .setEllipsizedWidth(ellipsisWidth);
        }
        result = builder.build();
    }
    return result;
}
Copy the code

onLayout

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 super.onLayout(changed, left, top, right, bottom);
    if (mDeferScroll >= 0) {
        int curs = mDeferScroll;
        mDeferScroll = -1;
        bringPointIntoView(Math.min(curs, mText.length()));
    }
    // Call auto-size after the width and height have been calculated.
    // If autoSize is supported, the text size will be recalculated and set
    autoSizeText();
}
Copy the code

onDraw

protected void onDraw(Canvas canvas) {
		
	// Restart the running light
    restartMarqueeIfNeeded();

     // Draw the background for this view
     super.onDraw(canvas);
		 	
     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;

     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.
         if(dr.mShowing[Drawables.LEFT] ! =null) {
             canvas.save();
             canvas.translate(scrollX + mPaddingLeft + leftOffset,
                     scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
             // Draw drawable to the screendr.mShowing[Drawables.LEFT].draw(canvas); canvas.restore(); }... Omit similar drawable drawing in other directionsint color = mCurTextColor;

     if (mLayout == null) {
         assumeLayout();
     }

     Layout layout = mLayout;

     if(mHint ! =null && mText.length() == 0) {
         if(mHintTextColor ! =null) {
             color = mCurHintTextColor;
         }
         layout = mHintLayout;
     }

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

     canvas.save();
     /* 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;

     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;

     // 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);
     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.0 f);
         }

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

     Path highlight = getUpdatedHighlightPath();
     if(mEditor ! =null) {
         // For editable text, use Editor's onDraw method
         mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
     } else {
         // For non-editable text, use the layer #draw method
         layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
     }
	 / / entertaining diversions
     if(mMarquee ! =null && mMarquee.shouldDrawGhost()) {
         final float dx = mMarquee.getGhostOffset();
         canvas.translate(layout.getParagraphDirection(0) * dx, 0.0 f);
         layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
     }

     canvas.restore();
}
Copy the code