Deep Exploration of Rich Text in Android TextView

Keywords: Android Attribute github

First, how to use it?

Firstly, the use of rich text in TextView is introduced. There are two main ways to display rich text in TextView. One is to use Spannable String class, the other is to write rich text directly into HTML form.

SpannableString

Spannable String is Android's built-in class for rich text processing. It basically covers all rich text representations you can think of, fonts, colors, pictures, click events. It's very powerful. Not much to say, go directly to the code:

Example

//Set the first three characters of Hello World to red and the background to blue
SpannableString textSpanned1 = new SpannableString("Hello World");
textSpanned1.setSpan(new ForegroundColorSpan(Color.RED),
        0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textSpanned1.setSpan(new BackgroundColorSpan(Color.BLUE),
        0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text1.setText(textSpanned1);

//Set the first three character fonts of Hello World in italics
SpannableString textSpanned2 = new SpannableString("Hello World");
textSpanned2.setSpan(new StyleSpan(Typeface.ITALIC),
        0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text2.setText(textSpanned2);

//Set the first three characters of Hello World to be underlined
SpannableString textSpanned3 = new SpannableString("Hello World");
textSpanned3.setSpan(new UnderlineSpan(),
        0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text3.setText(textSpanned3);

//Setting the first three characters of Hello World with a click event
SpannableStringBuilder textSpanned4 = new SpannableStringBuilder("Hello World");
ClickableSpan clickableSpan = new ClickableSpan() {
    @Override
    public void onClick(View view) {
        Toast.makeText(MainActivity.this, "Hello World", Toast.LENGTH_SHORT).show();
    }
};
textSpanned4.setSpan(clickableSpan,
        0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//Note: This sentence must be added at this time, otherwise the click event will not take effect.
text4.setMovementMethod(LinkMovementMethod.getInstance());
text4.setText(textSpanned4);

setSpan()

void setSpan (Object what, int start, int end, int flags)
parameter Explain
what style
start Character Index at Style Start
end Character Index at Style End
flags New Insert Character Settings

flags:

Value Explain
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE Neither before nor after
Spanned.SPAN_EXCLUSIVE_INCLUSIVE Not included in the front, but included in the back.
Spanned.SPAN_INCLUSIVE_EXCLUSIVE The former includes, the latter does not.
Spanned.SPAN_INCLUSIVE_INCLUSIVE Both front and back include

This flags may not be understood. It indicates whether this style works on other strings inserted before or after this string. For example:

SpannableStringBuilder textSpannedBuilder1 = new SpannableStringBuilder();
SpannableString textSpanned11 = new SpannableString("Hello");
textSpanned11.setSpan(new BackgroundColorSpan(Color.BLUE), 0, textSpanned11.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString textSpanned12 = new SpannableString("World");
text1.setText(textSpannedBuilder1.append(textSpanned11).append(textSpanned12));

SpannableStringBuilder textSpannedBuilder2 = new SpannableStringBuilder();
SpannableString textSpanned21 = new SpannableString("Hello");
textSpanned21.setSpan(new BackgroundColorSpan(Color.BLUE), 0, textSpanned21.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
SpannableString textSpanned22 = new SpannableString("World");
text2.setText(textSpannedBuilder2.append(textSpanned21).append(textSpanned22));

SpannableStringBuilder textSpannedBuilder3 = new SpannableStringBuilder();
SpannableString textSpanned31 = new SpannableString("Hello");
textSpanned31.setSpan(new BackgroundColorSpan(Color.BLUE), 0, textSpanned21.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
SpannableString textSpanned32 = new SpannableString("World");
textSpanned32.setSpan(new BackgroundColorSpan(Color.GREEN), 0, 3, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
text3.setText(textSpannedBuilder3.append(textSpanned31).append(textSpanned32));

  • In text1, "Hello" flags are SPAN_EXCLUSIVE_EXCLUSIVE, and the "World" inserted after it shows normal, no background.

  • In text2, "Hello" flags are SPAN_EXCLUSIVE_INCLUSIVE, which then inserts the background of "World" into blue.

  • Note that text3, where "Hello" is the same as text2, and a part of the character "World" is set to green. Obviously, this part of the character shows green. This shows that although the SPAN_EXCLUSIVE_INCLUSIVE attribute is set, the flags attribute is overwritten as long as the following string sets the same style.

Spannable String and Spanable String Builder

In the example above, we use the Spannable StringBuilder class. What's the difference between this class and Spannable String? In fact, just think about the difference between String and StringBuilder. Spannable String needs to specify a good string when it is created, and then it can't be changed. Spannable StringBuilder can use append() method to add rich text to existing ones. Add new rich text.

HTML

Next, the usage of HTML is introduced. Actually, HTML is simpler than Spannable String. We just need to write HTML according to our usual habit and add various labels to rich text to display in TextView. Let's take a look at an example.

Example

String htmlText1 = "<b>Hello World</b>";
text1.setText(Html.fromHtml(htmlText1));

String htmlText2 = "<font color='#ff0000'>Hello World</font>";
text2.setText(Html.fromHtml(htmlText2));

String htmlText3 = "<i><a href='https://Gavinli 369.github.io/'> My blog </a> </i>.“;
text3.setMovementMethod(LinkMovementMethod.getInstance());
text3.setText(Html.fromHtml(htmlText3));

Does it feel much simpler than Spannable String? In fact, the Html class is still processed in Spannable. We will see how it is implemented later.

HTML tags supported by TextView

Label Explain
font Setting fonts and colors
big Large font
small fine print
i Italics
b bold
tt Equal width font
br Line wrapping (no empty lines between rows)
p Line wrapping (there are empty lines between lines)
a link
img image

In fact, TextView supports more than these HTML tags. Later, we will show you the source code of HTML class, which contains all HTML tags supported by TextView. It is also important to note that different tags may have the same effect, such as strong tags and b tags, the effect of bold fonts, which you can see when you see HTML class source code, will know why.

2. Deep Exploration

Familiar with usage, we are going to explore further. Next let me take you deep into the TextView source code and unveil the mystery of rich text display of TextView...

Representation of Spannable

First of all, to understand how TextView's rich text is implemented, we need to understand how Android represents rich text internally. This is the inheritance system of Spannable related classes:

The Spannable String class and the SpannableStringBuilder class we used previously both implement the Spannable interface, and the setSpan() method is declared here. Look at the SpannableString class on the left, which inherits from a virtual class, SpannableStringInternal, and the rich text implementation method we are looking for is hidden in this class. Let's explore it.

void setSpan(Object what, int start, int end, int flags) {

    //Some irrelevant code has been omitted

    mSpans[mSpanCount] = what;
    mSpanData[mSpanCount * COLUMNS + START] = start;
    mSpanData[mSpanCount * COLUMNS + END] = end;
    mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
    mSpanCount++;
}

Looking at the setSpan() method, which we are most familiar with, we find that the setSpan method mainly changes the values of three global variables, mSpans, mSpanData and mSpanCount. We find the declarations of these variables:

private String mText;
private Object[] mSpans;
private int[] mSpanData;
private int mSpanCount;

private static final int START = 0;
private static final int END = 1;
private static final int FLAGS = 2;
private static final int COLUMNS = 3;

In fact, there are two arrays in Spannable String International. One mSpanData represents the first and last index and flags of the style, and the other mSpans represents the corresponding style.
The representation of mSpanData is interesting. It packages three variables together and only needs to take the value of the offset address corresponding to the variable when it is obtained. You can see the representation of this mSpanData array:

Spannable String Builder is much simpler. It stores the four variables in four arrays directly. There is no introduction to it here. Interested students can explore it by themselves.

Drawing Rich Text

Now that we know how rich text is represented, we will draw rich text. Let's first look at the onDraw() method of TextView.

protected void onDraw(Canvas canvas) {

    //...

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

    Layout layout = mLayout;

    //Save a lot of code

    final int cursorOffsetVertical = voffsetCursor - voffsetText;

    Path highlight = getUpdatedHighlightPath();
    if (mEditor != null) {
        mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
    } else {
        layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
    }

}

This involves the composition of the TextView class. When you look at the source code of TextView, you will find more than 10,000 lines of code. In fact, this is Android to facilitate the expansion of TextView, wrote a lot of code that should not belong to TextView here. You can see the source code of EditText, a total of more than 100 lines, most of the logic is handed directly to TextView. And this mEditor is used to process editable TextView. Whether it looks directly below, TextView gives the details of drawing to this mLayout to do. So what is this mLayout?

protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
        Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
        boolean useSaved) {
    Layout result = null;
    if (mText instanceof Spannable) {
        result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
                alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
                mBreakStrategy, mHyphenationFrequency,
                getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
    }

    //A large piece of code is omitted, including the instantiation of two other Layout classes, BoringLayout and StaticLayout

    return result;
}

After some searching, I found that mLayout was created here. I omitted the code created by two other Layout subclasses, BoringLayout and StaticLayout. In fact, these three classes call their parent Layout's draw() method directly, and draw() class calls drawText() method for text drawing. So let's go directly into Text () method:

Layout: Paragraph Format Computation

public void drawText(Canvas canvas, int firstLine, int lastLine) {
    TextLine tl = TextLine.obtain();

    // Draw the lines, one at a time.
    // The baseline is the top of the following line minus the current line's descent.
    for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {

        //Some calculation of paragraph format is omitted here. Alignment Span, Leading MarginSpan are all here.
        Alignment align = paraAlign;
        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;
        if (align == Alignment.ALIGN_NORMAL) {
            if (dir == DIR_LEFT_TO_RIGHT) {
                x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
            } else {
                x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
            }
        } else {
            int max = (int)getLineExtent(lineNum, tabStops, false);
            if (align == Alignment.ALIGN_OPPOSITE) {
                if (dir == DIR_LEFT_TO_RIGHT) {
                    x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
                } else {
                    x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
                }
            } else { // Alignment.ALIGN_CENTER
                max = max & ~1;
                x = ((right + left - max) >> 1) +
                        getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
            }
        }

        paint.setHyphenEdit(getHyphen(lineNum));
        Directions directions = getLineDirections(lineNum);
        if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab) {
            // XXX: assumes there's nothing additional to be done
            canvas.drawText(buf, start, end, x, lbaseline, paint);
        } else {
            tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops);
            tl.draw(canvas, x, ltop, lbaseline, lbottom);
        }
        paint.setHyphenEdit(0);
    }

}

Here Layout has calculated the paragraph format of each line, the number of empty front, center or right, while the specific text display style is handled by the TextLine class.

TextLine: Text Drawing

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) {

    //...

    if (c != null) {
        //...

        //Text background
        if (wp.bgColor != 0) {
            int previousColor = wp.getColor();
            Paint.Style previousStyle = wp.getStyle();

            wp.setColor(wp.bgColor);
            wp.setStyle(Paint.Style.FILL);
            c.drawRect(x, top, x + ret, bottom, wp);

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

        if (wp.underlineColor != 0) {
            // kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
            float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();

            int previousColor = wp.getColor();
            Paint.Style previousStyle = wp.getStyle();
            boolean previousAntiAlias = wp.isAntiAlias();

            wp.setStyle(Paint.Style.FILL);
            wp.setAntiAlias(true);

            wp.setColor(wp.underlineColor);
            c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);

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

        //Drawing text, calling drawTextRun() of canvas directly inside
        drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
                x, y + wp.baselineShift);
    }

    return runIsRtl ? -ret : ret;
}

Here TextView draws the text to be displayed on canvas, and some careful students may find out how the parameters of TextPaint come from. So we have to go back to the method handleRun() which calls it to find the answer:

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) {
    //...
    for (int i = start, inext; i < measureLimit; i = inext) {
        for (int j = i, jnext; j < mlimit; j = jnext) {
            //...
            wp.set(mPaint);
            for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
                //...
                CharacterStyle span = mCharacterStyleSpanSet.spans[k];
                //The key is to call the updateDrawState() method corresponding to Style and set the TextPaint property directly.
                span.updateDrawState(wp);
            }
            x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
                    top, y, bottom, fmi, needWidth || jnext < measureLimit, offset);
        }
    }
    return x - originalX;
}

Is there a feeling that you can see the sky through the clouds, so the whole process of using Spannable String to draw rich text in TextView is in front of everyone?

Drawing Rich Text Using Html

Some students have to say that there are also Html class parsing? In fact, Html internal or text into Spannable, the principle is the same, I take a paragraph here for you to see:

if (tag.equalsIgnoreCase("strong")) {
    end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
} else if (tag.equalsIgnoreCase("b")) {
    end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
} else if (tag.equalsIgnoreCase("em")) {
    end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("cite")) {
    end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("dfn")) {
    end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("i")) {
    end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
} else if (tag.equalsIgnoreCase("big")) {
    end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
} else if (tag.equalsIgnoreCase("small")) {
    end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
}

This also answers the question of why different labels produce the same effect.

Posted by sdat1333 on Sun, 19 May 2019 03:55:31 -0700