测试A:你这个横幅有问题啊!正常不是这样显示的...
我:这个不好改啊,之前就发现了,这是偶现的问题,暂时先不改了!!
两天之后...
测试B:我在测另一个需求时发现了这个问题,是不是bug?
我:emmm...应该是有问题的。(看来躲得过初一,躲不过十五啊)
之前在左某个需求的时候根据设计同学的要求,做了一个支持文案上下滚动的横幅,如下图所示:
但是在当文案变成中文之后,有的手机上会出现滚动得不对的问题,变成这样了:
问题点就是,有的时候横幅文案滚动展示没有问题,但是有的时候在某些手机上面就会呈现被截掉的感觉,其实最直观的感觉就是文案被截掉了。
先来说下这个上下滚动的view的实现原理,它实际上就是一个TextView。我们直接看xml的代码:
由于文案需要上下滚动,所以高度不能填wrap_content,这里固定了高度是30dp,字体大小为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);
});
}
到此,问题得以解决!
通过调整,之前出现问题的手机来展示也没有复现问题。