CoreText 学习2

本文主要是用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容