CoreText 學(xué)習(xí)1

本文主要是用Swift重寫了巧哥博客中的 Demo,博客的原始鏈接如下:

唐巧:
基于 CoreText 的排版引擎:基礎(chǔ)
基于 CoreText 的排版引擎:進(jìn)階

1、能輸出 Hello World 的 CoreText 工程:

class CTSimpleDisplayView: UIView {

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        // 獲取繪圖上下文
        let context = UIGraphicsGetCurrentContext()!
        
        // 翻轉(zhuǎn)坐標(biāo)系推沸。對(duì)于底層的繪制引擎來說,屏幕的左下角是(0, 0)坐標(biāo)。而對(duì)于上層的 UIKit 來說傻粘,左上角是(0, 0)坐標(biāo)。
        context.textMatrix = CGAffineTransform.identity
        context.translateBy(x: 0, y: self.bounds.size.height)
        context.scaleBy(x: 1.0, y: -1.0)
        
        // 初始化繪制路徑
        let path = CGMutablePath()
        path.addRect(self.bounds)
        
        // 初始化需要繪制的文字
        let attString = NSAttributedString(string: "Hello World!")
        
        // 初始化 CTFramesetter
        let framesetter = CTFramesetterCreateWithAttributedString(attString)
        
        // 創(chuàng)建 CTFrame帮掉∠蚁ぃ可以把 CTFrame 理解成畫布,畫布的范圍由 CGPath 決定蟆炊。
        let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), path, nil)
        
        // 繪制
        CTFrameDraw(frame, context)
    }
}

2稽莉、排版引擎框架

按照單一功能原則 (Single responsibility principle),我們將CTDisplayView中的部分內(nèi)容拆開涩搓,由 4 個(gè)類構(gòu)成:

CTFrameParserConfig:用于配置繪制的參數(shù)肩祥,例如:文字顏色,大小缩膝,行間距等混狠。
CTFrameParser:用于生成最后繪制界面需要的CTFrame實(shí)例。
CoreTextData:用于保存由CTFrameParser類生成的CTFrame實(shí)例以及CTFrame實(shí)際繪制需要的高度疾层。
CTDisplayView:持有CoreTextData類的實(shí)例将饺,負(fù)責(zé)將CTFrame繪制到界面上。

關(guān)于這 4 個(gè)類的關(guān)鍵代碼如下:

CTFrameParserConfig

struct CTFrameParserConfig {
    
    var width: CGFloat = 200.0
    var fontSize: CGFloat = 16.0
    var lineSpace = 8.0
    var textColor = UIColor.rgb(108, 108, 108)
}

CTFrameParser

// 用于生成最后繪制界面需要的 CTFrame 實(shí)例
class CTFrameParser: NSObject {

    /// 配置文字信息
    ///
    /// - Parameter config: 配置信息
    /// - Returns: 文字基本屬性
    class func attributes(config: CTFrameParserConfig) -> [String: Any] {
        // 字體大小
        let fontSize = config.fontSize
        let uiFont = UIFont.systemFont(ofSize: fontSize)
        let ctFont = CTFontCreateWithName(uiFont.fontName as CFString?, fontSize, nil)
        // 字體顏色
        let textColor = config.textColor
        
        // 行間距
        var lineSpacing = config.lineSpace
        
        let settings = [
            CTParagraphStyleSetting(spec: .lineSpacingAdjustment, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpacing),
            CTParagraphStyleSetting(spec: .maximumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpacing),
            CTParagraphStyleSetting(spec: .minimumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpacing)
        ]
        let paragraphStyle = CTParagraphStyleCreate(settings, settings.count)
        
        // 封裝
        let dict: [String: Any] = [
            NSForegroundColorAttributeName: textColor,
            NSFontAttributeName: ctFont,
            NSParagraphStyleAttributeName: paragraphStyle
        ]
        
        return dict
    }

    class func parse(content: String, config: CTFrameParserConfig) -> CoreTextData {
        let attributes = self.attributes(config: config)
        let contentString = NSAttributedString(string: content, attributes: attributes)
        
        // 創(chuàng)建 CTFramesetter 實(shí)例
        let framesetter = CTFramesetterCreateWithAttributedString(contentString)
        
        // 獲取要繪制的區(qū)域的高度
        let restrictSize = CGSize(width: config.width, height: CGFloat.greatestFiniteMagnitude)
        let coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil)
        let textHeight = coreTextSize.height
        
        // 生成 CTFrame 實(shí)例
        let frame = self.creatFrame(framesetter: framesetter, config: config, height: textHeight)
        
        // 將生成的 CTFrame 實(shí)例和計(jì)算好的繪制高度保存到 CoreTextData 實(shí)例中
        let data = CoreTextData(ctFrame: frame, height: textHeight)
        
        // 返回 CoreTextData 實(shí)例
        return data
    }

    /// 創(chuàng)建矩形文字區(qū)域
    ///
    /// - Parameters:
    ///   - framesetter: framesetter 文字內(nèi)容
    ///   - config: 配置信息
    ///   - height: 高度
    /// - Returns: 矩形文字區(qū)域
    class func creatFrame(framesetter: CTFramesetter, config: CTFrameParserConfig, height: CGFloat) -> CTFrame {
        let path = CGMutablePath()
        path.addRect(CGRect(x: 0, y: 0, width: config.width, height: height))
        
        return CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
    }
}

CoreTextData

// 用于保存由 CTFrameParser 類生成的 CTFrame 實(shí)例以及 CTFrame 實(shí)際繪制需要的高度
class CoreTextData: NSObject {
    
    var ctFrame: CTFrame
    var height: CGFloat
    
    init(ctFrame: CTFrame, height: CGFloat) {
        self.ctFrame = ctFrame
        self.height = height
    }
}

CTDisplayView

// 持有 CoreTextData 類的實(shí)例,負(fù)責(zé)將 CTFrame 繪制到界面上予弧。
class CTDisplayView: UIView {
       
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        // 獲取繪圖上下文
        let context = UIGraphicsGetCurrentContext()!
        
        // 翻轉(zhuǎn)坐標(biāo)系刮吧。對(duì)于底層的繪制引擎來說,屏幕的左下角是(0, 0)坐標(biāo)掖蛤。而對(duì)于上層的 UIKit 來說杀捻,左上角是(0, 0)坐標(biāo)。
        context.textMatrix = CGAffineTransform.identity
        context.translateBy(x: 0, y: self.bounds.size.height)
        context.scaleBy(x: 1.0, y: -1.0)
        
        if data != nil {
            CTFrameDraw(data!.ctFrame, context)
        }
    }
}

完成以上 4 個(gè)類之后蚓庭,我們就可以簡(jiǎn)單地在ViewController中致讥,加入如下代碼來配置CTDisplayView的顯示內(nèi)容,位置器赞,高度垢袱,字體,顏色等信息港柜。代碼如下所示:

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var displayView: CTDisplayView!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let config = CTFrameParserConfig()
        let data = CTFrameParser.parse(content: "按照以上原則请契,我們將`CTDisplayView`中的部分內(nèi)容拆開。", config: config)
     
        displayView.data = data
    }
}

3夏醉、定制排版文件格式

我們規(guī)定排版的模版文件為JSON格式爽锥,最終我們的CTFrameParser代碼如下:

// 用于生成最后繪制界面需要的 CTFrame 實(shí)例
class CTFrameParser: NSObject {

    /// 解析模板文件
    class func parseTemplateFile(path: String, config: CTFrameParserConfig) -> CoreTextData {
        
        let content = self.loadTemplateFile(path: path, config: config)
        let coreTextData = self.parse(content: content, config: config)
                
        return coreTextData
    }

    /// 加載模板文件
    class func loadTemplateFile(path: String, config: CTFrameParserConfig) -> NSAttributedString {
        
        let result = NSMutableAttributedString()
        
        let url = URL(fileURLWithPath: Bundle.main.path(forResource: path, ofType: "json")!)
        if let data = try? Data(contentsOf: url) {
            
            if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments), let array = jsonObject as? [[String: String]] {
                for item in array {
                    let type = item["type"]
                    
                    if type == "txt" {
                        let subStr = self.parseAttributedCotnentFromDictionary(dict: item, config: config)
                        result.append(subStr)
                    }
                }
            }
        }
        
        return result
    }

    /// 從字典中解析文字富文本信息
    ///
    /// - Parameters:
    ///   - dict: 文字屬性字典
    ///   - config: 配置信息
    /// - Returns: 文字富文本
    class func parseAttributedCotnentFromDictionary(dict: [String: String], config: CTFrameParserConfig) -> NSAttributedString {
  
        var attributes = self.attributes(config: config)
        
        // 設(shè)置文字顏色
        if let colorValue = dict["color"] {
            attributes[NSForegroundColorAttributeName] = UIColor(hexString: colorValue)
        }
        
        // 設(shè)置文字大小
        if let sizeValue = dict["size"] {
            
            if let n = NumberFormatter().number(from: sizeValue) {
                
                if n.intValue > 0 {
                    attributes[NSFontAttributeName] = UIFont.systemFont(ofSize: CGFloat(n))
                }
            }
        }
        
        // 文本
        let contentStr = dict["content"] ?? ""
        
        return NSAttributedString(string: contentStr, attributes: attributes)
    }

    /// 配置文字信息
    ///
    /// - Parameter config: 配置信息
    /// - Returns: 文字基本屬性
    class func attributes(config: CTFrameParserConfig) -> [String: Any] {
        // 字體大小
        let fontSize = config.fontSize
        let uiFont = UIFont.systemFont(ofSize: fontSize)
        let ctFont = CTFontCreateWithName(uiFont.fontName as CFString?, fontSize, nil)
        // 字體顏色
        let textColor = config.textColor
        
        // 行間距
        var lineSpacing = config.lineSpace
        
        let settings = [
            CTParagraphStyleSetting(spec: .lineSpacingAdjustment, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpacing),
            CTParagraphStyleSetting(spec: .maximumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpacing),
            CTParagraphStyleSetting(spec: .minimumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpacing)
        ]
        let paragraphStyle = CTParagraphStyleCreate(settings, settings.count)
        
        // 封裝
        let dict: [String: Any] = [
            NSForegroundColorAttributeName: textColor,
            NSFontAttributeName: ctFont,
            NSParagraphStyleAttributeName: paragraphStyle
        ]
        
        return dict
    }

    class func parse(content: NSAttributedString, config: CTFrameParserConfig) -> CoreTextData {
        // 創(chuàng)建 CTFramesetter 實(shí)例
        let framesetter = CTFramesetterCreateWithAttributedString(content)
        
        // 獲取要繪制的區(qū)域的高度
        let restrictSize = CGSize(width: config.width, height: CGFloat.greatestFiniteMagnitude)
        let coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil)
        let textHeight = coreTextSize.height
        
        // 生成 CTFrame 實(shí)例
        let frame = self.creatFrame(framesetter: framesetter, config: config, height: textHeight)
        
        // 將生成的 CTFrame 實(shí)例和計(jì)算好的繪制高度保存到 CoreTextData 實(shí)例中
        let data = CoreTextData(ctFrame: frame, height: textHeight)
        
        // 返回 CoreTextData 實(shí)例
        return data
    }
    
    
    /// 創(chuàng)建矩形文字區(qū)域
    ///
    /// - Parameters:
    ///   - framesetter: framesetter 文字內(nèi)容
    ///   - config: 配置信息
    ///   - height: 高度
    /// - Returns: 矩形文字區(qū)域
    class func creatFrame(framesetter: CTFramesetter, config: CTFrameParserConfig, height: CGFloat) -> CTFrame {
        let path = CGMutablePath()
        path.addRect(CGRect(x: 0, y: 0, width: config.width, height: height))
        
        return CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市畔柔,隨后出現(xiàn)的幾起案子氯夷,更是在濱河造成了極大的恐慌,老刑警劉巖释树,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肠槽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡奢啥,警方通過查閱死者的電腦和手機(jī)秸仙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桩盲,“玉大人寂纪,你說我怎么就攤上這事《慕幔” “怎么了捞蛋?”我有些...
    開封第一講書人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長柬姚。 經(jīng)常有香客問我拟杉,道長,這世上最難降的妖魔是什么量承? 我笑而不...
    開封第一講書人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任搬设,我火速辦了婚禮穴店,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拿穴。我一直安慰自己泣洞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開白布默色。 她就那樣靜靜地躺著球凰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腿宰。 梳的紋絲不亂的頭發(fā)上呕诉,一...
    開封第一講書人閱讀 49,837評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音酗失,去河邊找鬼义钉。 笑死昧绣,一個(gè)胖子當(dāng)著我的面吹牛规肴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播夜畴,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼拖刃,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了贪绘?” 一聲冷哼從身側(cè)響起兑牡,我...
    開封第一講書人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎税灌,沒想到半個(gè)月后均函,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菱涤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年苞也,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粘秆。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡如迟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出攻走,到底是詐尸還是另有隱情殷勘,我是刑警寧澤,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布昔搂,位于F島的核電站玲销,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏摘符。R本人自食惡果不足惜贤斜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一淳附、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蠢古,春花似錦奴曙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至堕战,卻和暖如春坤溃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嘱丢。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來泰國打工薪介, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人越驻。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓汁政,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缀旁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子记劈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件并巍、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,066評(píng)論 4 62
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,790評(píng)論 25 707
  • Swift版本點(diǎn)擊這里歡迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh閱讀 25,334評(píng)論 7 249
  • 天冷的時(shí)候,身體漸次張開你以一種自然的姿態(tài)抵達(dá)我內(nèi)心巨大的秘密剃执,順流而下 坐在河邊的我們誓禁,喝啤酒抽老牌子香煙。偶爾...
    茶人老七閱讀 158評(píng)論 0 1
  • 據(jù)說到目前為止忠蝗,10部手機(jī)中就有9部裝有微信现横,微信已經(jīng)成為了我們每天生活的一部分,我們每天都會(huì)花很多的時(shí)間在微信上...
    產(chǎn)品范閱讀 460評(píng)論 2 13