美文网首页
CoreText(一)

CoreText(一)

作者: likefly | 来源:发表于2018-02-02 09:55 被阅读24次

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

    1.苹果的基础框架

    coretext_arch.png
    从上图中,可以看出苹果的CoreText处于Core Graphics的上一层,处于Text KitWebKit的下一层,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/

    相关文章

      网友评论

          本文标题:CoreText(一)

          本文链接:https://www.haomeiwen.com/subject/cxydzxtx.html