让图标和中文真正居中
自动布局里图标和文字都设了垂直居中,中文却总比图标低一点。问题出在字体文件自己的纵向度量上。
- CATEGORY
- 研究
- YEAR
- 2026
- STATUS
- 笔记
- TOOLS
- fonttools / figma / chromium
entry记录
我在给一个 PC 端产品搭设计系统,颜色、字号、间距都做成了设计变量(token),按钮、输入框、导航项都做成了组件。正文统一使用开源的 Noto Sans SC(思源黑体的同源版本),字重只留 Regular 和 Bold。这些组件大多是图标加文字横向排列,用自动布局设为垂直居中;搜索框、菜单项、导航、按钮全是这个结构。
做组件时,图标和文字都设了居中,文字却总比图标低一点;同一段文字分别用微软雅黑和 Noto Sans SC 排出来,后者明显更靠下。读取两段文字的渲染边界,两者的字号、行高、对齐方式完全相同,差别只在字形墨迹在文字框里的位置,原因在字体文件本身。「居中了但看着不正」常常是字体度量的问题,不是布局的问题;先量墨迹的实际位置,再决定改哪里。
字体文件里记录着字体在一行里占多高:基线以上的部分称为上伸(ascender),基线以下的部分称为下伸(descender)。排版软件把多出来的行距平分到上下两头,所以行框的中线位于上伸减下伸的一半处。Noto Sans SC 的上伸是 1160、下伸是 288,上下很不对称;每个字号按 1000 单位算,行框中线比字体给汉字定的上下边界(汉字字面框)的中心高出 56 个单位,也就是字体自己把汉字往下挤了约 0.056 个字号:14px 时偏下约 0.7px,32px 时接近 2px;在 44px 高的按钮里,图标和文字的中线错开一像素,眼睛看得出来。这组数当年按所有字重里字形的最高点和最低点定,为的是各平台行距一致、字形不被裁掉,本来就不管居中;主流开源字体平台后来的中日韩字体规范已改成上伸、下伸围绕汉字字面框对称,但 Noto Sans SC 上架早,没有跟进。
先排除了三条路:设计工具自带的裁掉上下留白的功能(Vertical trim)会把文字框裁到大写字母的高度,行高 token 就失效了,不是通用解法;在组件里把图标下移 1px 只是治标,还要在 token 里多一个「光学偏移」的特殊值,而我要的是图标和文字各自的高度都不变;换一款中文字体也不行,比较了几款主流中文 UI 字体,有的度量同样不对称,有的在两个平台上一个偏上一个偏下,有的授权不许修改,而且换字体会让全站文字重新折行。
决定保留字形,只改字体的上伸和下伸。字形、字宽、字距不变,只挪动汉字在行框里的位置;开源许可证允许修改和再分发,只要改名。具体做法是把字体表里的三组度量(Mac 读的、Windows 读的、排版用的)统一成同一组数,让行框中线正好对准汉字字面框的中心。理论上上伸 1104、下伸 344 最对称,总高也和原版一样。能在源头改,就不在每个组件里补偿:改一次字体,用到它的地方同时生效,token 里也不用塞「光学偏移」这类特殊值。
字体改好上传到设计工具后,文字位置一点都没变;前后上传了十几次,分 8 轮排查。前 6 轮的猜测依次是:字体没装到本机,实际是上传到团队字体,不是本机安装;几个十几 KB、专门用来排查的小字体(诊断字体)生效了,怀疑 17MB 的完整可变字体处理失败,换成静态字重仍无效;完整字库大小的诊断字体也生效了,怀疑名字以「Noto Sans SC」开头被当成了原版,改名仍无效;又猜设计工具按字形内容去重,只改一个看不见的字形,仍无效;第 5 轮改了一个罕用字的字形,那个字显示成标记方块,说明文件确实被用了,度量却像被忽略,方向对了一半;第 6 轮出现转折,名字带 Noto、度量很夸张的那个字体生效了,名字干净、度量正常的那个仍无效,说明原因在度量的数值,不在名字。
第 7 轮改做阶梯测试:一次上传 7 个度量各不相同的小字体,同时测量位置。结果是设计工具和浏览器都会把基线对齐到整像素,但规则不同;7 个诊断字体的实测位置全部符合四舍五入,之前推出的向下取整规则有 4 个对不上。
之前的向下取整规则,是用设计工具自带的 Noto Sans SC 的实测位置加上 2.004 版文件的度量推出来的,根因就是这个错误前提:平台自带的是更早的版本,度量不同。用错的基准推出错的取整规则,又按错的规则选出了 1103 / 300 这组度量;偏偏这组数在设计工具里取整之后,每一档都和平台自带的 Noto 在同一个像素上。于是「位置没变」被读成「度量没生效」,又牵出一串关于名字、文件大小、内容去重的猜测。比较的基准必须当场实测,不能拿别处的数当前提。
纠错靠两件事:一是每一轮有了「这次应该生效了」的判断,我都去设计工具里看一眼,如实记下「还是一样,没有任何变化」,没有这些观察,错的假设会一直被当成结论;二是把逐个改单变量换成一次性的阶梯测试:用夸张的度量、多组并行,取整规则一次就拟合出来,快得多,也不容易被巧合误导。
按实测的两条规则重新搜索,选定上伸 1126、下伸 345。搜索标准依次是:两边基线一致、最大偏差最小、平均偏差最小、总高尽量接近原版。两条规则是:设计工具整体四舍五入;浏览器先把上伸、下伸各自取整,再把剩下的行距平分并向下取整。同一组度量在两边可能差 1px,所以要找一组在规范的 12 组字号里两边都在同一像素上的数。
改完后,规范内 12 组字号与行高的最大偏差从 1.84px 降到 0.56px,方向从统一偏下变成有上有下,两边位置一致。剩下的误差来自基线只能停在整像素上(整像素吸附):比如 13/21 这一档,理想基线在 15.44px,只能取 15 或 16,换什么字体都消不掉。
设计稿里的偏差一直比线上小:平台自带的老版本 Noto 在设计工具里只偏一点,线上浏览器加载的 2.004 版偏得更多,所以问题在设计稿里不显眼,到线上才明显。因此前端必须加载改过的网页字体,否则线上还是原来的偏移。
设计系统里 15 个文字样式的字体族本来就绑在同一个变量上,替换时只改了这个变量的值,所有文字样式和绑了样式的文字层就全切了过去;设计工具里实测 12 组全部按预期就位;字形和字宽没变,没有一处重新折行。为了以后复用,计算过程做成了一个小工具(度量计算器):输入上伸、下伸,逐档显示两边的基线和偏差,也能在一个范围里搜索最优组合。
process对比
8 轮排查
- 1
- 改版与原版位置完全一样 · 猜没装到本机 · 错:是上传到团队字体
- 2
- 十几 KB 的诊断字体生效 · 猜 17MB 可变字体处理失败 · 换静态字重仍无效
- 3
- 完整字库大小的诊断字体也生效 · 猜名字被当成原版 · 改名仍无效
- 4
- 只改一个看不见的字形 · 猜按字形内容去重 · 仍无效
- 5
- 改罕用字字形后显示为标记方块 · 文件被用了、度量被忽略 · 对了一半
- 6
- 名字带 Noto、度量夸张的生效 · 原因在度量数值,不在名字 · 转折点
- 7
- 一次上传 7 个度量不同的小字体 · 测出真实取整规则 · 找到根因
- 8
- 按新规则重算度量 · 12 组全部按预期落位
取整规则
设计工具 基线 = round( 行高/2 + (上伸 − 下伸)/2 × 字号 )
浏览器 基线 = floor( (行高 − round(上伸×字号) − round(下伸×字号)) / 2 ) + round(上伸×字号)度量与偏差
- 原版 Noto Sans SC
- 上伸 1160 · 下伸 288
- 理论对称
- 上伸 1104 · 下伸 344
- 按错误规则选出的
- 上伸 1103 · 下伸 300
- 最终
- 上伸 1126 · 下伸 345
- 原版 · 浏览器
- 平均 0.84px · 最大 1.84px · 全部偏下
- 平台自带 Noto · 设计工具
- 平均 0.45px · 最大 0.88px · 全部偏下(两档例外)
- 调整后 · 两边一致
- 平均 0.27px · 最大 0.56px · 有上有下
result小结
- 结论
- 只改度量、不动字形,规范内 12 组字号 / 行高的最大偏差从 1.84px 降到 0.56px,设计工具和浏览器第一次算出同一个位置。
- 参数
- 上伸 / 下伸 1160 / 288 → 1126 / 345 · 三组度量统一 · 7 个诊断字体拟合取整规则
- 没做完
- 线上必须加载改过的网页字体才会生效;13/21 这类档位的整像素残差,换什么字体都消不掉。
log日志
- 2026-10
- 首次记录