CoreText是apple提供的处理文字和字体的底层技术。他直接和Quartz
打交道,Quartz
能够处理OSX和iOS中的图形显示。
Quartz
能够处理字体、字形,将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此CoreText
为了排版,需要将文本的内容、位置、字体、字形直接传递给Quartz
。相比于苹果提供的UI框架中的组件,CoreText
直接和Quartz
来进行交互,具有较高的排版效果。
1.苹果的基础框架
coretext_arch.png从上图中,可以看出苹果的
CoreText
处于Core Graphics
的上一层,处于Text Kit
和WebKit
的下一层,UI组件和UIWebview处于更高的层,且都是基于CoreText
的上层,可以断定CoreText
应该会有更高的定制化以及更高的效率。
CoreText的框架
core_text底层框架.png- CTFrame可以理解过画布,画布的大小有CGPath决定
- CTFrame由很多CTLine组成, CTLine表示为一行
- CTLine由多个CTRun组成, CTRun相当于一行中的多个块, 但是CTRun不需要你自己创建, 由NSAttributedString的属性决定, 系统自动生成。每个CTRun对应不同属性
- CTFramesetter是一个工厂, 创建CTFrame, 一个界面上可以有多个CTFrame
2.建立一个输出Hello World的工程
项目很简单,我们不说废话,直接写一下核心的代码。
创建一个功能,新建一个CTDisplayView.swift
重写一下draw(_ rect: CGRect)
,然后把这个View贴出来就可以了。
我们来看一下draw(_ rect: CGRect)
里面的代码
class CTDisplayView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
// 1.获取上下文
let context = UIGraphicsGetCurrentContext()
// 2.转换坐标
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)
//设置行间距
let style = NSMutableParagraphStyle()
style.lineSpacing = 10
//5. 设置CTFramesetter,创建CTFrame
let frameSetter = CTFramesetterCreateWithAttributedString(mutableAttrStr)
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, mutableAttrStr.length), path, nil)
//6. 画出来
CTFrameDraw(frame, context!)
}
}
先看一下run起来的输出再分析代码
HelloWorld.jpeg
代码其实很简单,首先获取上下文,用于后续将内容绘制在画布上。然后将坐标系上下翻转,这是因为对于底层的绘制引擎来说,屏幕的左下角是(0, 0)坐标。而对于上层的 UIKit 来说,左上角是 (0, 0) 坐标。所以我们为了之后的坐标系描述按 UIKit 来做,所以先在这里做一个坐标系的上下翻转操作。翻转之后,底层和上层的 (0, 0) 坐标就是重合的了。
我们现在翻转坐标系的代码注释掉,会看到这样的效果。
翻转HelloWorld.jpeg
- 绘制区域,CoreText支持各种文字排版区域,我们这里简单的讲view的边界作为了排版区域,可以看下面一个例子,看一下CoreText是如何支持文字排版区域的
// 3获取路径
let path = CGMutablePath()
path.addEllipse(in: self.bounds)
// 4. 文本
let str = "Hello World! 创建绘制的区域,CoreText 本身支持各种文字排版的区域,我们这里简单地将 UIView 的整个界面作为排版的区域。 为了加深理解,建议读者将该步骤的代码替换成如下代码, 测试设置不同的绘制区域带来的界面变化。"
效果图如下:
椭圆区域.jpeg
3.做一个简单的排版引擎
从上面的demo中可以看出CoreText具有排版的能力,我们简单的把所有的代码都放在了draw(_ rect: CGRect)中,这样显然是不合理的。下面我们尝试做几个模块,把不同的功能都放到各自不同的类里面。
对于一个复杂的排版引擎来说,可以将其功能拆成以下几个类来完成:
一个显示用的类,仅负责显示内容,不负责排版
一个模型类,用于承载显示所需要的所有数据
一个排版类,用于实现文字内容的排版
一个配置类,用于实现一些排版时的可配置项
按照以上原则,我们将CTDisplayView中的部分内容拆开,由 4 个类构成:
CTFrameParserConfig类,用于配置绘制的参数,例如:文字颜色,大小,行间距等。
CTFrameParser类,用于生成最后绘制界面需要的CTFrameRef实例。
CoreTextData类,用于保存由CTFrameParser类生成的CTFrameRef实例以及CTFrameRef实际绘制需要的高度。
CTDisplayView类,持有CoreTextData类的实例,负责将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])
// 创建 CTFramesetterRef 实例
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
// 生成 CTFrameRef 实例
let frame = self.createFrameWithFramesetter(framesetter: frameSetter, config: config, height: textHeight)
// 将生成好的 CTFrameRef 实例和计算好的绘制高度保存到 CoreTextData 实例中,最后返回 CoreTextData 实例
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.转换坐标
context?.textMatrix = .identity
context?.translateBy(x: 0, y: self.bounds.size.height)
context?.scaleBy(x: 1.0, y: -1.0)
// 根据数据绘制
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中的部分内容拆开。", 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
}
}
运行起来看一下效果:
WechatIMG8.jpeg
如果我们希望给一段话的不同的区域设置不同的颜色,其实只要设置attributedString的属性就可以实现了:
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 增加了一个将 NSString 转 换为 CoreTextData 的方法。 但这样的实现方式有很多局限性,因为整个内容虽然可以定制字体 大小,颜色,行高等信息,但是却不能支持定制内容中的某一部分。 例如,如果我们只想让内容的前三个字显示成红色,而其它文字显 示成黑色,那么就办不到了。\n\n 解决的办法很简单,我们让`CTFrameParser`支持接受 NSAttributeString 作为参数,然后在 NSAttributeString 中设置好 我们想要的信息。"
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这个类中需要加一个方法:
class func parseAttributedContent(content: NSAttributedString, config: CTFrameParserConfig) -> CoreTextData {
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
// 生成 CTFrameRef 实例
let frame = self.createFrameWithFramesetter(framesetter: frameSetter, config: config, height: textHeight)
// 将生成好的 CTFrameRef 实例和计算好的绘制高度保存到 CoreTextData 实例中,最后返回 CoreTextData 实例
let data = CoreTextData()
data.ctFrame = frame
data.height = textHeight
return data
}
运行起来看一下:
WechatIMG9.jpeg
就目前来看,我们现在做的这个view基于CoreText
具有绘制文本的能力,也具有给文本不同的区域添加文字属性的能力,但是用起来其实是非常不方便的,在真正的项目中,我们其实希望有一套规则,有一个排版的文件来设置要文字的字体,颜色等信息。现在我们基于常用的json格式来做一下这件事。
[ { "color" : "blue",
"content" : " 更进一步地,实际工作中,我们更希望通过一个排版文件,来设置需要排版的文字的 ",
"size" : 16,
"type" : "txt"
},
{ "color" : "red",
"content" : " 内容、颜色、字体 ",
"size" : 22,
"type" : "txt"
},
{ "color" : "black",
"content" : " 大小等信息。\n",
"size" : 16,
"type" : "txt"
},
{ "color" : "default",
"content" : " 我在开发猿题库应用时,自己定义了一个基于 UBB 的排版模版,但是实现该排版文件的解析器要花费大量的篇幅,考虑到这并不是本章的重点,所以我们以一个较简单的排版文件来讲解其思想。",
"type" : "txt"
}
]
根据这个模板,我们实现一套规则代码来解析文字,并且将其显示:
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])
// 创建 CTFramesetterRef 实例
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
// 生成 CTFrameRef 实例
let frame = self.createFrameWithFramesetter(framesetter: frameSetter, config: config, height: textHeight)
// 将生成好的 CTFrameRef 实例和计算好的绘制高度保存到 CoreTextData 实例中,最后返回 CoreTextData 实例
let data = CoreTextData()
data.ctFrame = frame
data.height = textHeight
return data
}
class func parseAttributedContent(content: NSAttributedString, config: CTFrameParserConfig) -> CoreTextData {
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
// 生成 CTFrameRef 实例
let frame = self.createFrameWithFramesetter(framesetter: frameSetter, config: config, height: textHeight)
// 将生成好的 CTFrameRef 实例和计算好的绘制高度保存到 CoreTextData 实例中,最后返回 CoreTextData 实例
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文件的路径,但是实际的工作中这些信息可能来源于网络:
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
}
}
看一下效果:
WechatIMG10.jpeg
到现在为止,我们的代码实现了根据文件模板来显示文字内容,并根据模板提供的信息显示不容的颜色。后面我们要把这个view做成可以显示图片,并支持图片的点击,支持链接点击,支持数据号码识别,网页地址链接识别这样的一个控件,一起期待吧~~~
参考:
http://yangchao0033.github.io/blog/2016/01/26/coretextji-chu/
网友评论