CoreText是apple提供的處理文字和字體的底層技術(shù)。他直接和Quartz
打交道,Quartz
能夠處理OSX和iOS中的圖形顯示极颓。
Quartz
能夠處理字體、字形躯肌,將文字渲染到界面上,它是基礎(chǔ)庫中唯一能夠處理字形的模塊校仑。因此CoreText
為了排版卦方,需要將文本的內(nèi)容、位置、字體近刘、字形直接傳遞給Quartz
徽惋。相比于蘋果提供的UI框架中的組件踢京,CoreText
直接和Quartz
來進(jìn)行交互,具有較高的排版效果。
1.蘋果的基礎(chǔ)框架
從上圖中,可以看出蘋果的
CoreText
處于Core Graphics
的上一層厦取,處于Text Kit
和WebKit
的下一層更鲁,UI組件和UIWebview處于更高的層,且都是基于CoreText
的上層,可以斷定CoreText
應(yīng)該會(huì)有更高的定制化以及更高的效率。
CoreText的框架
- CTFrame可以理解過畫布,畫布的大小有CGPath決定
- CTFrame由很多CTLine組成, CTLine表示為一行
- CTLine由多個(gè)CTRun組成, CTRun相當(dāng)于一行中的多個(gè)塊, 但是CTRun不需要你自己創(chuàng)建, 由NSAttributedString的屬性決定, 系統(tǒng)自動(dòng)生成指蚜。每個(gè)CTRun對應(yīng)不同屬性
- CTFramesetter是一個(gè)工廠, 創(chuàng)建CTFrame, 一個(gè)界面上可以有多個(gè)CTFrame
2.建立一個(gè)輸出Hello World的工程
項(xiàng)目很簡單,我們不說廢話,直接寫一下核心的代碼。
創(chuàng)建一個(gè)功能棺聊,新建一個(gè)CTDisplayView.swift
重寫一下draw(_ rect: CGRect)
,然后把這個(gè)View貼出來就可以了祟同。
我們來看一下draw(_ rect: CGRect)
里面的代碼
class CTDisplayView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
// 1.獲取上下文
let context = UIGraphicsGetCurrentContext()
// 2.轉(zhuǎn)換坐標(biāo)
context?.textMatrix = .identity
context?.translateBy(x: 0, y: self.bounds.size.height)
context?.scaleBy(x: 1.0, y: -1.0)
// 3獲取路徑
let path = CGMutablePath()
path.addRect(self.bounds)
// 4. 文本
let str = "Hello world"
let mutableAttrStr = NSMutableAttributedString(string: str)
//設(shè)置行間距
let style = NSMutableParagraphStyle()
style.lineSpacing = 10
//5. 設(shè)置CTFramesetter砖顷,創(chuàng)建CTFrame
let frameSetter = CTFramesetterCreateWithAttributedString(mutableAttrStr)
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, mutableAttrStr.length), path, nil)
//6. 畫出來
CTFrameDraw(frame, context!)
}
}
先看一下run起來的輸出再分析代碼
代碼其實(shí)很簡單房轿,首先獲取上下文,用于后續(xù)將內(nèi)容繪制在畫布上掩幢。然后將坐標(biāo)系上下翻轉(zhuǎn)世曾,這是因?yàn)閷τ诘讓拥睦L制引擎來說,屏幕的左下角是(0, 0)坐標(biāo)。而對于上層的 UIKit 來說优俘,左上角是 (0, 0) 坐標(biāo)叶雹。所以我們?yōu)榱酥蟮淖鴺?biāo)系描述按 UIKit 來做谦炒,所以先在這里做一個(gè)坐標(biāo)系的上下翻轉(zhuǎn)操作。翻轉(zhuǎn)之后风喇,底層和上層的 (0, 0) 坐標(biāo)就是重合的了宁改。
我們現(xiàn)在翻轉(zhuǎn)坐標(biāo)系的代碼注釋掉,會(huì)看到這樣的效果魂莫。
- 繪制區(qū)域还蹲,CoreText支持各種文字排版區(qū)域,我們這里簡單的講view的邊界作為了排版區(qū)域耙考,可以看下面一個(gè)例子秽誊,看一下CoreText是如何支持文字排版區(qū)域的
// 3獲取路徑
let path = CGMutablePath()
path.addEllipse(in: self.bounds)
// 4. 文本
let str = "Hello World! 創(chuàng)建繪制的區(qū)域,CoreText 本身支持各種文字排版的區(qū)域琳骡,我們這里簡單地將 UIView 的整個(gè)界面作為排版的區(qū)域锅论。 為了加深理解,建議讀者將該步驟的代碼替換成如下代碼楣号, 測試設(shè)置不同的繪制區(qū)域帶來的界面變化最易。"
效果圖如下:
3.做一個(gè)簡單的排版引擎
從上面的demo中可以看出CoreText具有排版的能力怒坯,我們簡單的把所有的代碼都放在了draw(_ rect: CGRect)中,這樣顯然是不合理的藻懒。下面我們嘗試做幾個(gè)模塊剔猿,把不同的功能都放到各自不同的類里面。
對于一個(gè)復(fù)雜的排版引擎來說嬉荆,可以將其功能拆成以下幾個(gè)類來完成:
一個(gè)顯示用的類归敬,僅負(fù)責(zé)顯示內(nèi)容,不負(fù)責(zé)排版
一個(gè)模型類鄙早,用于承載顯示所需要的所有數(shù)據(jù)
一個(gè)排版類汪茧,用于實(shí)現(xiàn)文字內(nèi)容的排版
一個(gè)配置類,用于實(shí)現(xiàn)一些排版時(shí)的可配置項(xiàng)
按照以上原則限番,我們將CTDisplayView中的部分內(nèi)容拆開舱污,由 4 個(gè)類構(gòu)成:
CTFrameParserConfig類,用于配置繪制的參數(shù)弥虐,例如:文字顏色扩灯,大小,行間距等霜瘪。
CTFrameParser類珠插,用于生成最后繪制界面需要的CTFrameRef實(shí)例。
CoreTextData類颖对,用于保存由CTFrameParser類生成的CTFrameRef實(shí)例以及CTFrameRef實(shí)際繪制需要的高度捻撑。
CTDisplayView類,持有CoreTextData類的實(shí)例惜互,負(fù)責(zé)將CTFrameRef繪制到界面上布讹。
下面我們把這些代碼貼出來讀一下:
class CTFrameParserConfig: NSObject {
var width: CGFloat
var fontSize: CGFloat
var lineSpace: CGFloat
var textColor: UIColor
override init() {
self.width = 200.0
self.fontSize = 16.0
self.lineSpace = 8.0
self.textColor = UIColor.init(colorLiteralRed: 108, green: 108, blue: 108, alpha: 1)
}
}
class CTFrameParser: NSObject {
class func parseContent(content: NSString, config: CTFrameParserConfig) -> CoreTextData {
let attributes = self.attributesWithConfig(config: config)
let contentString = NSAttributedString(string: content as String, attributes: attributes as! [String : Any])
// 創(chuàng)建 CTFramesetterRef 實(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
// 生成 CTFrameRef 實(shí)例
let frame = self.createFrameWithFramesetter(framesetter: frameSetter, config: config, height: textHeight)
// 將生成好的 CTFrameRef 實(shí)例和計(jì)算好的繪制高度保存到 CoreTextData 實(shí)例中琳拭,最后返回 CoreTextData 實(shí)例
let data = CoreTextData()
data.ctFrame = frame
data.height = textHeight
return data
}
class func attributesWithConfig(config: CTFrameParserConfig) -> NSDictionary {
let fontSize = config.fontSize
let fontRef = CTFontCreateWithName("ArialMT" as CFString?, fontSize, nil)
var lineSpace = config.lineSpace
var theSettings: [CTParagraphStyleSetting] = [CTParagraphStyleSetting]()
let theSettingLine = CTParagraphStyleSetting(spec: .lineSpacingAdjustment, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace)
theSettings.append(theSettingLine)
let theSettingMax = CTParagraphStyleSetting(spec: .maximumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace)
theSettings.append(theSettingMax)
let theSettingMin = CTParagraphStyleSetting(spec: .minimumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace)
theSettings.append(theSettingMin)
let theParagraphRef = CTParagraphStyleCreate(theSettings, 3)
let textColor = config.textColor
let dict = NSMutableDictionary()
dict[kCTForegroundColorAttributeName] = textColor.cgColor
dict[kCTFontAttributeName] = fontRef
dict[kCTParagraphStyleAttributeName] = theParagraphRef
return dict
}
class func createFrameWithFramesetter(framesetter: CTFramesetter, config: CTFrameParserConfig, height: CGFloat) -> CTFrame {
let path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: config.width, height: height))
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
return frame
}
}
class CoreTextData: NSObject {
var ctFrame: CTFrame?
var height: CGFloat?
}
class CTDisplayView: UIView {
var data: CoreTextData?
override func draw(_ rect: CGRect) {
super.draw(rect)
// 1.獲取上下文
let context = UIGraphicsGetCurrentContext()
// 2.轉(zhuǎn)換坐標(biāo)
context?.textMatrix = .identity
context?.translateBy(x: 0, y: self.bounds.size.height)
context?.scaleBy(x: 1.0, y: -1.0)
// 根據(jù)數(shù)據(jù)繪制
if((self.data) != nil) {
CTFrameDraw((data?.ctFrame)!, context!)
}
}
}
在使用CTDisplayView的viewcontroller里面我只需要這樣做就可以了:
class ViewController: UIViewController {
@IBOutlet weak var ctView: CTDisplayView!
override func viewDidLoad() {
super.viewDidLoad()
let config = CTFrameParserConfig()
config.textColor = UIColor.red
config.width = self.ctView.frame.size.width
let data = CTFrameParser.parseContent(content: " 按照以上原則训堆,我們將CTDisplayView中的部分內(nèi)容拆開。", config: config)
self.ctView.data = data
// self.ctView.frame = CGRect(self.ctView.frame.origin.x, self.ctView.frame.origin.y, self.ctView.frame.size.width, data.height)
self.ctView.setHeight(data.height!)
self.ctView.backgroundColor = UIColor.yellow
}
}
運(yùn)行起來看一下效果:
如果我們希望給一段話的不同的區(qū)域設(shè)置不同的顏色白嘁,其實(shí)只要設(shè)置attributedString的屬性就可以實(shí)現(xiàn)了:
class ViewController: UIViewController {
@IBOutlet weak var ctView: CTDisplayView!
override func viewDidLoad() {
super.viewDidLoad()
let config = CTFrameParserConfig()
config.textColor = UIColor.black
config.width = self.ctView.frame.size.width
let content = " 對于上面的例子坑鱼,我們給 CTFrameParser 增加了一個(gè)將 NSString 轉(zhuǎn) 換為 CoreTextData 的方法。 但這樣的實(shí)現(xiàn)方式有很多局限性絮缅,因?yàn)檎麄€(gè)內(nèi)容雖然可以定制字體 大小鲁沥,顏色,行高等信息耕魄,但是卻不能支持定制內(nèi)容中的某一部分画恰。 例如,如果我們只想讓內(nèi)容的前三個(gè)字顯示成紅色吸奴,而其它文字顯 示成黑色允扇,那么就辦不到了缠局。\n\n 解決的辦法很簡單,我們讓`CTFrameParser`支持接受 NSAttributeString 作為參數(shù)考润,然后在 NSAttributeString 中設(shè)置好 我們想要的信息狭园。"
let attr = CTFrameParser.attributesWithConfig(config: config)
let attributedString = NSMutableAttributedString(string: content, attributes: attr as? [String : Any])
attributedString.addAttributes([kCTForegroundColorAttributeName as String: UIColor.red], range: NSMakeRange(0, 8))
let data = CTFrameParser.parseAttributedContent(content: attributedString, config: config)
self.ctView.data = data
self.ctView.setHeight(data.height!)
self.ctView.backgroundColor = UIColor.yellow
}
}
在CTFrameParser這個(gè)類中需要加一個(gè)方法:
class func parseAttributedContent(content: NSAttributedString, config: CTFrameParserConfig) -> CoreTextData {
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
// 生成 CTFrameRef 實(shí)例
let frame = self.createFrameWithFramesetter(framesetter: frameSetter, config: config, height: textHeight)
// 將生成好的 CTFrameRef 實(shí)例和計(jì)算好的繪制高度保存到 CoreTextData 實(shí)例中,最后返回 CoreTextData 實(shí)例
let data = CoreTextData()
data.ctFrame = frame
data.height = textHeight
return data
}
運(yùn)行起來看一下:
就目前來看糊治,我們現(xiàn)在做的這個(gè)view基于CoreText
具有繪制文本的能力唱矛,也具有給文本不同的區(qū)域添加文字屬性的能力,但是用起來其實(shí)是非常不方便的井辜,在真正的項(xiàng)目中绎谦,我們其實(shí)希望有一套規(guī)則,有一個(gè)排版的文件來設(shè)置要文字的字體抑胎,顏色等信息≡锘現(xiàn)在我們基于常用的json格式來做一下這件事。
[ { "color" : "blue",
"content" : " 更進(jìn)一步地阿逃,實(shí)際工作中铭拧,我們更希望通過一個(gè)排版文件,來設(shè)置需要排版的文字的 ",
"size" : 16,
"type" : "txt"
},
{ "color" : "red",
"content" : " 內(nèi)容恃锉、顏色搀菩、字體 ",
"size" : 22,
"type" : "txt"
},
{ "color" : "black",
"content" : " 大小等信息。\n",
"size" : 16,
"type" : "txt"
},
{ "color" : "default",
"content" : " 我在開發(fā)猿題庫應(yīng)用時(shí)破托,自己定義了一個(gè)基于 UBB 的排版模版肪跋,但是實(shí)現(xiàn)該排版文件的解析器要花費(fèi)大量的篇幅,考慮到這并不是本章的重點(diǎn)土砂,所以我們以一個(gè)較簡單的排版文件來講解其思想州既。",
"type" : "txt"
}
]
根據(jù)這個(gè)模板,我們實(shí)現(xiàn)一套規(guī)則代碼來解析文字萝映,并且將其顯示:
class CTFrameParser: NSObject {
class func parseContent(content: NSString, config: CTFrameParserConfig) -> CoreTextData {
let attributes = self.attributesWithConfig(config: config)
let contentString = NSAttributedString(string: content as String, attributes: attributes as? [String : Any])
// 創(chuàng)建 CTFramesetterRef 實(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
// 生成 CTFrameRef 實(shí)例
let frame = self.createFrameWithFramesetter(framesetter: frameSetter, config: config, height: textHeight)
// 將生成好的 CTFrameRef 實(shí)例和計(jì)算好的繪制高度保存到 CoreTextData 實(shí)例中吴叶,最后返回 CoreTextData 實(shí)例
let data = CoreTextData()
data.ctFrame = frame
data.height = textHeight
return data
}
class func parseAttributedContent(content: NSAttributedString, config: CTFrameParserConfig) -> CoreTextData {
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
// 生成 CTFrameRef 實(shí)例
let frame = self.createFrameWithFramesetter(framesetter: frameSetter, config: config, height: textHeight)
// 將生成好的 CTFrameRef 實(shí)例和計(jì)算好的繪制高度保存到 CoreTextData 實(shí)例中,最后返回 CoreTextData 實(shí)例
let data = CoreTextData()
data.ctFrame = frame
data.height = textHeight
return data
}
class func parseTemplateFile(path: String, config: CTFrameParserConfig) -> CoreTextData {
let content = self.loadTemplateFile(path: path as NSString, config: config)
let data = self.parseAttributedContent(content: content, config: config)
return data
}
class func loadTemplateFile(path: NSString, config: CTFrameParserConfig) ->NSAttributedString {
let fileContent = try? NSString(contentsOfFile: path as String, encoding: String.Encoding.utf8.rawValue)
let data = NSData(contentsOfFile: path as! String)
let result = NSMutableAttributedString()
do {
let array = try JSONSerialization.jsonObject(with: data as! Data , options: [JSONSerialization.ReadingOptions.init(rawValue: 0)]) as? NSArray
for item in array! {
let dict = item as! NSDictionary
let type = dict.object(forKey: "type") as! String
if type == "txt" {
let attributeString = self.parseAttributedContentFromNSDictionary(dict: dict, config: config)
result.append(attributeString)
}
}
} catch _ as NSError {
}
return result
}
class func parseAttributedContentFromNSDictionary(dict: NSDictionary, config: CTFrameParserConfig) -> NSAttributedString {
var attributes = self.attributesWithConfig(config: config) as! [String:Any]
let color = self.colorFromTemplate(name: dict.object(forKey: "color") as! NSString)
attributes[kCTForegroundColorAttributeName as String] = color.cgColor;
let contet = dict.object(forKey: "content")
let attributedString = NSAttributedString(string: contet as! String, attributes: attributes)
return attributedString
}
class func attributesWithConfig(config: CTFrameParserConfig) -> NSDictionary {
let fontSize = config.fontSize
let fontRef = CTFontCreateWithName("ArialMT" as CFString?, fontSize, nil)
var lineSpace = config.lineSpace
var theSettings: [CTParagraphStyleSetting] = [CTParagraphStyleSetting]()
let theSettingLine = CTParagraphStyleSetting(spec: .lineSpacingAdjustment, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace)
theSettings.append(theSettingLine)
let theSettingMax = CTParagraphStyleSetting(spec: .maximumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace)
theSettings.append(theSettingMax)
let theSettingMin = CTParagraphStyleSetting(spec: .minimumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace)
theSettings.append(theSettingMin)
let theParagraphRef = CTParagraphStyleCreate(theSettings, 3)
let textColor = config.textColor
let dict = NSMutableDictionary()
dict[kCTForegroundColorAttributeName] = textColor.cgColor
dict[kCTFontAttributeName] = fontRef
dict[kCTParagraphStyleAttributeName] = theParagraphRef
return dict
}
class func colorFromTemplate(name: NSString) -> UIColor {
if (name == "blue") {
return UIColor.blue
} else if (name == "red") {
return UIColor.red
} else {
return UIColor.black
}
}
// 生成 CTFrame
class func createFrameWithFramesetter(framesetter: CTFramesetter, config: CTFrameParserConfig, height: CGFloat) -> CTFrame {
let path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: config.width, height: height))
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
return frame
}
}
在viewcontroller里面序臂,我們需要提供json文件的路徑蚌卤,但是實(shí)際的工作中這些信息可能來源于網(wǎng)絡(luò):
class ViewController: UIViewController {
@IBOutlet weak var ctView: CTDisplayView!
override func viewDidLoad() {
super.viewDidLoad()
let config = CTFrameParserConfig()
config.textColor = UIColor.black
config.width = self.ctView.frame.size.width
let path = Bundle.main.path(forResource: "content", ofType: "json")! as String
let data = CTFrameParser.parseTemplateFile(path: path, config: config)
self.ctView.data = data
self.ctView.setHeight(data.height!)
self.ctView.backgroundColor = UIColor.yellow
}
}
看一下效果:
到現(xiàn)在為止,我們的代碼實(shí)現(xiàn)了根據(jù)文件模板來顯示文字內(nèi)容奥秆,并根據(jù)模板提供的信息顯示不容的顏色逊彭。后面我們要把這個(gè)view做成可以顯示圖片,并支持圖片的點(diǎn)擊构订,支持鏈接點(diǎn)擊侮叮,支持?jǐn)?shù)據(jù)號(hào)碼識(shí)別,網(wǎng)頁地址鏈接識(shí)別這樣的一個(gè)控件悼瘾,一起期待吧~~~
參考:
http://yangchao0033.github.io/blog/2016/01/26/coretextji-chu/