问题
做Android开发的人都知道怎么设置文字高度:
|
|
可是有多少人注意到,在手机上这个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的真实高度?
相关参数包括:
所使用字体(fallback的话不影响)的UPM(Units Per EM)
ascent/descent属性
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,会造成什么危害未知
|
|
在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如何渲染文本?
通过Paint的getFontMetrics获取字体尺寸参数,主要逻辑在Paint.cpp和Skia库的SkPaint.cpp;对文本测量,确定分行处和补充
对文本测量,确定分行处和补充…的位置,创建StaticLayout/DynamicLayout/BoringLayout
Layout通过Canvas.drawText进行渲染
8. 如果发生Fallback,逻辑怎样?
如果当前的字体文件找不到相应的图案,就会根据fonts.xml/fallback_fonts.xml的配置逐个检查直到找到为止(实现代码可能在Typeface/FontLoader)
如果找到新字体能提供该图案,则根据UPM和TextSize和Baseline确定绘制的位置和尺寸,但不影响TextView的测量高度
9. 如何使TextView的高度“恰好”(几乎)为中文或英文的高度?
可以自定义只包含极少图案的字体,并设置合适的ascender/descender,如上面的DroidSansFallback-flyme-stub-mod-mac.ttf可以做到50dp的字体恰好WRAP_CONTENT的高度也是50dp
但这样就要针对特定版本(4.4/5.0/6.0)嵌入不同的字体,并且AOSP/Flyme等各个ROM的默认字体可能不一样待续
10. 如何获取textView的fm和文字高度(space)?
|
|
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/画图上设置的字号跟实际的文字高度不一样,实际高度跟字号、抗锯齿和文字效果有关,但没找到明显规律,想精确还原设计稿,直接测量字符高度就好。