CoreText 学习2

作者: 老初 | 来源:发表于2017-03-14 17:05 被阅读29次

    本文主要是用Swift重写了巧哥博客中的 Demo,博客的原始链接如下:

    唐巧:
    基于 CoreText 的排版引擎:基础
    基于 CoreText 的排版引擎:进阶

    1、支持图文(链接)混排的排版引擎

    content.json文件修改为:

    [
      {
        "type": "img",
        "width": 200,
        "height": 108,
        "name": "coretext-image-1.jpg"
      },
      {
        "color": "blue",
        "content": " 更进一步地,实际工作中,我们更希望通过一个排版文件,来设置需要排版的文字的 ",
        "size": 16,
        "type": "txt"
      },
      {
        "color": "red",
        "content": " 内容、颜色、字体 ",
        "size": 22,
        "type": "txt"
      },
      {
        "color": "black",
        "content": " 大小等信息。\n",
        "size": 16,
        "type": "txt"
      },
      {
        "type": "img",
        "width": 200,
        "height": 130,
        "name": "coretext-image-2.jpg"
      },
      {
        "color": "default",
        "content": " 我在开发猿题库应用时,自己定义了一个基于 UBB 的排版模版,但是实现该排版文件的解析器要花费大量的篇幅,考虑到这并不是本章的重点,所以我们以一个较简单的排版文件来讲解其思想。",
        "type": "txt"
      },
      {
        "color": "default",
        "content": " 这在这里尝试放一个参考链接:",
        "type": "txt"
      },
      {
        "color": "blue",
        "content": " 链接文字 ",
        "url": "http://blog.devtang.com",
        "type": "link"
      },
      {
        "color": "default",
        "content": " 大家可以尝试点击一下 ",
        "type": "txt"
      }
    ]
    

    修改parseTemplateFile方法,增加一个名为imageArray的参数来保存解析的图片信息,增加一个名为linkArray的参数来保存解析的链接信息:

    /// 解析模板文件
    class func parseTemplateFile(path: String, config: CTFrameParserConfig) -> CoreTextData {
        var imageArray = [CoreTextImageData]()
        var linkArray  = [CoreTextLinkData]()
        
        let content = self.loadTemplateFile(path: path, config: config, imageArray: &imageArray, linkArray: &linkArray)
        
        let coreTextData = self.parse(content: content, config: config)
        
        coreTextData.imageArray = imageArray
        coreTextData.linkArray = linkArray
        
        return coreTextData
    }
    

    修改loadTemplateFile方法,增加了对于typeimglink的节点处理逻辑:

    /// 加载模板文件
    class func loadTemplateFile(path: String, config: CTFrameParserConfig, imageArray: inout [CoreTextImageData], linkArray: inout [CoreTextLinkData]) -> 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)
                    }
                    
                    if type == "image" {
                        let imageData = CoreTextImageData()
                        imageData.name = item["name"]
                        imageData.imagePosition = CGRect(x: 0.0, y: 0.0, width: 0.0, height: 0.0)
                        imageArray.append(imageData)
                        
                        let subStr = self.parseImageAttributedCotnentFromDictionary(dict: item, config: config)
                        result.append(subStr)
                    }
                    
                    if type == "link" {
                        let startPosition = result.length
                        let subStr = self.parseAttributedCotnentFromDictionary(dict: item, config: config)
                        result.append(subStr)
                        
                        var linkData = CoreTextLinkData()
                        linkData.title = item["content"]
                        linkData.url   = item["url"]
                        linkData.range = NSMakeRange(startPosition, result.length - startPosition)
                        linkArray.append(linkData)
                    }
                }
            }
        }
        
        return result
    }
    

    最后我们新建一个最关键的方法:parseImageAttributedCotnentFromDictionary,生成图片空白的占位符,并且设置其CTRunDelegate信息。其代码如下:

    /// 从字典中解析图片富文本信息
    ///
    /// - Parameters:
    ///   - dict: 文字属性字典
    ///   - config: 配置信息
    /// - Returns: 图片富文本
    class func parseImageAttributedCotnentFromDictionary(dict: [String: String], config: CTFrameParserConfig) -> NSAttributedString {
        var ascender: CGFloat = 0.0
        if let height = (dict["height"] as AnyObject).floatValue {
            ascender = CGFloat(height)
        }
        var width: CGFloat = 0.0
        if let w = (dict["width"] as AnyObject).floatValue {
            width = CGFloat(w)
        }
        let pic = PictureRunInfo(ascender: ascender, descender: 0.0, width: width)
        
        var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { refCon in
            print("RunDelegate dealloc!")
        }, getAscent: { (refCon) -> CGFloat in
            let pictureRunInfo = unsafeBitCast(refCon, to: PictureRunInfo.self)
            return pictureRunInfo.ascender
        }, getDescent: { (refCon) -> CGFloat in
            return 0
        }, getWidth: { (refCon) -> CGFloat in
            
            let pictureRunInfo = unsafeBitCast(refCon, to: PictureRunInfo.self)
            return pictureRunInfo.width
        })
        
        let selfPtr = UnsafeMutableRawPointer(Unmanaged.passRetained(pic).toOpaque())
        
        // 创建 RunDelegate, delegate决定留给图片的空间大小
        let runDelegate = CTRunDelegateCreate(&callbacks, selfPtr)
        
        let attributes : Dictionary = self.attributes(config: config)
        // 创建一个空白的占位符
        let space = NSMutableAttributedString(string: " ", attributes: attributes)
        
        CFAttributedStringSetAttribute(space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, runDelegate)
        return space
    }
    

    接着我们对CoreTextData进行改造:

    // 用于保存由 CTFrameParser 类生成的 CTFrame 实例以及 CTFrame 实际绘制需要的高度
    class CoreTextData: NSObject {
        
        var ctFrame: CTFrame
        var height: CGFloat
        var imageArray: [CoreTextImageData] = [CoreTextImageData]() {
    
            willSet {
                fillImagePosition(imageArray: newValue)
            }
            
        }
        var linkArray: [CoreTextLinkData]?
        
        init(ctFrame: CTFrame, height: CGFloat) {
            self.ctFrame = ctFrame
            self.height = height
        }
         
        private func fillImagePosition(imageArray: [CoreTextImageData]) {
            if imageArray.count == 0 {
                return
            }
            
            let lines = CTFrameGetLines(ctFrame) as Array
            var originsArray = [CGPoint](repeating: CGPoint.zero, count:lines.count)
            // 把 CTFrame 里每一行的初始坐标写到数组里
            CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), &originsArray)
            
            var imgIndex : Int = 0
            var imageData: CoreTextImageData? = imageArray[0]
            
            for index in 0..<lines.count {
                
                guard imageData != nil else {
                        return
                }
                
                let line = lines[index] as! CTLine
                let runObjArray = CTLineGetGlyphRuns(line) as Array
                
                for runObj in runObjArray {
                    let run = runObj as! CTRun
                    let runAttributes = CTRunGetAttributes(run) as NSDictionary
                    let delegate = runAttributes.value(forKey: kCTRunDelegateAttributeName as String)
                    
                    if delegate == nil {
                        continue
                    }
                    
                    var runBounds = CGRect()
                    var ascent: CGFloat = 0
                    var descent: CGFloat = 0
                    
                    runBounds.size.width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, nil))
                    runBounds.size.height = ascent + descent
                    
                    let xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil)
                    runBounds.origin.x = originsArray[index].x + xOffset
                    runBounds.origin.y = originsArray[index].y
                    runBounds.origin.y -= descent
                    
                    let path = CTFrameGetPath(ctFrame)
                    
                    let colRect = path.boundingBox
                    
                    let delegateBounds = runBounds.offsetBy(dx: colRect.origin.x, dy: colRect.origin.y)
                    
                    imageData!.imagePosition = delegateBounds
                    
                    imgIndex += 1
                    if imgIndex == imageArray.count {
                        imageData = nil
                        break
                    } else {
                        imageData = imageArray[imgIndex]
                    }
                }
            }
        }
    }
    

    2、添加对图片的点击支持

    CTDisplayView类增加:

    private func setupEvents() {
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(userTapGestureDetected(recognizer:)))
        self.addGestureRecognizer(tapGestureRecognizer)
        self.isUserInteractionEnabled = true
    }
    
    func userTapGestureDetected(recognizer: UITapGestureRecognizer) {
        let point = recognizer.location(in: self)
    
        if let imageArray = data?.imageArray {
            for imageData in imageArray {
                // 翻转坐标系,因为 imageData 中的坐标是 CoreText 的坐标系
                let imageRect = imageData.imagePosition
                var imagePosition = imageRect.origin
                imagePosition.y = self.bounds.size.height - imageRect.origin.y - imageRect.size.height
                let rect = CGRect(x: imagePosition.x, y: imagePosition.y, width: imageRect.size.width, height: imageRect.size.height)
                if rect.contains(point) {
                    print("\(imageData.name)")
                    
                    break
                }
            }
        }
    }
    
    最终展示.png

    Github地址:
    https://github.com/GuiminChu/JianshuExamples/tree/master/CoreTextDemo

    相关文章

      网友评论

        本文标题:CoreText 学习2

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