CoreText(一)

CoreText是apple提供的处理文字和字体的底层技术。他直接和Quartz打交道,Quartz能够处理OSX和iOS中的图形显示。
Quartz能够处理字体、字形,将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此CoreText为了排版,需要将文本的内容、位置、字体、字形直接传递给Quartz。相比于苹果提供的UI框架中的组件,CoreText直接和Quartz来进行交互,具有较高的排版效果。

1.苹果的基础框架

CoreText(一)_第1张图片
coretext_arch.png

从上图中,可以看出苹果的 CoreText处于 Core Graphics的上一层,处于 Text KitWebKit的下一层,UI组件和UIWebview处于更高的层,且都是基于 CoreText的上层,可以断定 CoreText应该会有更高的定制化以及更高的效率。

CoreText的框架
CoreText(一)_第2张图片
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起来的输出再分析代码


CoreText(一)_第3张图片
HelloWorld.jpeg

代码其实很简单,首先获取上下文,用于后续将内容绘制在画布上。然后将坐标系上下翻转,这是因为对于底层的绘制引擎来说,屏幕的左下角是(0, 0)坐标。而对于上层的 UIKit 来说,左上角是 (0, 0) 坐标。所以我们为了之后的坐标系描述按 UIKit 来做,所以先在这里做一个坐标系的上下翻转操作。翻转之后,底层和上层的 (0, 0) 坐标就是重合的了。
我们现在翻转坐标系的代码注释掉,会看到这样的效果。


CoreText(一)_第4张图片
翻转HelloWorld.jpeg
  • 绘制区域,CoreText支持各种文字排版区域,我们这里简单的讲view的边界作为了排版区域,可以看下面一个例子,看一下CoreText是如何支持文字排版区域的
// 3获取路径
let path = CGMutablePath()
path.addEllipse(in: self.bounds)
// 4. 文本
let str = "Hello World! 创建绘制的区域,CoreText 本身支持各种文字排版的区域,我们这里简单地将 UIView 的整个界面作为排版的区域。 为了加深理解,建议读者将该步骤的代码替换成如下代码, 测试设置不同的绘制区域带来的界面变化。"

效果图如下:


CoreText(一)_第5张图片
椭圆区域.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.size, value: &lineSpace)
        theSettings.append(theSettingLine)
        let theSettingMax = CTParagraphStyleSetting(spec: .maximumLineSpacing, valueSize: MemoryLayout.size, value: &lineSpace)
        theSettings.append(theSettingMax)
        let theSettingMin = CTParagraphStyleSetting(spec: .minimumLineSpacing, valueSize: MemoryLayout.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
    }

}

运行起来看一下效果:


CoreText(一)_第6张图片
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
    }

运行起来看一下:


CoreText(一)_第7张图片
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.size, value: &lineSpace)
        theSettings.append(theSettingLine)
        let theSettingMax = CTParagraphStyleSetting(spec: .maximumLineSpacing, valueSize: MemoryLayout.size, value: &lineSpace)
        theSettings.append(theSettingMax)
        let theSettingMin = CTParagraphStyleSetting(spec: .minimumLineSpacing, valueSize: MemoryLayout.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
    }

}

看一下效果:


CoreText(一)_第8张图片
WechatIMG10.jpeg

到现在为止,我们的代码实现了根据文件模板来显示文字内容,并根据模板提供的信息显示不容的颜色。后面我们要把这个view做成可以显示图片,并支持图片的点击,支持链接点击,支持数据号码识别,网页地址链接识别这样的一个控件,一起期待吧~~~
参考:
http://yangchao0033.github.io/blog/2016/01/26/coretextji-chu/

你可能感兴趣的:(CoreText(一))