Mobile Development 13 min read

Consistent TextView Line Spacing on Android: Analysis and Adaptation Solution

The article explains why Android TextView line spacing varies across devices due to mismatched FontMetrics calculations, and presents a low‑intrusion fix that uses a custom LineHeightSpan applied via an ETextView subclass to enforce a visual‑consistent line height equal to the text size on all screen resolutions.

Baidu Geek Talk
Baidu Geek Talk
Baidu Geek Talk
Consistent TextView Line Spacing on Android: Analysis and Adaptation Solution

Visual consistency of TextView line spacing across different Android devices is often unsatisfactory, causing extra work for UI review and reducing development efficiency.

Because Android devices have a wide range of screen resolutions, achieving a uniform display effect for text is a common challenge for both development and visual design teams.

Measurements on three sample devices show that the same XML layout with a 16dp text size produces a line spacing of 5px on a 720p device and 9px on a 1080p device, and the spacing grows when the text size is increased to 24dp.

The root cause is the mismatch between the visual definition of line spacing (the height of the bounding box drawn in Sketch) and Android's internal calculation based on FontMetrics (ascent, descent, leading). Visually, line spacing is the empty space between the descent of the upper line and the ascent of the lower line, while Android computes line height as descent - ascent + lineSpacingExtra (multiplied by lineSpacingMultiplier if set).

Key source snippets illustrate this:

/**
 * Class that describes the various metrics for a font at a given text size.
 */
public static class FontMetrics {
    public float top;
    public float ascent;
    public float descent;
    public float bottom;
    public float leading;
}

In StaticLayout.java the out() method calculates the line coordinates, applies LineHeightSpan to modify FontMetricsInt , and finally adds extra spacing:

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,
        ... ) {
    // ...
    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);
            }
        }
        above = fm.ascent;
        below = fm.descent;
        top = fm.top;
        bottom = fm.bottom;
    }
    // calculate extra spacing when lineSpacingMultiplier/Extra are used
    if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
        double ex = (below - above) * (spacingmult - 1) + spacingadd;
        int extra = ex >= 0 ? (int)(ex + EXTRA_ROUNDING) : - (int)(-ex + EXTRA_ROUNDING);
        // store line info
        lines[off + DESCENT] = below + extra;
        lines[off + EXTRA] = extra;
        v += (below - above) + extra;
    }
    // ...
    return v;
}

From the analysis we obtain two useful formulas:

Next line top = current line top + line height.

Line height (excluding first/last line) = descent - ascent + lineSpacing.

To make the TextView line height match the visual definition, a custom LineHeightSpan is introduced that scales the original height to a target value:

public class ExcludeInnerLineSpaceSpan implements LineHeightSpan {
    private final int mHeight;
    public ExcludeInnerLineSpaceSpan(int height) { mHeight = height; }
    @Override
    public void chooseHeight(CharSequence text, int start, int end,
                            int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
        int originHeight = fm.descent - fm.ascent;
        if (originHeight <= 0) return;
        float ratio = mHeight * 1.0f / originHeight;
        fm.descent = Math.round(fm.descent * ratio);
        fm.ascent = fm.descent - mHeight;
    }
}

A convenience subclass ETextView applies this span automatically:

public class ETextView extends TextView {
    /** Set text with custom line height equal to the visual text size */
    public void setCustomText(CharSequence text) {
        if (text == null) return;
        int lineHeight = (int) getTextSize();
        SpannableStringBuilder ssb = (text instanceof SpannableStringBuilder)
                ? (SpannableStringBuilder) text
                : new SpannableStringBuilder(text);
        ssb.setSpan(new ExcludeInnerLineSpaceSpan(lineHeight), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        setText(ssb);
    }
}

The solution uses only public Android APIs, has low intrusion, and has been deployed in Baidu App’s hot‑discussion page. Before‑and‑after screenshots demonstrate that the line spacing becomes consistent across devices of different resolutions and text sizes.

In summary, by redefining the TextView line height through a custom LineHeightSpan , the visual and system definitions of line spacing can be unified, eliminating the extra padding that caused inconsistency.

Mobile DevelopmentuiAndroidcompatibilityTextViewLineSpacing
Baidu Geek Talk
Written by

Baidu Geek Talk

Follow us to discover more Baidu tech insights.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.