CoreText學習筆記


CoreText是Apple系統的文字渲染引擎。

我們先看一個字符的字形圖,了解一下一個字形包含的部分:

 

它的坐標系為窗口的左下角為視圖的原點(跟Mac系統一樣的坐標系),而iOS系統的坐標系為窗口的左上角為視圖的原點。因此我們在使用CoreText進行繪制文字時,需要對其坐標系進行轉換,轉換方法如下:

CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1, -1);

未進行坐標系轉換時繪制結果如下:

轉換后如下:

 

CoreText繪制文本流程這里引用官方的截圖:(點擊查看官方文檔

從圖中可知,繪制文本總共涉及到以下幾個類:

  • CTFramesetter
  • CTTypesetter
  • CTFrame
  • CTLine
  • CTRun

以上這些類的操作都是線程安全的

CTFramesetter:就是用於繪制文本的總管。

CTFrame:一個文本段落,包括多行文本,多種樣式的文字。

CTLine:一行文本,它不會折行,代表一個段落中的一行文本。

CTRun:一行文本中,相同樣式的字符。

CTTypesetter:用於控制一行文本如何斷行。

 

接下來我們先看看一個簡單的文本繪制,代碼如下:

override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        // 轉換坐標系
        context.translateBy(x: 0, y: bounds.height)
        context.scaleBy(x: 1, y: -1)
        // 初始化文本矩陣
        context.textMatrix = .identity

        // 根據字符串,創建一個帶有屬性的字符串
        let strAttr: [NSAttributedString.Key : Any] = [
            .foregroundColor: color,
            .font: font
        ]
        let attrStr = NSAttributedString(string: text ?? "", attributes: strAttr)
        
        // 設置一個文本的繪制區域,CoreText只會在這個區域內進行繪制(超出部分的字符將不被繪制)。由於它是一個CGPath,因此我們可以指定一個不規則形狀的區域
        // 這里我們指定一個矩形區域,該區域是相對當前視圖bounds,擁有8pt內邊距的一個矩形區域
        let path = CGPath(rect: bounds.insetBy(dx: 8, dy: 8), transform: nil)
        
        // 初始化一個CTFramesetter
        let frameSetter = CTFramesetterCreateWithAttributedString(attrStr)
        // 創建CTFrame
        let attr: CFDictionary = [
            kCTFrameProgressionAttributeName: CTFrameProgression.topToBottom.rawValue as CFNumber // 這是文字的繪制方向,這里是從上到下繪制(默認值)
        ] as CFDictionary
        // frameAttributes參數允許為nil, stringRange參數指定加載文本的區間,如果lenght=0,將會加載全部文本
        let textFrame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: 0), path, attr)
        // 繪制文本
        CTFrameDraw(textFrame, context)
    }

這里我們繪制的文字是:“元旦快樂,新年快了!祝大家心想事成,萬事如意!”,實際顯示的效果如下:

我們發現,“祝”后面的文字沒有顯示出來,這是因為我們設置的繪制區域只能顯示這么多文字,其它部分文字被忽略了。

指定繪制區域是采用CGPath指定的,因此這樣會更加靈活,我們可以繪制一個任何形狀的區域用於顯示文字。

創建CTFramesetter有兩個方法,如下:

public func CTFramesetterCreateWithAttributedString(_ attrString: CFAttributedString) -> CTFramesetter

public func CTFramesetterCreateWithTypesetter(_ typesetter: CTTypesetter) -> CTFramesetter

這里我們采用的是第一種方法,另一種方法我們可以采用下面的方式來初始化:

// 創建一個CTTypesetter
let typesetter = CTTypesetterCreateWithAttributedString(attrStr)
// 初始化一個CTFramesetter
let frameSetter = CTFramesetterCreateWithTypesetter(typesetter)

根據CTFramesetter創建一個段落CTFrame方法如下:

func CTFramesetterCreateFrame(_ framesetter: CTFramesetter, _ stringRange: CFRange, _ path: CGPath, _ frameAttributes: CFDictionary?) -> CTFrame

stringRange:指定這個段落的字符區間,即:這個段落中的文本包括從attrStr字符串的location開始,length長度的字符串(實際上length如果超出了繪制區域,它依然是無法繪制出全部的文字)。當length=0時,CTFramesetter會連續不斷的將字符串繪制成每一個行,直到字符串被耗盡,或者超出繪制范圍為止。因此這里的length一般設置為0。它會自動管理我們的字符串。

path:指定繪制區間范圍。

frameAttributes:初始化CTFrame的一些屬性參數,如果傳nil,CTFramesetter會初始化一個默認值。這些屬性包括如下(在CTFrame文件中可以查到):

// 指定文字排版方向,它對應一個枚舉值CTFrameProgression,包括:topToBottom(默認)、rightToLeft、leftToRight
kCTFrameProgressionAttributeName(value為CFNumberRef類型)

// 對應枚舉:CTFramePathFillRule,包括:evenOdd(默認)、windingNumber
kCTFramePathFillRuleAttributeName(value為CFNumberRef類型)

// 指定繪制區域的寬度:默認0
kCTFramePathWidthAttributeName(value為CFNumberRef類型)

// 指定一個裁剪path數組
kCTFrameClippingPathsAttributeName(value為CFArrayRef,item為:CFDictionaryRefs or CGPathRef)

kCTFramePathClippingPathAttributeName(value為CGPathRef)

 

 我們看一下kCTFrameProgressionAttributeName這個屬性的效果(我們顯示的內容為:“元旦快樂”):

topToBottom(默認):

rightToLeft:

 leftToRight:

 

我們可以通過如下方法獲取CTFrame相關屬性和結果:

// 獲取CTFrame的stringRange屬性(該值在初始化CTFrame時有設置,如果初始化時stringRange的length=0,則這里返回的是attrStr的全部長度)
func CTFrameGetStringRange(_ frame: CTFrame) -> CFRange

// 獲取可見區域內的字符串區間(我們可以通過該方法來獲取指定區域內可以顯示的文字個數)
func CTFrameGetVisibleStringRange(_ frame: CTFrame) -> CFRange

// 獲取CTFrame的繪制區域
func CTFrameGetPath(_ frame: CTFrame) -> CGPath

// 獲取CTFrame的屬性字典
func CTFrameGetFrameAttributes(_ frame: CTFrame) -> CFDictionary?

// 獲取CTFrame中包含的CTLine(即:一個段落中的全部的行文字,我們可以通過這個知道該段落包含多少行文字)
func CTFrameGetLines(_ frame: CTFrame) -> CFArray

// 獲取每行的Origin坐標點
func CTFrameGetLineOrigins(_ frame: CTFrame, _ range: CFRange, _ origins: UnsafeMutablePointer<CGPoint>)

// 繪制段落
func CTFrameDraw(_ frame: CTFrame, _ context: CGContext)

獲取段落中每行的原點(Origin)坐標方法(CTFrameGetLineOrigins)參數說明:

frame:段落CTFrame

range:這個是段落中的行的范圍,指定哪些行,length=0,location=0表示所有行。

origins:返回原點坐標數組指針 

這個原點坐標就是文章開始的那張圖中的Origin,接下來我們看一個demo,我們獲取一個段落中第1行(注意這里1指的是下標,從0開始)之后的所有行的原點坐標,並用綠色圓點標記出來。

// 獲取段落中的總行數
let lineCount = CFArrayGetCount(CTFrameGetLines(textFrame))
// 獲取第1行之后的所有行的原點坐標
let startIndex = 1
var pts: [CGPoint] = Array<CGPoint>.init(repeating: .zero, count: lineCount - startIndex)
CTFrameGetLineOrigins(textFrame, CFRange(location: startIndex, length: 0), &pts)
for pt in pts {
    let v = UIView()
    v.frame = CGRect(x: pt.x, y: pt.y, width: 4, height: 4)
    v.backgroundColor = .green.withAlphaComponent(0.5)
    v.layer.cornerRadius = 2
    addSubview(v)
}

第一行之后每行的原點坐標如下:

所有行的原點坐標如下:

從圖中我們看到,每行的順序是從下到上遍歷的。(第一行是最下面的一行,這跟CoreText的坐標系有關)

但是CTFrameGetLines獲取的CTLine數組順序是從上到下的。

 

我們發現這個方法CTFrameGetVisibleStringRange很有用,它可以獲取當前區域內顯示的文字長度。這樣一來我們在解決閱讀器看小說的應用場景中的分頁問題很有幫助,我們可以采用如下方式來對一個小說內容進行分頁。

// 這里假設一篇文章的全部內容如下
let text = "隨着北京冬奧會臨近,作為冬奧主要賽區之一所在地的河北省,正全力做好冬奧會疫情防控工作。“要堅持舉全省之力推進冬奧會、冬殘奧會疫情防控,全程嚴管嚴防。要進一步完善組織領導體系,嚴格落實北京冬奧組委防控要求,不斷優化完善張家口賽區疫情防控方案和應急預案。”相關負責人表示,要堅持人物同防,強化國內外涉賽人員和物品消毒、人員檢測,築牢外防輸入、內防反彈堅固防線。要強化分區分類閉環管理,實行空間分區、人員分類、互不交叉,加強核酸采樣、核酸檢測、消毒消殺、流調溯源等人員力量,一旦發現陽性病例,高效快速應急處置。要加強賽事組織,完善交通、觀賽等方案,確保運動員和觀賽群眾安全。"
        
var list = [String]() // 用於存儲每頁將顯示的字符串
var index = 0
let queue = DispatchQueue(label: "queue") // 這里我們采用一個子線程來計算分頁數據(因為這里會相對耗時)
let rect = CGRect(x: 0, y: 0, width: 100, height: 100) // 我們指定一頁顯示的區域范圍
let path = CGPath(rect: rect, transform: nil)
queue.async {
    let attrStr = NSMutableAttributedString(string: text )
    attrStr.setAttributes([.font: UIFont.systemFont(ofSize: 15, weight: .bold)], range: NSRange(location: 0, length: text.count))
    // 初始化CTFramesetter
    let framesetter = CTFramesetterCreateWithAttributedString(attrStr)
    
    while index < text.count {
        // 這里的stringRange的length=0,讓CoreText自己計算可以顯示多少文字,
        let frame = CTFramesetterCreateFrame(framesetter, CFRange(location: index, length: 0), path, nil)
        // 獲取當前區域內顯示的文字范圍
        let range = CTFrameGetVisibleStringRange(frame)
        index += range.length
        let str = text.string(NSRange(location: range.location, length: range.length))
        list.append(str)
    }
    
    print("total length: \(text.count)")
    for s in list {
        print("line: \(s), length: \(s.count)")
    }
}

其實我們還有另一個方法來計算,方法如下:

func CTFramesetterSuggestFrameSizeWithConstraints(_ framesetter: CTFramesetter, 
                                                  _ stringRange: CFRange, 
                                                  _ frameAttributes: CFDictionary?, 
                                                  _ constraints: CGSize, 
                                                  _ fitRange: UnsafeMutablePointer<CFRange>?) -> CGSize

stringRange:這個跟上面介紹的CTFramesetter的創建時的參數一樣

frameAttributes:這個跟上面介紹的CTFramesetter的創建時的參數一樣

constraints:指定當前顯示區域尺寸,用於計算該區域內所顯示的文字范圍

fitRange:返回顯示的文字范圍

該方法返回值是CGSize,表示當前區域內,實際用來顯示文字的區域尺寸

下面是代碼demo:

// 這里假設一篇文章的全部內容如下
let text = "隨着北京冬奧會臨近,作為冬奧主要賽區之一所在地的河北省,正全力做好冬奧會疫情防控工作。“要堅持舉全省之力推進冬奧會、冬殘奧會疫情防控,全程嚴管嚴防。要進一步完善組織領導體系,嚴格落實北京冬奧組委防控要求,不斷優化完善張家口賽區疫情防控方案和應急預案。”相關負責人表示,要堅持人物同防,強化國內外涉賽人員和物品消毒、人員檢測,築牢外防輸入、內防反彈堅固防線。要強化分區分類閉環管理,實行空間分區、人員分類、互不交叉,加強核酸采樣、核酸檢測、消毒消殺、流調溯源等人員力量,一旦發現陽性病例,高效快速應急處置。要加強賽事組織,完善交通、觀賽等方案,確保運動員和觀賽群眾安全。"
        
var list = [String]() // 用於存儲每頁將顯示的字符串
var index = 0
let queue = DispatchQueue(label: "queue") // 這里我們采用一個子線程來計算分頁數據(因為這里會相對耗時)
let rect = CGRect(x: 0, y: 0, width: 100, height: 100) // 我們指定一頁顯示的區域范圍
let path = CGPath(rect: rect, transform: nil)
queue.async {
    let attrStr = NSMutableAttributedString(string: text )
    attrStr.setAttributes([.font: UIFont.systemFont(ofSize: 15, weight: .bold)], range: NSRange(location: 0, length: text.count))
    // 初始化CTFramesetter
    let framesetter = CTFramesetterCreateWithAttributedString(attrStr)
    
    while index < text.count {
        var range: CFRange = .init(location: 0, length: 0) // 可展示區域內的字符串范圍
        let size = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(location: index, length: 0), nil, rect.size, &range)
        print("當前區域內可展示的內容實際大小: \(size)")
        let str = text.string(.init(location: range.location, length: range.length))
        list.append(str)
        index += range.length
    }
    
    print("total length: \(text.count)")
    for s in list {
        print("line: \(s), length: \(s.count)")
    }
}

以上輸出結果如下:

total length: 284
line: 隨着北京冬奧會臨近,作為冬奧主要賽區之一所在地的河北省,正全, length: 30
line: 力做好冬奧會疫情防控工作。“要堅持舉全省之力推進冬奧會、冬殘, length: 30
line: 奧會疫情防控,全程嚴管嚴防。要進一步完善組織領導體系,嚴格, length: 29
line: 落實北京冬奧組委防控要求,不斷優化完善張家口賽區疫情防控方, length: 29
line: 案和應急預案。”相關負責人表示,要堅持人物同防,強化國內外涉, length: 30
line: 賽人員和物品消毒、人員檢測,築牢外防輸入、內防反彈堅固防線。, length: 30
line: 要強化分區分類閉環管理,實行空間分區、人員分類、互不交, length: 27
line: 叉,加強核酸采樣、核酸檢測、消毒消殺、流調溯源等人員力量,, length: 29
line: 一旦發現陽性病例,高效快速應急處置。要加強賽事組織,完善交, length: 29
line: 通、觀賽等方案,確保運動員和觀賽群眾安全。, length: 21

 

除了繪制一整段的段落文字外,我們還可以繪制一個單行的無換行的文字。通過CTLine來繪制一個單行文字,提供以下三種方法來創建CTLine:

// 初始化CTLine
func CTLineCreateWithAttributedString(_ attrString: CFAttributedString) -> CTLine

// 設置CTLine的截斷方式
// width:設置單行文本的顯示寬度(超出部分截斷)
// truncationType:(截斷方式,start:文本開始處截斷;middle:文本中間處截斷;end:文本末尾處截斷)
// truncationToken:截斷處填充的字符(例如:超出width后,在結尾處顯示“...”)
func CTLineCreateTruncatedLine(_ line: CTLine, 
                               _ width: Double, 
                               _ truncationType: CTLineTruncationType, 
                               _ truncationToken: CTLine?) -> CTLine?

// 設置CTLine的排版方式
// justificationFactor:適配因子,<=0:不適配;>1:全面適配;0<x<1;部分適配
// justificationWidth:排版觸發適配的寬度
func CTLineCreateJustifiedLine(_ line: CTLine, 
                               _ justificationFactor: CGFloat, 
                               _ justificationWidth: Double) -> CTLine?

我們先來看看第一種初始化方法:

override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext() else { return }
    context.translateBy(x: 0, y: bounds.height)
    context.scaleBy(x: 1, y: -1)
    context.textMatrix = .identity

    let attrs: [NSAttributedString.Key : Any] = [
        .foregroundColor: color,
        .font: font,
        .backgroundColor: UIColor.green
    ]
    let attrStr = NSMutableAttributedString(string: text ?? "")
    attrStr.setAttributes(attrs, range: NSRange(location: 0, length: attrStr.length))

    // 創建CTLine
    let line = CTLineCreateWithAttributedString(attrStr)
    // 繪制CTLine
    CTLineDraw(line, context)
}

顯示效果如下:

我們繪制的文字內容為:“元旦快樂,新年快了!祝大家心想事成,萬事如意!”,我們看到文字顯示在一行上,沒有換行,但是超出部分沒有被截斷,依然被繪制出來了。

接下來采用第二個方法實現對CTLine文字截斷設置(實現超出顯示區域后截斷,末尾顯示“...”):

override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext() else { return }
    context.translateBy(x: 0, y: bounds.height)
    context.scaleBy(x: 1, y: -1)
    context.textMatrix = .identity

    let attrs: [NSAttributedString.Key : Any] = [
        .foregroundColor: color,
        .font: font,
        .backgroundColor: UIColor.green
    ]
    let attrStr = NSMutableAttributedString(string: text ?? "")
    attrStr.setAttributes(attrs, range: NSRange(location: 0, length: attrStr.length))

    // 初始化CTLine
    let line = CTLineCreateWithAttributedString(attrStr)
    // 初始化截斷填充字符串:”...“(Unicode表示方式)
    let tokenStr = NSAttributedString(string: "\u{2026}", attributes: attrs)
    let token = CTLineCreateWithAttributedString(tokenStr)
    // 設置截斷方式,尾部截斷,截斷處顯示”...“(設置失敗,返回nil)
    if let ln = CTLineCreateTruncatedLine(line, bounds.width, .end, token) {
        CTLineDraw(ln, context)
    }
}

效果如下:

 

還有一種情況,就是我們的一行文字內容較少,就像下面這樣:

 有時我們需要文字始終均勻分布,就像下面這樣:

如果想實現上面這樣的排版,就需要使用第三種方法了:

override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext() else { return }
    context.translateBy(x: 0, y: bounds.height)
    context.scaleBy(x: 1, y: -1)
    context.textMatrix = .identity

    let attrs: [NSAttributedString.Key : Any] = [
        .foregroundColor: color,
        .font: font,
        .backgroundColor: UIColor.green
    ]
    let attrStr = NSMutableAttributedString(string: text ?? "")
    attrStr.setAttributes(attrs, range: NSRange(location: 0, length: attrStr.length))

    // 初始化CTLine
    let line = CTLineCreateWithAttributedString(attrStr)
    // 設置文字排版的適配方式
    if let ln = CTLineCreateJustifiedLine(line, 1, bounds.width) {
        CTLineDraw(ln, context)
    }
}

我們再來調整一下justificationFactor參數,從1,改成0.5,效果如下:

 

關於CTLine的操作還有其它方法,如下:

// 獲取CTLine上的字形個數(如果設置了截斷,那么這個被截斷的CTLine的字形個數為:當前截斷后顯示部分的字形個數)
func CTLineGetGlyphCount(_ line: CTLine) -> CFIndex

// 獲取CTLine中的所有CTRun
func CTLineGetGlyphRuns(_ line: CTLine) -> CFArray

// 獲取CTLine上包含的字符串范圍(設置截斷的CTLine的字符串范圍,依然等於CTLine包含的全部字符串,即:被截斷部分也算在內)
func CTLineGetStringRange(_ line: CTLine) -> CFRange

// 獲取對齊文本的偏移量
// flushFactor:<=0:完全左對齊;0<x<0.5:左側對齊;0.5:完全居中對齊:0.5<x<1:右側對齊,>1:完全右對齊
func CTLineGetPenOffsetForFlush(_ line: CTLine, _ flushFactor: CGFloat, _ flushWidth: Double) -> Double

// 計算CTLine的字形邊界值,ascent、descent、leading:分別對應文章開頭部分的字形圖中標注的內容
// return:CTLine的寬度
func CTLineGetTypographicBounds(_ line: CTLine, 
                                _ ascent: UnsafeMutablePointer<CGFloat>?, 
                                _ descent: UnsafeMutablePointer<CGFloat>?, 
                                _ leading: UnsafeMutablePointer<CGFloat>?) -> Double
                        
// 計算CTLine的邊界矩形(注意坐標系:原點為左下角),options:計算參考條件
func CTLineGetBoundsWithOptions(_ line: CTLine, _ options: CTLineBoundsOptions) -> CGRect

// 計算CTLine的末尾空格或制表符部分寬度
func CTLineGetTrailingWhitespaceWidth(_ line: CTLine) -> Double

// 計算CTLine邊界矩形(注意坐標系:原點為左下角),此函數純粹是為了方便使用符號作為圖像,不應用於排版目的。
func CTLineGetImageBounds(_ line: CTLine, _ context: CGContext?) -> CGRect

// 獲取CTLine中,某個position位置的字符串索引,該值將不小於第一個字符索引,同時不大於最后一個字符的索引(調用失敗返回kCFNotFound)
// 一般用於hit testing(確定點擊位置的字符串索引)
func CTLineGetStringIndexForPosition(_ line: CTLine, _ position: CGPoint) -> CFIndex

// 獲取CTLine中索引下標的字符的偏移量(secondaryOffset:次偏移量,return:主偏移量)
func CTLineGetOffsetForStringIndex(_ line: CTLine, 
                                   _ charIndex: CFIndex, 
                                   _ secondaryOffset: UnsafeMutablePointer<CGFloat>?) -> CGFloat
                                
// 枚舉CTLine中所有字符,在閉包中返回每個字符串的偏移量
// 閉包中參數:0:偏移量;1:字符索引;2:是否為起始偏移量,true(起始偏移量);3:(未知)
// 例如:枚舉遍歷字符串"元旦"每個字符的偏移量,block輸出如下:
// 0、offset: 0.0,    idx: 0, flag: true,  ptr: false
// 1、offset: 18.342, idx: 0, flag: false, ptr: false
// 2、offset: 18.342, idx: 1, flag: true,  ptr: false
// 3、offset: 36.684, idx: 1, flag: false, ptr: false
// 因此,相同idx的flag=false的offset - flag=true的offset 就是當前idx字符的字形寬度
func CTLineEnumerateCaretOffsets(_ line: CTLine, _ block: @escaping (Double, CFIndex, Bool, UnsafeMutablePointer<Bool>) -> Void)

其中使用CTLineGetPenOffsetForFlush方法可以實現CTLine相對於context的水平對齊方式

例如:居中對齊、居右對齊。下面是flushFactor不同范圍的值對應的效果:

 一個實現完全居中對齊的代碼如下:

override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext() else { return }
    context.translateBy(x: 0, y: bounds.height)
    context.scaleBy(x: 1, y: -1)
    context.textMatrix = .identity

    let attrs: [NSAttributedString.Key : Any] = [
        .foregroundColor: color,
        .font: font,
        .backgroundColor: UIColor.green
    ]
    let attrStr = NSMutableAttributedString(string: text ?? "")
    attrStr.setAttributes(attrs, range: NSRange(location: 0, length: attrStr.length))

    // 初始化CTLine
    let line = CTLineCreateWithAttributedString(attrStr)
    // 獲取對齊文本的偏移量
    // flushFactor:<=0:完全左對齊;0<x<0.5:左側對齊;0.5:完全居中對齊:0.5<x<1:右側對齊,>1:完全右對齊
    let offset = CTLineGetPenOffsetForFlush(line, 0.5, bounds.width)
    // 設置文本的起始位置坐標
    context.textPosition = CGPoint(x: offset, y: 0)
    CTLineDraw(line, context)
}

 

另外我們還可以實現垂直居中,這里我們需要計算出CTLine的字形高度,我們通過CTLineGetTypographicBounds方法計算字形高度,具體實現代碼如下:

// 初始化CTLine
let line = CTLineCreateWithAttributedString(attrStr)
// 獲取對齊文本的偏移量
// flushFactor:<=0:完全左對齊;0<x<0.5:左側對齊;0.5:完全居中對齊:0.5<x<1:右側對齊,>1:完全右對齊
let offset = CTLineGetPenOffsetForFlush(line, 0.5, bounds.width)
var ascent: CGFloat = 0, descent: CGFloat = 0, leading: CGFloat = 0
CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
let lineHeight = ascent + descent + leading
// 設置文本的起始位置坐標
context.textPosition = CGPoint(x: offset, y: (bounds.height - lineHeight)/2.0)
CTLineDraw(line, context)

這里的ascent、descent、leading對應文章開頭的字形圖中標記的位置。最終實現效果如下:

 

實現列排版,先看效果:

 

實現代碼如下:

override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext() else { return }
    context.translateBy(x: 0, y: bounds.height)
    context.scaleBy(x: 1, y: -1)
    context.textMatrix = .identity

    let attrs: [NSAttributedString.Key : Any] = [
        .foregroundColor: color,
        .font: font,
        .backgroundColor: UIColor.green
    ]
    let attrStr = NSMutableAttributedString(string: text ?? "")
    attrStr.setAttributes(attrs, range: NSRange(location: 0, length: attrStr.length))

    let frameSetter = CTFramesetterCreateWithAttributedString(attrStr)
    let paths = paths(column: 4) // 計算4列的區域路徑
    var index = 0
    for path in paths {
        // 分別繪制每列的文字
        let textFrame = CTFramesetterCreateFrame(frameSetter, CFRange(location: index, length: 0), path, nil)
        let range = CTFrameGetVisibleStringRange(textFrame) // 獲取path區域內可見的文本范圍
        index += range.length
        CTFrameDraw(textFrame, context)
    }

}
// 計算每列繪制區域
private func paths(column: Int) -> [CGPath] {
    let columnWidth = bounds.width / CGFloat(column)
    var list = [CGRect]()
    var temRect = bounds
    for _ in 0..<column {
        // 利用divided函數將temRect矩形按指定距離切割,返回值為元祖,slice:切割指定距離的矩形;remainder:剩余部分的矩形
        let res = temRect.divided(atDistance: columnWidth, from: .minXEdge)
        list.append(res.slice)
        temRect = res.remainder // 剩余部分繼續切割
    }
    return list.map({ (rec) -> CGPath in
        let path = CGMutablePath()
        path.addRect(rec.insetBy(dx: 8, dy: 10)) // 每列矩形增加內邊距
        return path
    })
}

實際上就是利用CTFramesetter進行的布局,只是分別對每列的矩形區域內渲染文字。

因此利用CTFramesetter我們可以實現不同樣式的排版,只要定義我們需要的path即可。

 

使用CTFramesetter遍歷CTLine和CTRun,我們先看下面的字符串效果:

第一行文字包括兩種樣式的字形,因此它的CTLine->CTRun為兩個

let textFrame = CTFramesetterCreateFrame(frameSetter, CFRange(location: 0, length: 0), path, attr)
let lines = CTFrameGetLines(textFrame)
let lineCount = CFArrayGetCount(lines)
for i in 0..<lineCount {
    let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, i), to: CTLine.self)
    let runs = CTLineGetGlyphRuns(line)
    let runCount = CFArrayGetCount(runs)
    for j in 0..<runCount {
        let aRun = unsafeBitCast(CFArrayGetValueAtIndex(runs, j), to: CTRun.self)
        let count = CTRunGetGlyphCount(aRun)
        print("[i: \(i), j: \(j)]:glyphCount: \(count)")
    }
}

打印結果如下:

[i: 0, j: 0]:glyphCount: 2
[i: 0, j: 1]:glyphCount: 1
[i: 1, j: 0]:glyphCount: 4
[i: 2, j: 0]:glyphCount: 3

 

我們可以通過CTRun相關如下函數獲取該CTRun內字形的屬性,包括:顏色、字體等

// 獲取CTRun的文本屬性(顏色、字體...)
func CTRunGetAttributes(_ run: CTRun) -> CFDictionary

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM