问题

做Android开发的人都知道怎么设置文字高度:

1
2
3
4
5
6
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:textSize="20sp"
android:text="Abc" />

可是有多少人注意到,在手机上这个TextView的实际高度是并不是20sp,为什么?

换句话说,如何使TextView文字的尺寸位置与设计稿精确一致?

分析

因手上只有魅蓝Note3(Flyme系统,基于Android 5.1,1090x1920,density=3),以下分析和结论均在此机子上测试,其他版本的原理都是一样的,区别在于字体文件和配置可能不一样。

(待续)

分析的过程就是各种查资料各种尝试,很多人其实只需要知道结论就行了,所以先把结论贴出来:

结论

1. 中文和英文放在同一个TextView会导致高度改变吗?

NO

density=3屏幕下,”夔Madpx”/“s夔Madpx”/“Madpx”对应尺寸50sp/50dp/150px高度都是202px(includePadding)/176px

2. WRAP_CONTENT 的TextView的真实高度 与 textSize 是否成正比例?

YES 无论是否includePadding,无论是否多行

3. 根据 textSize 如何确定单行 WRAP_CONTENT 的TextView的真实高度?

相关参数包括:

  1. 所使用字体(fallback的话不影响)的UPM(Units Per EM)

  2. ascent/descent属性

  3. top/bottom参数

  • 禁止includePadding时, TextView实际占据高度是 (ascent - descent) / UPM * textSize

  • 开启includePadding时, TextView实际占据高度是 (top - bottom) / UPM * textSize

4. includePadding的实际效果?

由于top/bottom对应于字体文件的所有图案的ymax和ymin,所以可以推出下面的结论:

如果设置了includePadding,则所设置字体里,最上和最下的图案能刚好容纳得下

但是,如果发生了fallback,例如设置了英文字体(默认情况),但是出现了中文字符,绘制中文时通过fallback机制用了另一个字体文件的图案,那么,TextView的高度与另一个字体文件无关

所以,如果不指定字体文件(各种ROM的字体不一定一样),includePadding无法保证

includePadding上下空白的高度与top/bottom/ascent/descent有关,只影响第一行和最后一行

以下值基于fm = getPaint().fontMetrics

———— TopPadding = includePadding ? fm.top : fm.ascent
———— Ascent = fm.ascent
– Line1 – Baseline = 0
———— Descent = fm.descent
———— Ascent = fm.ascent
– Line2 – Baseline = 0
———— Descent = fm.descent
———— Ascent = fm.ascent
– Line3 – Baseline = 0
———— Descent = fm.descent
———— BottomPadding = includePadding ? fm.bottom : fm.descent

top和bottom分别对应字体文件参数里的ymax和ymin

top = font.props.ymax
bottom = font.props.ymin

所以

TopPadding = includePadding ? fm.top - fm.ascent: 0
BottomPadding = includePadding ? fm.bottom - fm.descent: 0

5. android中使用的是mac(hhea)的ascender/descender,还是OS/2的winAscent/winDescent,还是TypoAscender/TypoDescender?

从下面实验可以确定android使用的是mac(hhea)对应的ascender/descender

  • ascender/descender和winAscent/winDscent一致 字体DroidSansFallback-flyme-stub.ttf

E/FontSpace: textView3: space=172.851562 top=-121.875000, ascender=-121.875000, descender=18.164062, bottom=18.164062

  • 把ascender/descender修改后的字体DroidSansFallback-flyme-stub-mod-mac.ttf

E/FontSpace: textView4: space=208.593750 top=-121.875000, ascender=-146.484375, descender=29.296875, bottom=18.164062

  • 把winAscent/winDscent修改后的字体DroidSansFallback-flyme-stub-mod-win.ttf

E/FontSpace: textView5: space=172.851562 top=-121.875000, ascender=-121.875000, descender=18.164062, bottom=18.164062

  • 把typoAscender/typoDscender修改后的字体DroidSansFallback-flyme-stub-mod-typo.ttf

E/FontSpace: textView6: space=172.851562 top=-121.875000, ascender=-121.875000, descender=18.164062, bottom=18.164062

6. TextView默认字体是哪个?

Flyme 5.1环境下,没有DroidSansFallback.ttf,不是DroidSansFallback-flyme.ttf,默认尺寸与设置了android:typeface=”sans”一致
从源代码TypefaceImpl.cpp,可以确认安卓5.1的默认字体是写死在代码里的,位置在/system/fonts/Roboto-Regular.ttf,与system_fonts.xml没什么关系,如果该文件加载异常,则默认字体是一个包含空列表List的FontFamily,会造成什么危害未知

1
2
3
const char *fns[] = {
"/system/fonts/Roboto-Regular.ttf",
};

在Flyme5.1环境下,可以确认Roboto-Regular.ttf的fm与默认的fm一致

  • 默认

E/FontSpace: textView1: space=175.781250 top=-159.228516, ascender=-139.160156, descender=36.621094, bottom=41.455078

  • 指定Roboto-Regular.ttf字体

E/FontSpace: textView9: space=175.781250 top=-159.228516, ascender=-139.160156, descender=36.621094, bottom=41.455078

估计安卓系统的默认字体会考虑各种字体Fallback情况下,使用默认字体的上下留白依然不会造成截断情况,所以默认字体的上下留白会显得很宽

7. android的TextView如何渲染文本?

  1. 通过Paint的getFontMetrics获取字体尺寸参数,主要逻辑在Paint.cpp和Skia库的SkPaint.cpp;对文本测量,确定分行处和补充

  2. 对文本测量,确定分行处和补充…的位置,创建StaticLayout/DynamicLayout/BoringLayout

  3. Layout通过Canvas.drawText进行渲染

8. 如果发生Fallback,逻辑怎样?

  • 如果当前的字体文件找不到相应的图案,就会根据fonts.xml/fallback_fonts.xml的配置逐个检查直到找到为止(实现代码可能在Typeface/FontLoader)

  • 如果找到新字体能提供该图案,则根据UPM和TextSize和Baseline确定绘制的位置和尺寸,但不影响TextView的测量高度

9. 如何使TextView的高度“恰好”(几乎)为中文或英文的高度?

  1. 可以自定义只包含极少图案的字体,并设置合适的ascender/descender,如上面的DroidSansFallback-flyme-stub-mod-mac.ttf可以做到50dp的字体恰好WRAP_CONTENT的高度也是50dp
    但这样就要针对特定版本(4.4/5.0/6.0)嵌入不同的字体,并且AOSP/Flyme等各个ROM的默认字体可能不一样

  2. 待续

10. 如何获取textView的fm和文字高度(space)?

1
2
3
TextPaint paint = textView.getPaint();
Paint.FontMetrics fm = new Paint.FontMetrics();
float space = paint.getFontMetrics(fm);

11. SourceHanSansCN-Normal.ttf和NotoSansHans-Regular.otf区别是?

从主要属性上来看,几乎没区别(数字差小于1%),前者字重是Light后者是Normal,但fonts设置的字重两者都是400。

前者有30888个图案,后者…

12. 假定思源字体fontSize = 100%,在不设置typeface/fontfamily的情况下,用的是什么字体?

英文使用Roboto-Regular.ttf绘制,中文会fallback到SourceHanSansCN-Normal.ttf->NotoSansHans-Regular.otf->DroidSansFallback(每个ROM可能不一样)

12.1 单行文字实际占用高度多少?

文字占用高度只跟默认字体有关,UPM=2048

  • 禁止includePadding时, 实际占用高度为(1900 - -500)/2048 = 117.2%

  • 开启includePadding时, 实际占用高度为(2174 - -566)/2048 = 133.8%

12.2 开启includePadding时上下各多占多少空白?

上面多(2174-1900)/2048 = 13.4%,下面多(566-500)/2048 = 3.2%

12.3 单行文字英文dp实际图案高度和上下空白多少?

ymax(d)=1547,ymin(p)=-427
实际占用高度为(1547 - -427)/2048 = 96.4%

  • 禁止includePadding时,距离上边界(1900-1547)/2048=17.2%,距离下边界(-427 - -500)/2048=3.6%,如希望垂直方向精确居中,需补偿下边距17.2%-3.6%=13.6%

  • 开启includePadding时,距离上边界(2174-1547)/2048=30.6%,距离下边界(-427 - -566)/2048=6.8%,如希望垂直方向精确居中,需补偿下边距30.6%-6.8%=23.8%

12.4 单行文字英文M实际图案高度和上下空白多少?

ymax(M)=1467,ymin(M)=-11

实际占用高度为(1467 - -11)/2048 = 72.2%

  • 禁止includePadding时,距离上边界(1900-1467)/2048=21.1%,距离下边界(-11 - -500)/2048=23.9%,如希望垂直方向精确居中,需补偿上边距23.9-21.1%=2.8%

  • 开启includePadding时,距离上边界(2174-1467)/2048=34.5%,距离下边界(-11 - -566)/2048=27.1%,如希望垂直方向精确居中,需补偿下边距34.5%-27.1%=7.4%

12.5 单行文字中文实际图案高度和上下空白多少?

只考虑常用中文情况下,fallback生效的字体是思源字体,因为两者区别极小,所以以SourceHanSansCN-Normal.ttf的值来计算,UPM=1000

常用中文字符的ymax和ymin都是字符’夔’(\u5914),ymax(夔)=842,ymin(夔)=-74

实际占用高度为(842 - -74)/1000 = 91.6%

  • 禁止includePadding时,距离上边界1900/2048-842/1000=8.6%,距离下边界-74/1000 - -500/2048=17.0%,如希望垂直方向精确居中,需补偿上边距17.0-8.6%=8.4%

  • 开启includePadding时,距离上边界2174/2048-842/1000=22.0%,距离下边界-74/1000 - -566/2048=20.2%,如希望垂直方向精确居中,需补偿下边距22.0%-20.2%=1.8%

所以,如果想指定偏大的高度值让英文垂直居中,禁止includePadding更居中,想让中文垂直居中,开启includePadding更居中

12.6 多行中文设置多大的lineExtraSpace才能实现n倍行距?

默认字体中文的高度填充率是91.2%,所以打算实现n倍行距需要补充的额外空白 = n * 91.2 - (100 - 91.2) = 0.912n - 0.088

13. 如何测量设计稿的文字尺寸?

PS/画图上设置的字号跟实际的文字高度不一样,实际高度跟字号、抗锯齿和文字效果有关,但没找到明显规律,想精确还原设计稿,直接测量字符高度就好。