之前实现了一个在UILabel上点击文字,实现文字高亮的效果。实际上是利用 Core Text
实现的:用Core Text
的相关方法利用文字的CFAttributedString
(富文本)属性重写UILabel
的drawrect:
方法,每次点击文本后改变CFAttributedString
属性并用Core Text
重绘UILabel,先来看一下CoreText
。
CoreText
框架层次
Core Text is an advanced, low-level technology for laying out text and handling fonts. Core Text works directly with Core Graphics (CG), also known as Quartz, which is the high-speed graphics rendering engine that handles two-dimensional imaging at the lowest level in OS X and iOS.
官方文档中提到Core Text
是一个底层的负责文字布局,处理字体的框架,直接与Core Graphics
交互。Core Text
实际上是C
实现的,所以使用起来非常灵活,并且可以直接对context
进行绘制。
从框架的层次结构上看,可以证明CoreText相当底层,利用Core Text实现的功能适用于以他为底层的所有控件。
绘制原理
如图是Core Text Object
的层次结构。
CTFramesetter:层次结构中的顶层,接受一个attributed string
和一个 graphics path
作为输入 , framesetter
可以生成 frames of text (CTFrameRef)
。 每个 CTFrame object
代表一个段落。CTFramesetter
在生成frames
的时候会用到typesetter object (CTTypesetterRef)
,framesetter
负责把段落的风格应用到frame 上,typesetter负责把富文本中的字符转换为图形化的字形并填入组成frame
的line object(CTLine)
中。
CTLine:每一个 CTFrame object
都包含一个或多个line (CTLine) objects
,每一个line
代表一行文字。
CTRun:每一个 CTLine object
包含一个 glyph run (CTRun) objects
数组。一个 glyph run
是一系列连续的共享相同attributes and direction
的glyph
(字形)。一个CTLine object
可能包含一个或多个CTRun object
。
由于CTFrame
可以直接把自己绘制到graphics context
上,我们就可以通过改变 NSAttributedString object
的属性来改变CTFrame object
绘制出来的样子。
实现点击高亮
获取点击位置
// 获取点击到的Character在整个段落文字中的Index
func indexOf(point: CGPoint) -> CFIndex{
// 反转坐标
let reversedPoint = CGPoint(x: point.x, y: self.bounds.maxY - point.y)
// 获取段落的lines
let lines = CTFrameGetLines(mCTFrame!) as NSArray
// 获取lines的origin坐标
var originsArray = [CGPoint] (repeating: .zero, count: lines.count)
CTFrameGetLineOrigins(mCTFrame!, CFRangeMake(0, lines.count), &originsArray)
for i in 0.. originsArray[i].y) { // 遍历到点击的行
let line = lines.object(at: i) as! CTLine
// 用对应的line和点击的坐标获取点击的character的Index
return CTLineGetStringIndexForPosition(line, reversedPoint)
}
}
return 0
}
可以通过获得的Index计算对应String的Index Range。
关于func CTLineGetStringIndexForPosition(_ line: CTLine, _ position: CGPoint) -> CFIndex
:这个方法是通过在这一行中分析点击的character是由哪个typesetter
创建,并分析出其对应的字形的位置。
添加attribute属性
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.convertCoordinateSystem(view: self)
let mutablePath = UIBezierPath(rect: rect)
let mutableAttributeString = NSMutableAttributedString(string: self.text!)
mutableAttributeString.addAttribute(NSFontAttributeName, value: ARLTICLE_CONTENT_FONT, range: NSMakeRange(0, mutableAttributeString.length))
// 文本排版格式
let style = NSMutableParagraphStyle()
style.lineSpacing = LINE_SPACING
style.alignment = .justified
mutableAttributeString.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(0, mutableAttributeString.length))
/*
* 此处省略计算高亮range相关代码
*/
// range:高亮String的Range
mutableAttributeString.addAttributes([NSBackgroundColorAttributeName: MAIN_COLOR, NSForegroundColorAttributeName: HIGHT_LIGHT_TEXT_COLOR],range:range)
}
highlightWordsLoaction.removeLast()
allowSelectWord = true
}
let framesetter = CTFramesetterCreateWithAttributedString(mutableAttributeString)
// mCTFrame之前已经声明
mCTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, mutableAttributeString.length), mutablePath.cgPath, nil)
CTFrameDraw(mCTFrame!, context!)
}
最后
没填的坑:从第二段开始高亮位置向前偏移,并且UILabel高度计算不准确,填完补充解决办法。