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

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

 

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

两天之后...

 

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

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

 

 

一、问题背景

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

 

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

 

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

 

二、实现原理

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

<SimpleVerticalScrollTextView
    android:id="@+id/tv_top_photo_panel_content"
    android:layout_width="wrap_content"
    android:layout_height="30dp"
    android:layout_marginStart="5dp"
    android:layout_marginLeft="5dp"
    android:layout_marginEnd="15dp"
    android:layout_marginRight="15dp"
    android:clickable="false"
    android:focusable="false"
    android:gravity="center_vertical"
    android:maxWidth="231dp"
    android:textSize="13dp" />

 

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

 

三、问题根源

 

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

 

<TextView
    android:id="@+id/tv_test_temp"
    android:layout_width="match_parent"
    android:layout_height="30dp"
    android:layout_marginTop="50dp"
    android:background="#141414"
    android:text="大家来看数据来大家来看数据来看基里洛夫看了撒娇地方看了撒娇可怜的风景卡拉斯京福利卡时间的反馈啦就是"
    android:textColor="#ffffff"
    android:textSize="13dp" />

小米9(2340x1080像素):

三星C5pro(1920x1080像素):

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

 

通过查阅源码可以发现,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;
    }
}

 

 

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

四、解决方案

 

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

 

所以只需要自定义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);
    });
}

 

到此,问题得以解决!

 

 

五、修改结果

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

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值