本文主要是用Swift
重写了巧哥博客中的 Demo,博客的原始链接如下:
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
方法,增加了对于type
是img
和link
的节点处理逻辑:
/// 加载模板文件
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
网友评论