我的安卓第一课:Android 字体度量系统(Font Metrics)

2 阅读4分钟

引言

本周在写代码的时候,涉及到一部分跑马灯的实现,其中涉及到使用Canvas绘制字体、获取与调整字体大小等功能的实现。中途还遇到过字体绘制时出现字体超出view高度或低于view底部导致字体绘制不全的问题。故本文将将自己在编码时搜集到的资料进行记录。

推荐fft字体查看软件:FontForge

字体度量相关属性

计算机字体度量系统初认知

upscalemedia-transformed.jpeg

上图是来自于网络的字体度量展示图。其中各部分的描述如下所示

核心基准与范围

单词描述
Baseline(基线)字符排列的基准线,类似书写时的 “底线”,多数字母(如E h)底部对齐此线,j的下伸部分会突破。
Origin(原点)字体度量的参考原点,通常位于基线左侧起始位置,是计算字符位置的基准点。
Bounding rectangle(边界矩形)包围字符的最小矩形,定义单个字符的视觉范围,辅助排版时确定字符间距、碰撞检测。
Line height两行文字基线之间的垂直距离,包含字体本身高度(ascent + descent )和行间距(leading ),控制文本行与行的间隔。

字符垂直度量

单词描述
Ascent(上伸高度)基线到字体中最高字符(如E h的顶部)的距离,体现字符向上延伸的最大范围。
Descent(下伸高度)基线到字体中最低字符(如j的下伸部分)的距离,体现字符向下延伸的最大范围。
Cap height(大写字母高度)基线到大写字母(如E)顶部的距离,反映大写字母的视觉高度,影响字体风格(如衬线体、无衬线体的差异)。
X-height(x 字高)基线到小写字母x(无上下伸的字母)顶部的距离,决定字体 “紧凑感”,是影响可读性的关键参数。

字符间距与倾斜

单词描述
Advancement(字宽推进)一个字符在基线方向上的宽度(含空白间隔),决定字符间的水平间距,排版时控制文字排列密度
Italic angle(倾斜角度)斜体字的倾斜角度,体现字体风格(如意大利体的倾斜度),影响文字的视觉流动感

Android Font Metrics

/**
 * 描述特定文本大小下字体各种度量标准的类。
 * 请记住,Y 值随向下方向递增,因此这些值将为正,
 * 而测量向上距离的值将为负。此类由 getFontMetrics () 方法返回。  
 */
public static class FontMetrics {

    /**
     * 对于给定文本大小的字体,最高字符超出 baseline 的长度。
     */
    public float   top;
    
    /**
     * 对于单行间距文本,推荐的 baseline 以上距离。
     */
    public float   ascent;
    /**
     * 对于单行间距文本,推荐的 baseline 以下距离。
     */
    public float   descent;
    
    /**
     * 对于给定文本大小的字体,最低字符超出 baseline 的长度。
     */
    public float   bottom;
    /**
     * 建议添加在文本行之间的额外间距。(可理解为行间距)。
     */
    public float   leading;

    @Override
    public boolean equals(Object o) { ... }
    @Override
    public int hashCode() { ... }
    @Override
    public String toString() { ... }
}

安卓提供的属性与上图相似,但有点不同。上面的属性中,比较常用的是ascentdescent。(主要,根据我做项目查到的资料来看,top与bottom没看到有用的)。top与bottom则是Android加入的,主要是为了兼容一些特殊字体。安卓实际的字体渲染高度由top与bottom决定。

image.png

image.png

image.png

编码操作

计算字体高度

如果你需要计算字体的高度,可以使用

Paint.FontMetrics fontMetrics = paint.getFontMetrics(); 
// 字体高度 = 上伸部分(ascent 为负) + 下伸部分(descent 为正) 
float fontHeight = fontMetrics.descent - fontMetrics.ascent;

虽然安卓实际中的字体高度由 descent 与 ascent 决定,但实际渲染时则是使用 top 与 bottom,留出了空白,来兼容少数特殊字体。

为什么上面的坐标为负?
因为在度量系统中,并不以元素的左上角为坐标原点。其中 X 依然为元素最左侧,而 Y 则是 baseline 的位置。

计算行间距

行间距表示相邻两行的基线之间的距离。默认行距的实际值等于字体设置中的|Descent| + |Aescent|(leading = 0)。根据上节的图可知,如果你需要计算当前的间距,可以使用

Paint.FontMetrics fontMetrics = paint.getFontMetrics();
// 行高 = 上伸绝对值 + 下伸 + 行间距(leading),leading 是字体建议的行与行之间额外间距
float lineHeight = fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading; 

Canvas绘制文字

Canvas绘制文字的APIcanvas.drawText(str, X, Y, paint)。该API中(X, Y)为绘制起点,同时也是字体的原点的位置。这里的字体原点指的是 baseline 的位置

假设字体所在的View位于(0, 0)。我们希望将字体绘制在view的左上角。则

错误的绘制方式,下面的绘制方式会导致字体只有baseline下的内容可被展示。baseline上面的内容会因为超出view而无法显示

canvas.drawText(str, 0, 0, paint)

image.png

正确的绘制方式

Paint.FontMetrics fontMetrics = paint.getFontMetrics();
canvas.drawText(str, 0, -fontMetrics.ascent, paint)

image.png

参考

www.sohu.com/a/302218138…

juejin.cn/post/724214…

blog.csdn.net/flyeek/arti…

glyphsapp.com/zh/learn/ve…