『TextLayout』Font 與大小計算
前端作為一個展示平臺,打交道最多的就是文字和圖形。其實,文字也是一種圖形。在查閱資料后,大概總結了下:字符布局范圍,文字繪制到屏幕上的流程,自定義 inputView等。
環境信息
macOS 10.12.4
Xcode 8.4
iOS 10.4
字體
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;
官方提供的頂層接口就有好幾個,那么,應該用哪個,哪個更為準確呢?我們一一試驗一下。
經過查看調用棧,最終的調用方法分別為:
- NSStirng : boundingRectWithSize:options:attributes:context:
- NSAttributedString : boundingRectWithSize:options:context:
- UIFont : lineHeight
而 UI 控件則是在這些方法上進行向上取整,以保證渲染效率。除此之外, UILabel 的 textRectForBounds:limitedToNumberOfLines: 方法實在有趣,不僅可以自行判斷是計算 text 還是 attributedText,而且還能給定高度。也就是說,當文本 < limitedLines 時,返回文本自身高度,而超過時,則返回最大高度。
封裝
根據日常用到的計算場景,我重新封裝了 category,下面是主要修改:
- 提供單行文本的高度計算。直接 ceil(font.lineHeight) 。
- 提供指定行數的文本的高度計算,與單行類似。
- 給 NSString 、 NSSAttributedString 提供指定文本行數的計算方式,其中,在 NSString 的 + load 方法中初始化靜態 UILabel ,并直接調用 label 的相關方法進行計算。
完整代碼可以查看 repo 的 UIFont+STSize , UILabel+STSize , NSString+STSize , NSSAttibutedString+STSize 。
問題
當 NSAttributedString 的有 firstLineHeadIndent 時,也就是首行縮進屬性,計算會有問題。具體情況如下:
attributtedText.string = @”abcabcabc…(1000)…abc”;
numberOfLines = 0;
firstLineHeadIndent = 20;
maxSize = CGSizeMake(10, HUGE);
調用 UILabel 的 textRectForBounds:limitedToNumberOfLines: 方法,能正確獲得 size。
但是,當 attributtedText.string 太短,只能顯示一行時,返回的 size 大小卻只包含 text 的 size,而沒有加上 firstLineHeadIndent。
也就是說,當 text 只夠顯示一行文本時,如果有首行縮進,就會出問題,所以在使用時,還是要小心。
來自:http://www.saitjr.com/ios/textlayout-font-and-size.html