深入理解Core Text排版引擎

iOS系統(tǒng)上可以使用UILable、UITextFileld、TextKit顯示文本,TextKit也可以做一些布局控制,但如果需要精細的布局控制,或者自線程異步繪制文本,就必須使用Core Text和Core Graphics,本文比較系統(tǒng)地講解Core Text排版核心概念。

iOS文本系統(tǒng)框架

iOS文本系統(tǒng)框架

Core Text是iOS 系統(tǒng)文本排版核心框架,TextKit和WebKit都是封裝在CoreText上的,TextKit是iOS7引入的,在iOS7之前幾乎所有的文本都是 WebKit 來處理的,包括UILable、UITextFileld等,TextKit是從Cocoa文本系統(tǒng)移植到iOS系統(tǒng)的。
文本渲染過程中Core Text只負責排版,具體的繪制操作是通過Core Graphics框架完成的。如果需要精細的排版控制可以使用Core Text,否則可以直接使用Text Kit。

Core Text排版引擎框架

CoreText排版引擎框架

CTFramesetter是Core Text中最上層的類,CTFramesetter持有attributed string并創(chuàng)建CTTypesetter,實際排版由CTTypesetter完成。CTFrame類似于書本中的「頁」,CTLine類似「行」,CTRun是一行中具有相同屬性的連續(xù)字形。CTFrame、CTLine、CTRun都有對應的Draw方法繪制文本,其中CTRun支持最精細的控制。

CoreText排版引擎框架

排版核心概念

要實現(xiàn)精細的排版控制,就必須理解排版的概念,因為Core Text很多api都涉及到排版概念,這些概念是平臺無關(guān)的,其他系統(tǒng)也一樣適應。
排版引擎通過下面兩步對文本進行排版:

  • 生成字形(glyph generation)
  • 字形布局(glyph layout)

字符(Characters)和字形(Glyphs)

字符和字形概念比較好理解,下圖很直觀


Glyphs of the character A

字型(Typefaces)和字體(Fonts)

字型和字體的概念可能沒這么好區(qū)分,直接引用官方文檔的原話

A typeface is a set of visually related shapes for some or all of the characters in a written language.

A font is a series of glyphs depicting the characters in a consistent size, typeface, and typestyle.

字體是字型的子集,字型也叫font family,比如下圖:

Fonts in the Times font family

字體屬性(Font metrics)

排版引擎要布局字型,就必須知道字型大小和怎樣布局,這些信息就叫字體屬性,開發(fā)過程也是通過這些屬性來計算布局的。字體屬性由字體設計者提供,同一個字體不同字形的屬性相同,主要屬性如下圖:

Glyph metrics
Glyph metrics
  • baseline:字符基線,baseline是虛擬的線,baseline讓盡可能多的字形在baseline上面,CTFrameGetLineOrigins獲取的Origins就是每一行第一個CTRun的Origin
  • ascent:字形最高點到baseline的推薦距離
  • descent:字形最低點到baseline的推薦距離
  • leading:行間距,即前一行的descent與下一行的ascent之間的距離
  • advance width:Origin到下一個字形Origin的距離
  • left-side bearing:Origin到字形最左邊的距離
  • right-side bearing:字形最右邊到下一個字形Origin的距離
  • bounding box:包含字形最小矩形
  • x-height:一般指小寫字母x最高的到baseline的推薦距離
  • Cap-height:一般指H或I最高的到baseline的推薦距離

部分字體屬性可以通過UIFont的方法獲取,ascent、descent、leading可以通過CTRunGetTypographicBounds、CTRunGetTypographicBounds方法獲取,通過ascent、descent、leading可以計算line的實際高度。CTFrame、CTLine、CTRun都有提供api獲取屬性和繪制文本,控制粒度也由高到低,可以根據(jù)具體需求使用不同的粒度。
CTLine上不同CTRun默認是底部對齊的,如果一行文本有Attachment,并且Attachment比字體高,會導致字符偏下,如下圖:

帶有Attachment的文本渲染

如果要使字符居中對齊,可以通過CGContextSetTextPosition調(diào)整每個CTRun的originY,調(diào)整后如下圖:

垂直方向居中

字距調(diào)整

排版系統(tǒng)默認按advance width逐個字符渲染,這樣會導致有些字符間距離很大,為了使排版后可讀性更高,一般會調(diào)整字距,如下圖:

Kerning

坐標系變換

UIKit和Core Graphics使用不同的坐標系,UIKit坐標系原點在左上角,Core Graphics坐標系在左下角,如下圖:

Core Graphics和UIKit坐標系

使用Core Graphics繪制前必須進行坐標變換,否則繪制后的文本是倒立的,
如下圖:

坐標未變換

坐標一般通過下面方法進行變換:

//1.設置字形的變換矩陣為不做圖形變換
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//2.平移方法,將畫布向上平移bounds的高度
CGContextTranslateCTM(context, 0.0f, self.bounds.size.height);
//3.縮放方法,x軸縮放系數(shù)為1,則不變,y軸縮放系數(shù)為-1,則相當于以x軸為軸旋轉(zhuǎn)180度
CGContextScaleCTM(context, 1.0f, -1.0f);

變換之后就將Core Graphics坐標系變換成UIKit坐標系了。

Attachment

Core Text 不能直接繪制圖像,但可以留出空白空間來為圖像騰出空間。通過設置 CTRun 的 delegate,可以確定 CTRun 的 ascent space, descent space and width,如下圖:

Attachment渲染

當Core Text遇到一個設置了CTRunDelegate的CTRun,它就會詢問delegate:“我需要留多少空間給這塊的數(shù)據(jù)”。通過在CTRunDelegate中設置這些屬性,您可以在文本中給圖片留開空位。具體方法可以參考「 Core Text Tutorial for iOS: Making a Magazine App

點擊響應

使用文本渲染的時候經(jīng)常需要不同的文本響應不同的點擊事件,Core Text本身是不支持點擊事件的,要實現(xiàn)不同的文本響應不同的點擊事件,就必須知道點擊的是哪個字符,核心過程:

  • 重寫UIView Touch Event方法捕捉點擊事件
  • 通過Core Text查找touch point對應的字符

這里主要講下如何通過Core Text查找touch point對應的字符,核心代碼如下:

- (CFIndex)characterIndexAtPoint:(CGPoint)p {
    CFIndex idx = NSNotFound;
    if (!_lines) {
        return idx;
    }
    CGPoint *linesOrigins = (CGPoint*)malloc(sizeof(CGPoint) * CFArrayGetCount(_lines));
    if (!linesOrigins) {
        return idx;
    }
    
    p = CGPointMake(p.x - _rect.origin.x, p.y - _rect.origin.y);
    // Convert tap coordinates (start at top left) to CT coordinates (start at bottom left)
    p = CGPointMake(p.x, _rect.size.height - p.y);
    
    CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), linesOrigins);
    
    for (CFIndex lineIndex = 0; lineIndex < _fitNumberOfLines; lineIndex++) {
        CGPoint lineOrigin = linesOrigins[lineIndex];
        CTLineRef line = CFArrayGetValueAtIndex(_lines, lineIndex);
        
        // Get bounding information of line
        CGFloat ascent = 0.0f, descent = 0.0f, leading = 0.0f;
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat yMin = (CGFloat)floor(lineOrigin.y - descent);
        CGFloat yMax = (CGFloat)ceil(lineOrigin.y + ascent);
        
        // Apply penOffset using flushFactor for horizontal alignment to set lineOrigin since this is the horizontal offset from drawFramesetter
        CGFloat flushFactor = 0.0;;
        CGFloat penOffset = (CGFloat)CTLineGetPenOffsetForFlush(line, flushFactor, _rect.size.width);
        lineOrigin.x = penOffset;
        lineOrigin.y = lineOrigin.y - _originY;
        
        // Check if we've already passed the line
        if (p.y > yMax) {
            break;
        }
        // Check if the point is within this line vertically
        if (p.y >= yMin) {
            // Check if the point is within this line horizontally
            if (p.x >= lineOrigin.x && p.x <= lineOrigin.x + width) {
                // Convert CT coordinates to line-relative coordinates
                CGPoint relativePoint = CGPointMake(p.x - lineOrigin.x, p.y - lineOrigin.y);
                idx = CTLineGetStringIndexForPosition(line, relativePoint);
                break;
            }
        }
    }
    free(linesOrigins);
    return idx;
}

引用

Quartz 2D Programming Guide
Core Text Programming Guide
Text Programming Guide for iOS
Cocoa Text Architecture Guide
Core Text Tutorial for iOS: Making a Magazine App
初識 TextKit
Font wiki

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容