『TextLayout』Font 与大小计算

前端作为一个展示平台,打交道最多的就是文字和图形。其实,文字也是一种图形。在查阅资料后,大概总结了下:字符布局范围,文字绘制到屏幕上的流程,自定义 inputView等。

环境信息
macOS 10.12.4
Xcode 8.4
iOS 10.4


字体

字体排版不是一天两天能说清楚的,这里推荐两本书:《西文字体》与《Thinking with Type》,外加一个小视频,里面介绍了字体中的一些基本概念。

glyph

字形,估计用英文还要好理解一点:symbol。每个字,都有各种各样的字形,如「A」:

那么,是否代表着字母和字形之间,有某种对应关系呢?这也不尽然。

Ligature

连体。如果说,字母和字形是一对多的关系,其实也不对,因为还会存在连体的情况。这使得一个字形,也对应这多个字母:

计算机存储字符的方式为「数字 – 字符编码」的映射表。而 iOS 与 macOS 平台下,均使用 Unicode 编码。它独立于平台,语言等存在,解决了计算机系统中,各种编码方案之间的冲突。除此之外,还提供了应该如何处理上下文,如何断句换行,如何在不同语言间排版,如何格式化数字、时间等解决方案。

typeface

字体。font 和 typeface 翻译过来都是字体,所以为什么说中文理解更麻烦呢。来看看英文解释:

  • typeface: a particular design of type
  • font: a set of type of particular face and size

所以,font 其实是 typeface 和 size 的组合。我们在初始化 UIFont 的时候就能看出,需要指定 font name(这是之后要介绍的 font family),还需要指定 font size。当然,font 也不只是包含这两个信息,接下来提到的 typestyle 也是其中之一。

typestyle

字体样式。一种字体可能会提供多种不同的样式。如,斜体、粗体等。

font family

即同一种字体,不同样式的组合。如,宋体+粗体,宋体+细体,宋体+斜体,它们均为宋体,但是又有着不同的样式,整个组合形式,即宋体的 font family。

综合以上的概念,可以得出如下公式:

字体布局

将文字渲染到界面的过程,即是将 text 生成 glyph,通过 text layout 排版到 text view 的过程。对于英文来说,从 text view 的左上角开始排版,到达右边界后,另起一行,直到布局到右下角,结束。

平时经常需要计算文字大小,用于符合设计图要求。但是,文字的范围,行间距这些到底是什么?有没有更简便的计算方式?先来看看字符之间都有哪些间隙:

图中标的名字,都对应着 UIFont 的属性:

@property(nonatomic, readonly) CGFloat ascender;
@property(nonatomic, readonly) CGFloat descender;
@property(nonatomic, readonly) CGFloat capHeight;
@property(nonatomic, readonly) CGFloat xHeight;
@property(nonatomic, readonly) CGFloat lineHeight;
@property(nonatomic, readonly) CGFloat leading;

所以,要计算一行文本的高度,可以直接调用 lineHeight。而实际调用 UILabel 计算出来的高度,等于 ceil(font.lineHeight),这也是计算方法内部,做的优化。

下面这个图,是单个字母的布局规则,通过它来认识布局中的其他元素:

metrics

单位长度。对于横向布局的字符来说,布局系统会给一个单位间距,也就是途中看到的 Advance width。也就是从 origin 点,到 glyph 真正渲染的距离,这也是与下一个 glyph 之间的间距。在这里,左间距叫做 left-side bearing,又间距叫做 right-side bearing。而纵向排版,则是用 ascent 与 descent 表示,他们分别代表顶部与底部和 origin 距离。Bounding box 即是真正渲染出来,用户能看到的部分。

kerning

字间距。默认情况下,横向排版就是一个字接一个字,但是很多时候,为了好看,我们会调整字间距。给 NSAttibutedString 设置对应的 NSKernAttributeName 即可。

leading

行间距。这个应该很好理解了,从之前的图可以看出:

字体大小计算

通过对布局的基本介绍,大致能知道 glyph 的布局范围。那么,我们再来看看平时用得最多的文字范围计算。在我接触到的项目中,几乎都有类似字体范围计算的 category,目前我司的是这样的:

@interface UILabel (STExtension)

- (CGSize)st_size;
- (CGSize)st_sizeWithMaxsize:(CGSize)size;

@end

不仅如此,还有 NSString 的,还有 NSAttributedString 的。而散落在工程中的,还有各种各样的计算方法:

// NSStirngDrawing.h
- (CGRect)boundingRectWithSize:options:attributes:context:
- (CGSize)sizeWithAttributes:

// UILabel.h
- (CGRect)textRectForBounds:limitedToNumberOfLines:

// UIView.h
- (CGSize)sizeThatFits
  
// UIFont.h
@property(nonatomic, readonly) CGFloat lineHeight;

官方提供的顶层接口就有好几个,那么,应该用哪个,哪个更为准确呢?我们一一试验一下。

经过查看调用栈,最终的调用方法分别为:

  • NSStirngboundingRectWithSize:options:attributes:context:
  • NSAttributedStringboundingRectWithSize:options:context:
  • UIFontlineHeight

而 UI 控件则是在这些方法上进行向上取整,以保证渲染效率。除此之外,UILabeltextRectForBounds:limitedToNumberOfLines: 方法实在有趣,不仅可以自行判断是计算 text 还是 attributedText,而且还能给定高度。也就是说,当文本 < limitedLines 时,返回文本自身高度,而超过时,则返回最大高度。

封装

根据日常用到的计算场景,我重新封装了 category,下面是主要修改:

  • 提供单行文本的高度计算。直接 ceil(font.lineHeight)
  • 提供指定行数的文本的高度计算,与单行类似。
  • NSStringNSSAttributedString 提供指定文本行数的计算方式,其中,在 NSString+ load 方法中初始化静态 UILabel,并直接调用 label 的相关方法进行计算。

完整代码可以查看 repoUIFont+STSizeUILabel+STSizeNSString+STSizeNSSAttibutedString+STSize

问题

NSAttributedString 的有 firstLineHeadIndent 时,也就是首行缩进属性,计算会有问题。具体情况如下:

attributtedText.string = @”abcabcabc…(1000)…abc”;

numberOfLines = 0;

firstLineHeadIndent = 20;

maxSize = CGSizeMake(10, HUGE);

调用 UILabeltextRectForBounds:limitedToNumberOfLines: 方法,能正确获得 size。

但是,当 attributtedText.string 太短,只能显示一行时,返回的 size 大小却只包含 text 的 size,而没有加上 firstLineHeadIndent。

也就是说,当 text 只够显示一行文本时,如果有首行缩进,就会出问题,所以在使用时,还是要小心。