CoreText 学习1

作者: 老初 | 来源:发表于2017-03-10 17:22 被阅读40次

    本文主要是用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<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)
            
            // 创建 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<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 {
            // 创建 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)
        }
    }
    

    相关文章

      网友评论

        本文标题:CoreText 学习1

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