Android font, 安卓字体全攻略

一直没有详细地去了解android字体的相关内容, 实际开发的时候总是对设计稿上面字体和其他控件的间距, 字体内部的行距很疑惑, 直接设置好像每次都差几个像素, 简直逼死强迫症患者.
今天我们就一起来看看, 字体的秘密.

推荐阅读:

终于有人把 【移动开发】 从基础到实战的全套视频弄全了

字体结构

要想对攻略字体, 我们先了解清楚字体里面都有些什么.
在分析字体的时候, 我们基本只需要关注垂直方向, 如下图

font结构


垂直方向有5条关键横线.
绿色的横线是最关键的基线(base line), 字体的位置都是相对于基线的, 所以从坐标的角度看, 基线就是y=0的坐标轴.

顶部(ascent)和底部(descent)的红色横线分别为字体的上下"边界".

注意, 虽然说是"边界", 但是实际渲染字体的时候是可能会超过边界的, 我个人理解这两条线算是字体设计者在设计层面给程序提供的一个参考值. 这两条线在设计字体的时候是可以由设计者设置的.

基线到ascent的区域称为升部(ascender), 即图中右侧紫色的区域.
基线到descent的区域称为降部(descender), 即图中右侧蓝色的区域.

黄色的虚线是主线(mean line), 决定无升部的小写字母的高度, 例如e, z, c等. 这个高度又叫x字高(x-height), 也就是图中右侧褐色的区域.

玫红色的虚线(叫啥我也不知道)决定了大写字母的高度. 这个高度又叫大写高度(cap height), 也就是图中右侧绿色的区域.

Em, UPM

除了上述基本结构, 我们还需要搞清楚Em的概念, 有些地方也叫UPM. 简单地说, 字体设计者和程序之间需要有一个抽象的单位来描述字体的高度, 在金属活字印刷时代就有Em来表示一个金属块的高度了, 所以也就沿用了以前Em的说法, 来表示字体的基本单位.

关键知识点: 在Android中, 设置text size的时候, 就是设置1Em的大小.

Em是由字体设计者在设计的时候自行决定将1Em划分成多少份, 然后其他字体中的距离都是用相对Em的大小来描述的.

上面提到的很多值都是在字体设计的时候设置的, 显然这些设置是保存在字体文件当中的, 而在Android中, 最常用的字体文件格式就是.ttf(True Type Font), 所以我们有必要稍微了解一下这种文件.

TTF(True Type Font)文件

TTF简单地说就是一个标准, 用来统一字体的描述方式.
我们的目的不是为了设计字体, 只是希望搞清楚, 字体当中的设置是怎样影响字体在Android TextView中的显示的, 尤其想搞清楚如何根据字体文件计算垂直方向上字体占用的空间.

注意, 接下来很多关于Ascent和Descent的结论都是通过代码实测得到, 能力有限, 并没有弄清楚其中的原理, 希望知道的朋友可以评论补充 :P

分析字体文件设置, 我们需要一个工具来查看这些.ttf文件, 我这里用的是FontForge, 用这个软件打开Android的默认字体Roboto Regular, 看看其中的字体信息.
打开文件后, 选择Element ー> Font Info打开字体信息面板

FontForge


先看看General选项

Em信息


上图可以看出, Roboto中, 把1Em分成了2048份,

实际上, 大部分ttf字体都是把1Em分成了2048份. 可能也有部分字体会分成4096份.

这里还会看到Ascent和Descent的值, 不过经过实测, 这两个并不是真正在Android中用到的Ascent和Descent.(我也很崩溃...这部分的资料很少, 并没有深究这其中究竟有什么不同)

真正在Android中的Ascent和Descent值需要看OS/2选项

字体信息面板


实测结论就是, 红框中的这两个值才是Android中的Ascent和Descent.

图中顶部的Win Ascent和Win Descent是表示所有字中最高和最低的边界, 但是这两个值并不能对应上Android中的值, 原因不明...

在这图中也能看到x-height和cap height的值.
那么这个1900和-500是什么意思呢?

像素计算

要计算字体的高度, 需要记住以下几点:

  1. 设置text size的时候是设置1Em的值
  2. Roboto把1Em分成了2048份
  3. 在Roboto中, Ascent为1900, Descent为-500
  4. 在字体中, 基线(base line)是y=0的坐标轴
    根据1, 2两点, 可以知道, 1份的值是(textSize / 2048) px, 假设text size是2048px, 那么1份就是1px.
    而1900表示Ascent在基线上方, 距离是1900份. -500表示Descent在基线的下方, 距离是500份.
    所以理论上, 如果在字体的text size是2048px, 那么对于这份Roboto Regular字体来说
ascender = 2048px / 2048 * 1900 = 1900px
// 同理
cap height = 1456px
x-height = 1082px
descender = 500px
总高度 = ascender + descender = 1900px + 500px = 2400px

随便打开一个软件, 使用Roboto Regular字体在文本框中输入一段文字, 很容易就能验证这个结论是正确的, 下图是使用Sketch验证的截图

Sketch 2048px


基线为0, 左侧可以看到各条线距离基线的距离, 右侧可以看到文本框总高度为2400px, 和计算值一致.

那么在Android的TextView中显示是不是也是这样呢?

Android TextView中的字体结构

在Android中实测得到的各个区域的值也是一致的, 但是字体的高度却不等于TextView的高度, 如下图

Android font结构

粉红色就是TextView的背景色, 可以看到在Ascent和Descent之外分别还有一点距离才到TextView的边缘, 也就是右侧使用橙色方块标出的fontPadding.

看到这个fontPadding, 不禁有几点疑问

  1. 这个fontPadding是什么东西? 有什么用?
  2. 这两个距离是由谁加上去的? 是字体设计者还是Android自己?
  3. 还有我们最关心的问题, 这两个距离的值怎么计算?

我们一个一个问题来看.

font padding

设计字体的时候设置的Ascent和Descent我认为只是一个参考值, 因为世界上的除了字母和数字外还有其他一些字体, 例如顶部有变音符的, 艺术字体这类需要占用额外空间的字体, 所以font padding就是这个额外空间, 来确保所有字体都能显示在区域内.

实际上, 上面提到的, ttf文件中的Win-Ascent和Win-Descent就是这个作用, 但是和Android中实际读取到的值并不一致.

那么这两个值怎么算? 我目前找到的办法是通过代码, 利用Paint#getFontMetrics获取这两个值.

FontMetrics

先简单介绍下这个类, 包含了5个变量

  1. top: 即上边界, 因为在Android中, y轴正方向是向下的, 而基准线是y=0, 所以这个值是一个负数.
  2. ascent: 字体文件中设置的Ascent值(即上文提到的在FontForge中查看到的HHead Ascent), 也是负数, 理由同上
  3. descent: 字体文件中设置的Descent值(即上文提到的在FontForge中查看到的HHead Descent), 正数
  4. bottom: 下边界, 正数
  5. leading: 两行之间, 上一行的bottom和下一行的top的间距, 然而这个值总是0, 可以忽略.
    更具体的说明可以看看这个回答 Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics

而顶部的font padding就是|top - ascent|, 底部的font padding就是bottom - ascent

我们来实测以下, 通过以下方法读取字体的相关值

public static void printFontMetrics(Context context, @FontRes int fontRes, int emSize) {
    Paint paint = new Paint();
    // 设置字体, 使用兼容库来通过font资源id获取Typeface实例
    paint.setTypeface(ResourcesCompat.getFont(context, fontRes));
    // 把字体大小设置成em size方便查看
    paint.setTextSize(emSize);
    FontMetrics metrics = paint.getFontMetrics();
    Log.d("metrics",
        "top = " + metrics.top +
            ", ascent = " + metrics.ascent +
            ", descent = " + metrics.descent +
            ", bottom = " + metrics.bottom +
            ", leading = " + metrics.leading);
}

对于Rotobo Regular, 调用

// 从上面可以知道Rotobo Regular的em size是2048
printFontMetrics(context, R.font.roboto_regular, 2048);

输出为

D/metrics: top = -2163.0, ascent = -1900.0, descent = 500.0, bottom = 555.0, leading = 0.0

ascentdescent的值和我们从FontForge中查看ttf文件得到的值一样, 由于坐标系的不同, 符号相反.

但是topbottom我并没有找到规律, 希望知道的朋友指教一下.

不过不影响结论, 当textSize=2048的时候, 上面的Android font结构图中的fontPadding, 顶部的值是2163 - 1900 = 263, 底部的值是550 - 500 = 55, 可以自行截图验证, 得到以上值之后, 我们就可以通过计算得到字体的上下font padding了

// Rotobo Regular字体
topFontPadding = textSzie * (2163 - 1900) / 2048
bottomFontPadding = textSize * (550 - 500) / 2048

同时还能知道字体的实际高度

// Rotobo Regular字体
height = textSize * (2163 + 550) / 2048 = textSize * 1.3247

那么为什么是由topbottom决定字体的高度的呢? 那么我们就要看TextView的实现了, 而对于普通的文本, 绘制是由android.text.BoringLayout负责的.

BoringLayout

决定文本高度的关键代码在于init方法, 其实很简单, 不看下面的代码也没关系

void init(CharSequence source,
    TextPaint paint, int outerwidth,
    Alignment align,
    float spacingmult, float spacingadd,
    BoringLayout.Metrics metrics, boolean includepad,
    boolean trustWidth) {
    int spacing;
    // 忽略非重点代码
    // metrics虽然不是FontMetrics, 但含义一致
    // spacing就是字体单行所占高度
    // mDesc就是字体的下边界
    if (includepad) {
        spacing = metrics.bottom - metrics.top;
        mDesc = metrics.bottom;
    } else {
        spacing = metrics.descent - metrics.ascent;
        mDesc = metrics.descent;
    }

    mBottom = spacing;

    // 忽略非重点代码
    // 记录上下font padding
    if (includepad) {
        mTopPadding = metrics.top - metrics.ascent;
        mBottomPadding = metrics.bottom - metrics.descent;
    }
}

逻辑很简单, 关键在includepad, 这个值其实就是android:includeFontPadding的值, 这个值默认是true的, 所以默认情况下

Android中的字体高度是|bottom| + |top|, 而普通软件(例如word, Sketch或者其他设计软件)中, 字体高度使用的是|descent| + |ascent|, 所以Android中的字体在垂直方向上总是比设计稿的多占一点空间.

分析到这里, 解决方案也很明显了

对于普通的字体, 要完美复刻设计稿的字体高度, 应该把android:includeFontPadding设置为false

当然你也可以手动计算这个font padding, 然后做偏移.
不过这个值默认为true是有原因的, 因为这个距离是为了保证
字体中所有"符号"都能显示完全, 因此对于特殊的字体, 如果把这个值设为false, 有可能导致部分字母显示不全, 例如Heavenly Font, 对比如下

Heavenly Font


右侧是把android:includeFontPadding设置为false后的情况, 部分字母显示不完整.
因此使用这个方法前先确定下字体的能够正常显示, 不过实际上大部分常规字体都不需要这个额外空间的, 大部分情况下还是能够放心使用的.

注意, 对于指定的字体文件不支持的文字, 例如使用英文字体文件输入中文, 样式会使用系统默认字体的样式, 但是空间计算的时候还是会按照指定的字体文件的参数来计算, 而不是默认字体的参数.

行距

行距就是相邻两行的基线之间的距离.

默认行距的实际值等于字体设置中的|Descent| + |Aescent|

例如对于Roboto Regular来说, textSize为2048px时, 行距为500 + |-1900| = 2400px

在Android的TextView中, 可以通过android:lineSpacingExtraandroid:lineSpacingMultiplier修改行距. 其中lineSpacingExtra默认值为0, lineSpacingMultiplier默认值为1, 有以下公式

行距=默认行距 * lineSpacingMultiplier + lineSpacingExtra

希望大家看完, 都能了解清楚字体在Android中, 占用高度的计算规则, 如有纰漏, 欢迎评论

关注Android技术的更多详细信息,我这边给大家分享一个福利:

福利1 免费直播课程

《腾讯课堂Android高级开发工程师系列直播》

适听人群:Android初、中、高级开发工程师

3.17-3.23 连续7天每晚8点准时直播,持续进行

3月17日:实现安全可靠的Android网络连接

3月18日:设计模式应该如何运用到Android项目开发中

3月19日:组件化架构演进之路—路由框架原理与实现

3月20日:图片加载框架如何选型及原理分析

3月21日:基于Android一个小时实现人脸追踪

3月22日:架构师教你选择一个适合自己的app的架构

3月23日:Hook源码实现阿里无闪烁换肤

福利2 Android开发资料包

该资料包中主要包括「Java语言进阶与Android相关技术核」、「2)App开发框架知识体系(app亦对象)」、「360° Android app全方位性能调优」、「Android前沿技术」、「NDK 模块开发」等内容,全方位扩充你的知识体系。

想要参与Android进阶免费系列直播课

以及获取Android开发工程师资料包的同学,

点击加入:加入

免费课程,名额有限,先到先得~~

猜你喜欢

转载自blog.csdn.net/weixin_43901866/article/details/88670523