Original Zhanghao Baidu App technology

background

Android screen fragmentation is serious, and various screen resolutions emerge in endlessly. It is the common goal of Baidu App R&D team and visual team to display the same effect on the screen with different resolutions.

In the Android development of Baidu App, TextView’s line-spacing screen adaptation has been entangled between r&d and vision for a long time

Let’s explore a simple and elegant TextView line spacing adaptation.

Analysis of the

First, analyze the reason why the upstream spacing of TextView is inconsistent in different devices. The UI team of Baidu App uses Sketch for UI design and UI review, so the following font size measurement in this paper is completed with Sketch.

Let’s start with a simple XML layout:

<TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Although the actual layout of this view depends on other properties in its parent and any sibling views. Although..."
        android:textSize="16dp"/>
Copy the code

This code is run on models with different resolutions, and the line spacing of each model is measured using Sketch as follows:

Next change the size of textSize to 24dp and see what Mate20 looks like:

<TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Although the actual layout of this view depends on other properties in its parent and any sibling views. Although..."
        android:textSize="24dp"/>
Copy the code

The measurement results of different font size and line spacing on the same device (Mate 20) are shown in the figure below:

Specifically, the larger the font size, the larger the line spacing. This is annoying, because when the size changes, the line spacing is affected, and the line spacing has to be adjusted to match the size, which adds extra work.

The XML layout does not have the lineSpacingExtra/lineSpacingMultiplier property, so where do the line spacing measurements come from?

This is because the visual definition of line spacing is inconsistent with the Android definition of line spacing. It is very simple to define the line spacing visually: use Sketch to input text of the same size in the two adjacent lines of text, and draw a rectangle with the height of the text. For example, in the design drawing of 1080P and Density =3, if the text size is 16dp, the height of the rectangle is set to 48px. The space between the two rectangular boxes is the line space of the text, as can be seen from the measurement renderings above.

That is, even if the lineSpacingExtra/lineSpacingMultiplier property is not set, there is still some line spacing from a visual perspective.

What causes the line spacing measured visually without the lineSpacingExtra/lineSpacingMultiplier property? The following is a detailed analysis of TextView source code, first look at the following figure:

/**
 * Class that describes the various metrics for a font at a given text size.
 * Remember, Y values increase going down, so those values will be positive,
 * and values that measure distances going up will be negative. This class
 * is returned by getFontMetrics().
 */
public static class FontMetrics {
    /**
     * The maximum distance above the baseline for the tallest glyph in
     * the font at a given text size.
     */
    public float   top;
    /**
     * The recommended distance above the baseline for singled spaced text.
     */
    public float   ascent;
    /**
     * The recommended distance below the baseline for singled spaced text.
     */
    public float   descent;
    /**
     * The maximum distance below the baseline for the lowest glyph in
     * the font at a given text size.
     */
    public float   bottom;
    /**
     * The recommended additional space to add between lines of text.
     */
    public float   leading;
}
Copy the code

The code explains the meaning of each field of the font measurement information in great detail, so you can read the comments without explaining too much. TextView computes the details of the coordinate information for each line of text in the Out () method of the StaticlayOut.java class, as follows:


private int out(final CharSequence text, final int start, final int end, int above, int below,
        int top, int bottom, int v, final float spacingmult, final float spacingadd,
        final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
        final boolean hasTab, final int hyphenEdit, final boolean needMultiply,
        @NonNull final MeasuredParagraph measured,
        final int bufEnd, final boolean includePad, final boolean trackPad,
        final boolean addLastLineLineSpacing, final char[] chs,
        final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
        final floattextWidth, final TextPaint paint, final boolean moreChars) { final int j = mLineCount; Final int off = j * mColumns; // Final int off = j * mColumns; final int want = off + mColumns + TOP; // A one-dimensional array that holds the calculated coordinates of the TextView lines. int[] lines = mLines; final int dir = measured.getParagraphDir(); // This provides an external interface to modify the font metrics.if(chooseHt ! = null) { fm.ascent = above; fm.descent = below; fm.top = top; fm.bottom = bottom;for (int i = 0; i < chooseHt.length; i++) {
            if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
                ((LineHeightSpan.WithDensity) chooseHt[i])
                        .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
            } else{ chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm); }} // Get the modified font metric property above = fm.ascent; below = fm.descent; top = fm.top; bottom = fm.bottom; }if (firstLine) {
        if (trackPad) {
            mTopPadding = top - above;
        }

        if(includePad) {// If the current line is the first line of TextView, the above(ascent) value is replaced with top. above = top; } } int extra;if (lastLine) {
        if (trackPad) {
            mBottomPadding = bottom - below;
        }

        if(includePad) {// If the current line is the last line of TextView, below(Descent) uses bottom instead. below = bottom; }}if(needMultiply && (addLastLineLineSpacing || ! // The spacingMult variable corresponds to the lineSpacingMultiplier property configuration. // the spacingAdd variable corresponds to the lineSpacingExtra property configuration. double ex = (below - above) * (spacingmult - 1) + spacingadd;if (ex >= 0) {
            extra = (int)(ex + EXTRA_ROUNDING);
        } else{ extra = -(int)(-ex + EXTRA_ROUNDING); }}else{ extra = 0; } line [off + START] = START; lines[off + TOP] = v; lines[off + DESCENT] = below + extra; lines[off + EXTRA] = extra; V += (below-above) + extra; mLineCount++;return v;
}
Copy the code

Some extraneous code has been omitted for space reasons. The key code is commented in detail above, but I won’t explain it here. From line 87, the following two formulas can be obtained:

  • Top coordinate calculation: The top value of the next row = the top value of the row + the row height
  • Row height calculation (excluding first and last rows) : Row height = descent – ascent + row spacing (Descent is positive and ascent is negative)

In order to make it easier for you to understand the line height, I draw the two lines of the baseline and top of each line. The red line is the baseline, the green line is the top line, and the distance between the two adjacent green lines is the line height, as shown in the figure below:

However, when Chinese characters draw the part below the baseline in the DESCENT range, they do not occupy all the space of DESCENT and will leave some distance. The same is true when Chinese characters draw the part above the baseline in the Ascent range. Therefore, the line spacing measured by Sketch is the remaining space after the descent range occupied by the previous Chinese character and the remaining space after the Ascent range occupied by the next Chinese character.

Adaptation scheme

Through the above analysis, we know that the inherent line spacing of TextView is caused by the fact that the Chinese characters drawn do not occupy the full space of Descent and Ascent, and the line spacing varies with different size and resolution. If you can get rid of this line spacing, you can achieve the purpose of adaptation. How do I get rid of it? Let’s look at the TextView and visual definition of the height of a line of text:

  • TextView: line height = descent – ascent (descent is positive, ascent is negative)
  • Visual: Line height = font size (e.g. 16dp text, line height =48px)

This line spacing can be removed by changing the default line height of the TextView to match the visually defined line height. How do I change the default line height of my TextView? When TextView is designed, it provides an interface to modify the line height of TextView. Back to the source code analysis of the TextView above, lines 20 to 39, store the measurement information of the font in the FM variable, and then pass the FM variable through the LineHeightSpan interface, we can modify the line height of the TextView with the help of this LineHeightSpan interface.

The final adaptation scheme is as follows:

Public class ExcludeInnerLineSpaceSpan implements LineHeightSpan {/ / TextView line height private final int mHeight; public ExcludeInnerPaddingSpan(int height) { mHeight = height; } @Override public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Printmetricsint FM) {// Final int originHeight = fm.descent - fm.ascent;if (originHeight <= 0) {
            return; } // Calculate the scale value finalfloatRatio = mHeight * 1.0f/originHeight; // Descent fm.descent = math. round(fm.descent * ratio); Ascent fm.ascent = fm.descent - mHeight; }}Copy the code

LineHeightSpan ExcludeInnerLineSpaceSpan realized interface, this class is used to remove TextView take line spacing. Line 5, the constructor, takes the latest row height as an argument. Line 14 calculates the original row height. Line 19 calculates the ratio of the height of the new row to the height of the original row. Lines 21-23 modify the Ascent and Descent parameters for font metrics based on their proportional values.

Define a TextView and provide a setCustomText() method for the user to call. The code is as follows:

Public class ETextView extends TextView {/** * removes the padding between each line of text ** @param text */ public voidsetCustomText(CharSequence text) {
        if (text == null) {
            return; } // get the visual definition of each line of the lineHeight int lineHeight = (int) getTextSize(); SpannableStringBuilder ssb ;if(text instanceof SpannableStringBuilder) { ssb = (SpannableStringBuilder) text; / / set LineHeightSpan SSB. SetSpan (new ExcludeInnerLineSpaceSpan (lineHeight), 0, the text. The length (), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); }else{ ssb = new SpannableStringBuilder(text); / / set LineHeightSpan SSB. SetSpan (new ExcludeInnerLineSpaceSpan (lineHeight), 0, the text. The length (), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } // Call the systemsetThe Text () methodsetText(ssb); }}Copy the code

ShowCase

This scheme uses the system open API, which is simple and less invasive. It has been launched on the hot discussion page of Baidu App. Comparison of adaptation effect before and after, as shown in the picture below: