本文主要是用Swift
重寫了巧哥博客中的 Demo,博客的原始鏈接如下:
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)
}
}