BUG FIX有感-深入了解TextView的行间距计算逻辑

测试A:你这个横幅有问题啊!正常不是这样显示的...

BUG FIX有感-深入了解TextView的行间距计算逻辑_第1张图片

 

我:这个不好改啊,之前就发现了,这是偶现的问题,暂时先不改了!!

BUG FIX有感-深入了解TextView的行间距计算逻辑_第2张图片

两天之后...

 

测试B:我在测另一个需求时发现了这个问题,是不是bug?

我:emmm...应该是有问题的。(看来躲得过初一,躲不过十五啊)

 

 

一、问题背景

之前在左某个需求的时候根据设计同学的要求,做了一个支持文案上下滚动的横幅,如下图所示:

 

但是在当文案变成中文之后,有的手机上会出现滚动得不对的问题,变成这样了:

 

问题点就是,有的时候横幅文案滚动展示没有问题,但是有的时候在某些手机上面就会呈现被截掉的感觉,其实最直观的感觉就是文案被截掉了。

BUG FIX有感-深入了解TextView的行间距计算逻辑_第3张图片

 

二、实现原理

先来说下这个上下滚动的view的实现原理,它实际上就是一个TextView。我们直接看xml的代码:

 

由于文案需要上下滚动,所以高度不能填wrap_content,这里固定了高度是30dp,字体大小为13dp,但是为什么只显示两行还是装不完呢?

BUG FIX有感-深入了解TextView的行间距计算逻辑_第4张图片

 

三、问题根源

 

于是我在不同机型上面尝试复现上面的问题,我发现在不同手机上面的表现是不一样的,在有的手机上面会有这个问题,但是有的手机上面就没有这个问题,这时我大概能够明确了,这是跟手机分辨率相关的,于是我找了有问题的手机和没问题的手机显示如下简单的布局,发现真的是显示会不一样:

 

小米9(2340x1080像素):

三星C5pro(1920x1080像素):

看到这里大家可能有疑问,XML里面并没有设置TextView的lineSpacingExtra(行间距)和lineSpacingMultiplier(行间距的倍数),为什么最终显示出来还是会有行间距呢?

BUG FIX有感-深入了解TextView的行间距计算逻辑_第5张图片

 

通过查阅源码可以发现,TextView在度量字体时会使用到FontMetrisInt这个类来保存相关的信息,里面保存了top、ascent、descent、bottom、leading等信息。

public static class FontMetricsInt {
    /**
     * The maximum distance above the baseline for the tallest glyph in
     * the font at a given text size.
     */
    public int   top;
    /**
     * The recommended distance above the baseline for singled spaced text.
     */
    public int   ascent;
    /**
     * The recommended distance below the baseline for singled spaced text.
     */
    public int   descent;
    /**
     * The maximum distance below the baseline for the lowest glyph in
     * the font at a given text size.
     */
    public int   bottom;
    /**
     * The recommended additional space to add between lines of text.
     */
    public int   leading;

    @Override public String toString() {
        return "FontMetricsInt: top=" + top + " ascent=" + ascent +
                " descent=" + descent + " bottom=" + bottom +
                " leading=" + leading;
    }
}

 

BUG FIX有感-深入了解TextView的行间距计算逻辑_第6张图片

 

所以这里就引出了baseline的概念,所谓的baseline就是文字展示时会有一个基准线,引用一张其他人的图,可以比较直观地了解每个属性的作用。所以我们通常理解的一行的行高,指的就是ascent和descent之间的绝对距离,而在绘制如汉字时,文字并不会占满ascent和descent的位置,导致在视觉上感觉字体之间会有行间距。

BUG FIX有感-深入了解TextView的行间距计算逻辑_第7张图片

四、解决方案

 

综上分析可以知道,系统默认计算的ascent和descent的差值往往会比字体大小要大,而行高就是使用descent和ascent之间的差值,所以在即便我们的Textview设置了高度为30dp,字体设置为13dp,它仍然放不下两行文字。为了解决这个问题,我们能不能自定义行高呢?亦或者说自定义descent和ascent之间的距离呢?通过分析源码可以发现是可以的,如下所示,在TextView绘制文字时,会判断设置的CharSequence是否是自定义的LineHeightSpan,如果是的话,就会使用自定义的参数:

BUG FIX有感-深入了解TextView的行间距计算逻辑_第8张图片

 

所以只需要自定义LineHeightSpan,并重写chooseHeight方法即可,然后传入我们想要的行高:

public class CustomLineHeightSpan implements LineHeightSpan {
    // TextView行高
    private final int mHeight;

    public CustomLineHeightSpan(int height) {
        mHeight = height;
    }

    @Override
    public void chooseHeight(CharSequence text, int start, int end,
                             int spanstartv, int lineHeight,
                             Paint.FontMetricsInt fm) {
        // 原始行高
        final int originHeight = fm.descent - fm.ascent;
        if (originHeight <= 0) {
            return;
        }
        // 计算比例值
        final float ratio = mHeight * 1.0f / originHeight;
        // 根据最新行高,修改descent
        fm.descent = Math.round(fm.descent * ratio);
        // 根据最新行高,修改ascent
        fm.ascent = fm.descent - mHeight;
    }
}

 

然后,继承TextView实现方法setCustomText,而不要使用setText方法,因为需要调用setText时传入CustomLineHeightSpan,这里我们设置行高为TextView的高度/行数,可以实现每行的高度平分TextView的高度,当然你也可以设置为自己想要的高度。

public void setCustomText(final CharSequence text) {
    if (text == null) {
        return;
    }
    // 先设置text,避免外部拿不到textview的宽度
    setText(text);
    post(() -> {
        int lineHeight = getMeasuredHeight() / mShowLineCount;
        SpannableStringBuilder ssb;
        if (text instanceof SpannableStringBuilder) {
            ssb = (SpannableStringBuilder) text;
            // 设置LineHeightSpan
            ssb.setSpan(new CustomLineHeightSpan(lineHeight),
                    0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        } else {
            ssb = new SpannableStringBuilder(text);
            // 设置LineHeightSpan
            ssb.setSpan(new CustomLineHeightSpan(lineHeight),
                    0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        // 调用系统setText()方法
        setText(ssb);
    });
}

 

到此,问题得以解决!

BUG FIX有感-深入了解TextView的行间距计算逻辑_第9张图片

 

 

五、修改结果

通过调整,之前出现问题的手机来展示也没有复现问题。

 

 

你可能感兴趣的:(#,2.Android适配方案,android)