@TOC TextView is a text display UI control provided by Android. It is also the first familiar Weight component for Android developers. It can be used with Html and Spannable to display text, display Html, and highlight. 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.

In the previous TextView drawing process at https://blog.csdn.net/wanggang514260663/article/details/113996117 The onMeasure, onLayout and onDraw of TextView are briefly analyzed. In TextView, there is a class -Layout that runs through the whole process. This section mainly analyzes Layout.

Layout has three main implementation classes

  • StaticLayout

StaticLayout is not allowed to be modified after text has been arranged and laid out.

  • BoringLayout

BoringLayout is the simplest implementation of Layout. It is mainly used for single-line text display and only supports left-to-right display orientation. It is not recommended to use it directly in your own development; if you need to use it, use isBoring first to determine if the text meets your requirements.

  • DynamicLayout

DynamicLayout allows you to modify text after arranging the layout, which updates the text content.

BoringLayout

The class dependency diagram for BoringLayout is shown above.

BoringLayout#isBoring

If you want to use the BoringLayout method, you first need to call the BoringLayout. IsBoring method to determine whether BoringLayout is supported.

/** * Return Metrics if text supports BoringLayout, otherwise null */
public static Metrics isBoring(CharSequence text, TextPaint paint) {
    return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
}
public static Metrics isBoring(CharSequence text, TextPaint paint, TextDirectionHeuristic textDir, Metrics metrics) {
    // Returns the length of the text
    final int textLength = text.length();
    The hasAnyInterestingChars method returns true if the text is \t, \n, or bidi Unicode characters or proxies for Unicode characters
    if (hasAnyInterestingChars(text, textLength)) {
        return null;  // There are some interesting characters. Not boring.
    }
    // Check whether the text direction is from right to left
    if(textDir ! =null && textDir.isRtl(text, 0, textLength)) {
        return null;  // The heuristic considers the whole text RTL. Not boring.
    }
    // Whether it is a Spanned object
    // Style manipulation in text, such as highlighting and clickability, is supported by Spanned
    if (text instanceof Spanned) {
        Spanned sp = (Spanned) text;
        Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
        if (styles.length > 0) {
            return null;  // There are some ParagraphStyle spans. Not boring.
        }
    }
    Metrics fm = metrics;
    if (fm == null) {
        fm = new Metrics();
    } else {
        fm.reset();
    }
    // We will focus on TextLine below
    TextLine line = TextLine.obtain();
    line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
             Layout.DIRS_ALL_LEFT_TO_RIGHT, false.null.0 /* ellipsisStart, 0 since text has not been ellipsized at this point */.0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);
    fm.width = (int) Math.ceil(line.metrics(fm));
    TextLine.recycle(line);
    return fm;
}
Copy the code
  • BoringLayout#hasAnyInterestingChars
private static boolean hasAnyInterestingChars(CharSequence text, int textLength) {
        final int MAX_BUF_LEN = 500;
        final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);
        try {
            for (int start = 0; start < textLength; start += MAX_BUF_LEN) {
                final int end = Math.min(start + MAX_BUF_LEN, textLength);

                // No need to worry about getting half codepoints, since we consider surrogate code
                // units "interesting" as soon we see one.
                // Copy the string to the target array buffer using the corresponding class call getChars based on the type of text
                TextUtils.getChars(text, start, end, buffer, 0);

                final int len = end - start;
                for (int i = 0; i < len; i++) {
                    final char c = buffer[i];
                    / / TextUtils couldAffectRtl (c) to determine if it is unicode bidi algorithm, agent of unicode characters, direct return true
                    if (c == '\n' || c == '\t' || TextUtils.couldAffectRtl(c)) {
                        return true; }}}return false;
        } finally{ TextUtils.recycle(buffer); }}Copy the code

BoringLayout#make

public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,
            Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics,
            boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
    return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics,
                            includePad, ellipsize, ellipsizedWidth);
}
Copy the code
  • BoringLayout#BoringLayout
public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align,
            float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad,
            TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
    /* * It is silly to have to call super() and then replaceWith(), * but we can't use "this" for the callback until the call to * super() finishes. */
    OuterWidth cannot <0. <0 throws IllegalArgumentException
    super(source, paint, outerWidth, align, spacingMult, spacingAdd);

    boolean trust;

    // If ellipSize == null or the ellipsize type is MARQUEE, the width included in the text is returned
    if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
        mEllipsizedWidth = outerWidth;
        mEllipsizedStart = 0;
        mEllipsizedCount = 0;
        trust = true;
    } else {
        // The replaceWith method in the Layout class simply reassigns the parameters in the constructor
        // Textutils. ellipse size is used for calculation and clipping. If the text is within the specified width, the text is returned to the original text. If the text exceeds the specified width, the ellipsis is displayed based on the text direction and TruncateAt type.
        replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true.this),
                    paint, outerWidth, align, spacingMult, spacingAdd);
        mEllipsizedWidth = ellipsizedWidth;
        trust = false;
    }
    init(getText(), paint, align, metrics, includePad, trust);
}
Copy the code
void init(CharSequence source, TextPaint paint, Alignment align,
            BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) {
    int spacing;
	
	// if mDirect == null, use Layout#draw, otherwise use canvas.drawText
    if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
        mDirect = source.toString();
    } else {
        mDirect = null;
    }

    mPaint = paint;
	
    //includePad corresponds to the TextView_includeFontPadding property
	// When set to false, telling computation removes the spacing between top and ascent, and between bottom and Descent, so the font overall takes up less space than when set to true
	// The TextView_includeFontPadding property defaults to true
	if (includePad) {
	    Bottom - mertics. Top is descent from bottom and ascent: : descent is descent from bottom and ascent
	    spacing = metrics.bottom - metrics.top;
	    mDesc = metrics.bottom;
	} else {
	    spacing = metrics.descent - metrics.ascent;
	    mDesc = metrics.descent;
	}
	mBottom = spacing;
	//baseline = mbottom-mdesc, mBottom = mbottom-mdesc, mBottom = mbottom-mdesc, mBottom = mbottom-mdesc

    // Here we calculate and return a maximum line width
    if (trustWidth) {
        mMax = metrics.width;
    } else {
        /* * If we have ellipsized, we have to actually calculate the * width because the width that was passed in was for the * full text, not the ellipsized form. */
        //TextLine is complicated
        TextLine line = TextLine.obtain();
        line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
                 Layout.DIRS_ALL_LEFT_TO_RIGHT, false.null,
                 mEllipsizedStart, mEllipsizedStart + mEllipsizedCount);
        mMax = (int) Math.ceil(line.metrics(null));
        TextLine.recycle(line);
    }
    if(includePad) { mTopPadding = metrics.top - metrics.ascent; mBottomPadding = metrics.bottom - metrics.descent; }}Copy the code

BoringLayout#replaceOrMake

The BoringLayout#replaceOrMake method is similar to the make method in general implementation, except that make builds a new BoringLayout while replaceOrMake reuses it

public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth,
            Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
            boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
    boolean trust;

    if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
        replaceWith(source, paint, outerWidth, align, spacingMult, spacingAdd);

        mEllipsizedWidth = outerWidth;
        mEllipsizedStart = 0;
        mEllipsizedCount = 0;
        trust = true;
    } else {
    	// This method is implemented in Layout and is used to reassign variables.
    	// Textutils. ellipse size is used for calculation and clipping. If the text is within the specified width, the text is returned to the original text. If the text exceeds the specified width, the ellipsis is displayed based on the text direction and TruncateAt type.
        replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true.this),
                paint, outerWidth, align, spacingMult, spacingAdd);

        mEllipsizedWidth = ellipsizedWidth;
        trust = false;
    }

    init(getText(), paint, align, metrics, includePad, trust);
    return this;
}
Copy the code
  • layout#replaceWith
void replaceWith(CharSequence text, TextPaint paint,
                              int width, Alignment align,
                              float spacingmult, float spacingadd) {
    if (width< 0) {
        throw new IllegalArgumentException("Layout: " + width + " < 0");
    }

    mText = text;
    mPaint = paint;
    mWidth = width;
    mAlignment = align;
    mSpacingMult = spacingmult;
    mSpacingAdd = spacingadd;
    mSpannedText = text instanceof Spanned;
}
Copy the code

BoringLayout#draw

As mentioned in the previous article -TextView Drawing process, if the onDraw text of TextView is not editable, use the Layer #draw method. So look at the BoringLayout implementation of the Draw method.

public void draw(Canvas c, Path highlight, Paint highlightpaint,
                 int cursorOffset) {
    // In init, there is a judgment that controls the value of mDirect
    //source instanceof String && align == Layout.Alignment.ALIGN_NORMAL
    if(mDirect ! =null && highlight == null) {
    	//mbottom-mdesc is used to calculate the baselineY position of the drawing line
        c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
    } else {
        // call Layout#draw to draw
        super.draw(c, highlight, highlightpaint, cursorOffset); }}Copy the code

Mbottom-mdesc calculates baselineY, which involves knowledge about Metrics. For knowledge about Metrics, please refer to Canvas. drawText comprehension and FontMetrics text measurement.

TextLine

TextLine was mentioned earlier in the introduction to BoringLayout and will be introduced next.

/**
 * Represents a line of styled text, for measuring in visual order and
 * for rendering.
 *
 * <p>Get a new instance using obtain(), and when finished with it, return it
 * to the pool using recycle().
 *
 * <p>Call set to prepare the instance for use, then either draw, measure,
 * metrics, or caretToLeftRightOf.
 *
 * @hide* /
public class TextLine{... }Copy the code

A quick review of where BoringLayout is used.

. TextLine line = TextLine.obtain(); line.set(paint, text,0, textLength, Layout.DIR_LEFT_TO_RIGHT,
         Layout.DIRS_ALL_LEFT_TO_RIGHT, false.null.0 /* ellipsisStart, 0 since text has not been ellipsized at this point */.0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);
 fm.width = (int) Math.ceil(line.metrics(fm)); TextLine.recycle(line); .Copy the code

In fact, there are only a few methods that TextLine itself uses, so let’s go through them one by one.

TextLine#obtain

As you can see from the TextLine introduction, this method is used to get a TextLine object.

// Create a shared data set of 3 textlines
private static final TextLine[] sCached = new TextLine[3]; . A little codepublic static TextLine obtain(a) {
    TextLine tl;
    synchronized (sCached) {
        for (int i = sCached.length; --i >= 0;) {
            // If there are any available textlines in the collection, use them, and remove them from the collection.
            if(sCached[i] ! =null) {
                tl = sCached[i];
                // remove from the collection
                sCached[i] = null;
                returntl; }}}// If there is no TextLine available in sCached, create a new one.
    tl = new TextLine();
    return tl;
}
Copy the code

TextLine#recycle

Recycle is a method paired with the TextLine#obtain method

/**
 * Puts a TextLine back into the shared pool. Do not use this TextLine once
 * it has been returned.
 * @param tl the textLine
 * @returnNull, as a convenience from clearing References to the Provided * TextLine * Set the TextLine object back to the sCached shared pool */
public static TextLine recycle(TextLine tl) {
   tl.mText = null;
   tl.mPaint = null;
   tl.mDirections = null;
   tl.mSpanned = null;
   tl.mTabs = null;
   tl.mChars = null;
   tl.mComputed = null;
   
   // All three are implementations of SpanSet, which splits Spanned objects from Spanned data and stores them in arrays. This can be accessed through the getNextTransition method.
   //private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
   // new SpanSet
      
       (MetricAffectingSpan.class);
      
   //MetricAffectingSpan is related to the width and height of the text
   tl.mMetricAffectingSpanSpanSet.recycle();
   //private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
   // new SpanSet
      
       (CharacterStyle.class);
      
   //CharacterStyle
   tl.mCharacterStyleSpanSet.recycle();
   //private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
   // new SpanSet
      
       (ReplacementSpan.class);
      
   //ReplacementSpan supports content replacement, as the name indicates
   tl.mReplacementSpanSpanSet.recycle();

   synchronized(sCached) {
       for (int i = 0; i < sCached.length; ++i) {
           // The pool that is not available is then dumped into the shared pool.
           if (sCached[i] == null) {
               sCached[i] = tl;
               break; }}}return null;
}
Copy the code

TextLine#set

/** * initializing a TextLine and one of the works that set the website for use. **@param paint the base paint for the line
 * @param text the text, can be Styled
 * @param start the start of the line relative to the text
 * @param limit the limit of the line relative to the text
 * @param dir the paragraph direction of this line
 * @param directions the directions information of this line
 * @param hasTabs true if the line might contain tabs
 * @param tabStops the tabStops. Can be null
 * @param ellipsisStart the start of the ellipsis relative to the line
 * @param ellipsisEnd the end of the ellipsis relative to the line. When there
 *                    is no ellipsis, this should be equal to ellipsisStart.
 */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,Directions directions, boolean hasTabs, TabStops tabStops,
        int ellipsisStart, int ellipsisEnd) {
    mPaint = paint;
    mText = text;
    mStart = start;
    mLen = limit - start;
    mDir = dir;
    mDirections = directions;
    if (mDirections == null) {
        throw new IllegalArgumentException("Directions cannot be null");
    }
    mHasTabs = hasTabs;
    mSpanned = null;

    boolean hasReplacement = false;
    // Determine whether the type supports Spanned
    if (text instanceof Spanned) {
    	// Become Spanned
        mSpanned = (Spanned) text;
        //replementSpan is the type of span commonly used. As described earlier, SPANSet is all of the spanned data
        // Unblock the span object of the supported type. Unblock the span object, the start position of the SPAN and the end bit of the SPAN
        // spanFlags are stored in arrays. Spanset provides the getNextTransition method for access
        // Data saved in spanset.
        mReplacementSpanSpanSet.init(mSpanned, start, limit);
        The numberOfSpans method is used to get the total number of SPAN objects you split.
        hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
    }

    mComputed = null;
    //PrecomputedText is a new API provided in Android P, PrecomputedTextCompat can
    // Enable the app to perform the most time-consuming part of the text layout work beforehand, even in background threads, to cache the layout results,
    // And return valuable measurement data.
    
    // For more information about PrecomputedText, see article:
    //https://blog.csdn.net/ecjtuhq/article/details/104366104
    if (text instanceof PrecomputedText) {
        // Here, no need to check line break strategy or hyphenation frequency since there is no
        // line break concept here.
        mComputed = (PrecomputedText) text;
        if(! mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) { mComputed =null;
        }
    }

    mCharsValid = hasReplacement;
	// If the text type is Spanned &&SPANset size >0, mCharsValid == true
    if (mCharsValid) {
        if (mChars == null || mChars.length < mLen) {
            mChars = ArrayUtils.newUnpaddedCharArray(mLen);
        }
        // Split text into char data and assign the value to the target mChars
        TextUtils.getChars(text, start, limit, mChars, 0);
        // It should be true
        if (hasReplacement) {
            // Handle these all at once so we don't have to do it as we go.
            // Replace the first character of each replacement run with the
            // object-replacement character and the remainder with zero width
            // non-break space aka BOM. Cursor movement code skips these
            // zero-width characters.
            // Replace the first character of each replacement run with the object replacement character,
            // Replace the rest of the characters with a zero-width uninterrupted space, also known as a BOM
            char[] chars = mChars;
            for (int i = start, inext; i < limit; i = inext) {
                inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
                if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)
                        && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {
                    // transition into a span
                    chars[i - start] = '\ufffc';
                    for (int j = i - start + 1, e = inext - start; j < e; ++j) {
                        chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
                    }
                }
            }
        }
    }
    mTabs = tabStops;
    mAddedWidthForJustify = 0;
    mIsJustifying = false;
	// Start and end positions of ellipsesmEllipsisStart = ellipsisStart ! = ellipsisEnd ? ellipsisStart :0; mEllipsisEnd = ellipsisStart ! = ellipsisEnd ? ellipsisEnd :0;
}
Copy the code

BoringLayout is finished here, StaticLayout and DynamicLayout will be explained in subsequent articles.