本文主要是用Swift
重写了巧哥博客中的 Demo,博客的原始链接如下:
唐巧:
基于 CoreText 的排版引擎:基础
基于 CoreText 的排版引擎:进阶
1、能输出 Hello World 的 CoreText 工程:
class CTSimpleDisplayView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
// 获取绘图上下文
let context = UIGraphicsGetCurrentContext()!
// 翻转坐标系。对于底层的绘制引擎来说,屏幕的左下角是(0, 0)坐标。而对于上层的 UIKit 来说,左上角是(0, 0)坐标。
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)
// 创建 CTFrame。可以把 CTFrame 理解成画布,画布的范围由 CGPath 决定。
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attString.length), path, nil)
// 绘制
CTFrameDraw(frame, context)
}
}
2、排版引擎框架
按照单一功能原则 (Single responsibility principle)
,我们将CTDisplayView
中的部分内容拆开,由 4 个类构成:
CTFrameParserConfig
:用于配置绘制的参数,例如:文字颜色,大小,行间距等。
CTFrameParser
:用于生成最后绘制界面需要的CTFrame
实例。
CoreTextData
:用于保存由CTFrameParser
类生成的CTFrame
实例以及CTFrame
实际绘制需要的高度。
CTDisplayView
:持有CoreTextData
类的实例,负责将CTFrame
绘制到界面上。
关于这 4 个类的关键代码如下:
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 实例
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.size, value: &lineSpacing),
CTParagraphStyleSetting(spec: .maximumLineSpacing, valueSize: MemoryLayout.size, value: &lineSpacing),
CTParagraphStyleSetting(spec: .minimumLineSpacing, valueSize: MemoryLayout.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)
// 创建 CTFramesetter 实例
let framesetter = CTFramesetterCreateWithAttributedString(contentString)
// 获取要绘制的区域的高度
let restrictSize = CGSize(width: config.width, height: CGFloat.greatestFiniteMagnitude)
let coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil)
let textHeight = coreTextSize.height
// 生成 CTFrame 实例
let frame = self.creatFrame(framesetter: framesetter, config: config, height: textHeight)
// 将生成的 CTFrame 实例和计算好的绘制高度保存到 CoreTextData 实例中
let data = CoreTextData(ctFrame: frame, height: textHeight)
// 返回 CoreTextData 实例
return data
}
/// 创建矩形文字区域
///
/// - Parameters:
/// - framesetter: framesetter 文字内容
/// - config: 配置信息
/// - height: 高度
/// - Returns: 矩形文字区域
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 实例以及 CTFrame 实际绘制需要的高度
class CoreTextData: NSObject {
var ctFrame: CTFrame
var height: CGFloat
init(ctFrame: CTFrame, height: CGFloat) {
self.ctFrame = ctFrame
self.height = height
}
}
CTDisplayView
:
// 持有 CoreTextData 类的实例,负责将 CTFrame 绘制到界面上。
class CTDisplayView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
// 获取绘图上下文
let context = UIGraphicsGetCurrentContext()!
// 翻转坐标系。对于底层的绘制引擎来说,屏幕的左下角是(0, 0)坐标。而对于上层的 UIKit 来说,左上角是(0, 0)坐标。
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 个类之后,我们就可以简单地在ViewController
中,加入如下代码来配置CTDisplayView
的显示内容,位置,高度,字体,颜色等信息。代码如下所示:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var displayView: CTDisplayView!
override func viewDidLoad() {
super.viewDidLoad()
let config = CTFrameParserConfig()
let data = CTFrameParser.parse(content: "按照以上原则,我们将`CTDisplayView`中的部分内容拆开。", config: config)
displayView.data = data
}
}
3、定制排版文件格式
我们规定排版的模版文件为JSON
格式,最终我们的CTFrameParser
代码如下:
// 用于生成最后绘制界面需要的 CTFrame 实例
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)
// 设置文字颜色
if let colorValue = dict["color"] {
attributes[NSForegroundColorAttributeName] = UIColor(hexString: colorValue)
}
// 设置文字大小
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.size, value: &lineSpacing),
CTParagraphStyleSetting(spec: .maximumLineSpacing, valueSize: MemoryLayout.size, value: &lineSpacing),
CTParagraphStyleSetting(spec: .minimumLineSpacing, valueSize: MemoryLayout.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 {
// 创建 CTFramesetter 实例
let framesetter = CTFramesetterCreateWithAttributedString(content)
// 获取要绘制的区域的高度
let restrictSize = CGSize(width: config.width, height: CGFloat.greatestFiniteMagnitude)
let coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil)
let textHeight = coreTextSize.height
// 生成 CTFrame 实例
let frame = self.creatFrame(framesetter: framesetter, config: config, height: textHeight)
// 将生成的 CTFrame 实例和计算好的绘制高度保存到 CoreTextData 实例中
let data = CoreTextData(ctFrame: frame, height: textHeight)
// 返回 CoreTextData 实例
return data
}
/// 创建矩形文字区域
///
/// - Parameters:
/// - framesetter: framesetter 文字内容
/// - config: 配置信息
/// - height: 高度
/// - Returns: 矩形文字区域
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)
}
}