『TextLayout』Font 与大小计算
sdt1005
7年前
<p>前端作为一个展示平台,打交道最多的就是文字和图形。其实,文字也是一种图形。在查阅资料后,大概总结了下:字符布局范围,文字绘制到屏幕上的流程,自定义 inputView等。</p> <p>环境信息</p> <p>macOS 10.12.4</p> <p>Xcode 8.4</p> <p>iOS 10.4</p> <h2><strong>字体</strong></h2> <p><strong>glyph</strong></p> <p>字形,估计用英文还要好理解一点:symbol。每个字,都有各种各样的字形,如「A」:</p> <p><img src="https://simg.open-open.com/show/c832c454ed5c8c487814c35ff7829089.png"></p> <p>那么,是否代表着字母和字形之间,有某种对应关系呢?这也不尽然。</p> <p>Ligature</p> <p>连体。如果说,字母和字形是一对多的关系,其实也不对,因为还会存在连体的情况。这使得一个字形,也对应这多个字母:</p> <p><img src="https://simg.open-open.com/show/6bd9e123167a13dada63e6cc7fb60bc9.png"></p> <p>计算机存储字符的方式为「数字 – 字符编码」的映射表。而 iOS 与 macOS 平台下,均使用 Unicode 编码。它独立于平台,语言等存在,解决了计算机系统中,各种编码方案之间的冲突。除此之外,还提供了应该如何处理上下文,如何断句换行,如何在不同语言间排版,如何格式化数字、时间等解决方案。</p> <p><strong>typeface</strong></p> <p>字体。font 和 typeface 翻译过来都是字体,所以为什么说中文理解更麻烦呢。来看看英文解释:</p> <ul> <li>typeface: a particular design of type</li> <li>font: a set of type of particular face and size</li> </ul> <p>所以,font 其实是 typeface 和 size 的组合。我们在初始化 UIFont 的时候就能看出,需要指定 font name(这是之后要介绍的 font family),还需要指定 font size。当然,font 也不只是包含这两个信息,接下来提到的 typestyle 也是其中之一。</p> <p><strong>typestyle</strong></p> <p>字体样式。一种字体可能会提供多种不同的样式。如,斜体、粗体等。</p> <p><strong>font family</strong></p> <p>即同一种字体,不同样式的组合。如,宋体+粗体,宋体+细体,宋体+斜体,它们均为宋体,但是又有着不同的样式,整个组合形式,即宋体的 font family。</p> <p>综合以上的概念,可以得出如下公式:</p> <h2><strong>字体布局</strong></h2> <p>将文字渲染到界面的过程,即是将 text 生成 glyph,通过 text layout 排版到 text view 的过程。对于英文来说,从 text view 的左上角开始排版,到达右边界后,另起一行,直到布局到右下角,结束。</p> <p>平时经常需要计算文字大小,用于符合设计图要求。但是,文字的范围,行间距这些到底是什么?有没有更简便的计算方式?先来看看字符之间都有哪些间隙:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/ebee9fc0453ba51fdaf795baca80b89c.png"></p> <p>图中标的名字,都对应着 UIFont 的属性:</p> <pre> <code class="language-objectivec">@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;</code></pre> <p>所以,要计算一行文本的高度,可以直接调用 lineHeight 。而实际调用 UILabel 计算出来的高度,等于 ceil(font.lineHeight) ,这也是计算方法内部,做的优化。</p> <p>下面这个图,是单个字母的布局规则,通过它来认识布局中的其他元素:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ff680f0114ede15722d81b71660ddf6b.png"></p> <p><strong>metrics</strong></p> <p>单位长度。对于横向布局的字符来说,布局系统会给一个单位间距,也就是途中看到的 Advance width。也就是从 origin 点,到 glyph 真正渲染的距离,这也是与下一个 glyph 之间的间距。在这里,左间距叫做 left-side bearing,又间距叫做 right-side bearing。而纵向排版,则是用 ascent 与 descent 表示,他们分别代表顶部与底部和 origin 距离。Bounding box 即是真正渲染出来,用户能看到的部分。</p> <p><strong>kerning</strong></p> <p>字间距。默认情况下,横向排版就是一个字接一个字,但是很多时候,为了好看,我们会调整字间距。给 NSAttibutedString 设置对应的 NSKernAttributeName 即可。</p> <p><strong>leading</strong></p> <p>行间距。这个应该很好理解了,从之前的图可以看出:</p> <h2><strong>字体大小计算</strong></h2> <p>通过对布局的基本介绍,大致能知道 glyph 的布局范围。那么,我们再来看看平时用得最多的文字范围计算。在我接触到的项目中,几乎都有类似字体范围计算的 category,目前我司的是这样的:</p> <pre> <code class="language-objectivec">@interface UILabel (STExtension) - (CGSize)st_size; - (CGSize)st_sizeWithMaxsize:(CGSize)size; @end</code></pre> <p>不仅如此,还有 NSString 的,还有 NSAttributedString 的。而散落在工程中的,还有各种各样的计算方法:</p> <pre> <code class="language-objectivec">// 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;</code></pre> <p>官方提供的顶层接口就有好几个,那么,应该用哪个,哪个更为准确呢?我们一一试验一下。</p> <p>经过查看调用栈,最终的调用方法分别为:</p> <ul> <li>NSStirng : boundingRectWithSize:options:attributes:context:</li> <li>NSAttributedString : boundingRectWithSize:options:context:</li> <li>UIFont : lineHeight</li> </ul> <p>而 UI 控件则是在这些方法上进行向上取整,以保证渲染效率。除此之外, UILabel 的 textRectForBounds:limitedToNumberOfLines: 方法实在有趣,不仅可以自行判断是计算 text 还是 attributedText,而且还能给定高度。也就是说,当文本 < limitedLines 时,返回文本自身高度,而超过时,则返回最大高度。</p> <h3><strong>封装</strong></h3> <p>根据日常用到的计算场景,我重新封装了 category,下面是主要修改:</p> <ul> <li>提供单行文本的高度计算。直接 ceil(font.lineHeight) 。</li> <li>提供指定行数的文本的高度计算,与单行类似。</li> <li>给 NSString 、 NSSAttributedString 提供指定文本行数的计算方式,其中,在 NSString 的 + load 方法中初始化静态 UILabel ,并直接调用 label 的相关方法进行计算。</li> </ul> <p>完整代码可以查看 repo 的 UIFont+STSize , UILabel+STSize , NSString+STSize , NSSAttibutedString+STSize 。</p> <h2><strong>问题</strong></h2> <p>当 NSAttributedString 的有 firstLineHeadIndent 时,也就是首行缩进属性,计算会有问题。具体情况如下:</p> <p>attributtedText.string = @”abcabcabc…(1000)…abc”;</p> <p>numberOfLines = 0;</p> <p>firstLineHeadIndent = 20;</p> <p>maxSize = CGSizeMake(10, HUGE);</p> <p>调用 UILabel 的 textRectForBounds:limitedToNumberOfLines: 方法,能正确获得 size。</p> <p>但是,当 attributtedText.string 太短,只能显示一行时,返回的 size 大小却只包含 text 的 size,而没有加上 firstLineHeadIndent。</p> <p>也就是说,当 text 只够显示一行文本时,如果有首行缩进,就会出问题,所以在使用时,还是要小心。</p> <p> </p> <p>来自:http://www.saitjr.com/ios/textlayout-font-and-size.html</p> <p> </p>