AttribuedString
AttribuedString
对象包含很多的属性,每一个属性都有其对应的字符区域,使用NSRange
来进行描述的。
- 用
NSDictionary
来存放属性值 - 对
range
内的字符指定属性 - 对于控件上字符都是通过
NSTextStorage
实例来存储 -
isEqual
比较每个字符以及他们的属性来判等 - 和
CFAttributedStringRef
无缝桥接
CoreText基础
CoreText
一开始便是定位于桌面的排版系统,使用了传统的原点在左下角的坐标系,所以它在绘制文本的时候都是参照左下角的原点进行绘制的。 但iOS的UIView的drawRect
方法的context被做了次flip,如果你啥也不做处理,直接在这个context上进行CoreText绘制,你会发现文字是镜像且上下颠倒。*** Core Text本身并不支持图片绘制,图片的绘制你还得通过Core Graphics来进行***。CoreText可以通过CTRun的设置为你的图片在文本绘制的过程中留出适当的空间。这个设置就使用到CTRunDelegate了,CTRunDelegate作为CTRun相关属性或操作扩展的一个入口,使得我们可以对CTRun做一些自定义的行为。为图片留位置的方法就是加入一个空白的CTRun,自定义其ascent,descent,width等参数,使得绘制文本的时候留下空白位置给相应的图片。然后图片在相应的空白位置上使用Core Graphics接口进行绘制。
使用CTRunDelegateCreate
可以创建一个CTRunDelegate,它接收两个参数,一个是callbacks结构体,一个是所有callback调用的时候需要传入的对象。 callbacks的结构体为CTRunDelegateCallbacks,主要是包含一些回调函数,比如有返回当前run的ascent,descent,width这些值的回调函数,至于函数中如何鉴别当前是哪个run,可以在CTRunDelegateCreate的第二个参数来达到目的,因为CTRunDelegateCreate的第二个参数会作为每一个回调调用时的入参。
CoreText对象模型
如上图所述,其中Framesetter对应的类型是CTFramesetter
,通过CFAttributedString
进行初始化,它作为CTFrame
对象的生产工厂,负责根据path生产对应的CTFrame。
CTFrame
是可以通过CTFrameDraw
函数直接绘制到context上的,当然你可以在绘制之前,操作CTFrame
中的CTLine
,进行一些参数的微调。
CTLine
可以看做Core Text绘制中的一行的对象 通过它可以获得当前行的line ascent,line descent ,line leading,还可以获得Line下的所有Glyph Runs。
CTRun
或者叫做 Glyph Run,是一组共享相同attributes(属性)的字形的集合体。
CTFrame是指整个该UIView子控件的绘制区域,CTLine则是指每一行,CTRun
则是每一段具有一样属性的字符串。比如某段字体大小、颜色都一致的字符串为一个CTRun,CTRun不可以跨行,不管属性一致或不一致。通常的结构是每一个CTFrame有多个CTLine,每一个CTLine有多个CTRun。
CoreText实例
- 通过
UIGraphisGetCurrentContext()
获取当前的环境 - (可选)通过
CGContextSetRGBFillColor
,CGContextFillRect
等方法在context上填充图形 - 通过
CGContextConcatCTM(_:_:)
来转换坐标轴。转换矩阵为(1,0;0,-1;0,self.bounds.height) - 设置需要添加到context上
NSMutableAttributedString
属性 - 创建绘制区域
CGPathCreateMutable()
,CGPathAdd... - 根据
AttributedString
生成CTFramesetterRef
- 进行绘制
CTFrameDraw(_:_:)
//文字的绘制,CTFrameDraw会因为行高不一致导致排版不美观
图片的插入
1-4同上
- 为图片设置
CTRunDelegate
,delegate决定留给图片的空间大小 - 设置
CTRun
代理CTRunDelegateCreate
,并添加到一个NSMutableAttributedString
实例中去。 - 将上述字符添加到文字中去使用
NSMutableAttributedString
的insertAttributedString
方法 - 根据
AttributedString
生成CTFramesetterRef
,并生成相应的frame用于计算 - 这一步开始用来绘制图片,首先绘制图片以外的部分使用
CTFrameDraw
() - 处理绘制图片的逻辑,从frame中获取lines ,
CTFrameGetLines
-
CTFrameGetLineOrigins
把每一个CTLine的origin坐标写到数组里。//origin坐标是一个字字形的坐标,具体参考文献1 - 遍历CTRun找出图片所在的CTRun并进行绘制,包括获取每行的初始坐标
- 将当前的CTRun放入字典中来判断当前CTRun是否包含图片
- 如果有,那么就确定其所在位置
- 通过
CGContextDrawImage(_:_:_:)
画出图片
Swift代码
绘制的代码放在drawRect中去
override func drawRect(rect: CGRect) {
// 步骤1:得到当前用于绘制画布的上下文,用于后续将内容绘制在画布上
let context = UIGraphicsGetCurrentContext()
// 步骤2:翻转当前的坐标系
let transform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty:self.bounds.height)
CGContextConcatCTM(context, transform)
//步骤3:创建需要绘制的文字及属性
let str = NSMutableAttributedString(string: "在足球界,年龄是个十分奇怪的东西,你要么太年轻,要么太老;要么经验不足,要么老得什么都做不了;要么乃义务,要么就是应该做地更好。不管是成名太早,还是原地踏步太久,都会给一位球运动员造成很坏的影响。年轻人总是会受到最多的抨击,或者被球迷、主教练或者媒体榨干所有价值。最近的贝拉希诺就是一个很好的例子。这位前锋在转会窗期间犯下了一些低级的错误,每个人都对这位现年22岁的前锋有点看法,导致他从顶梁柱变成了普通的一根桩子")
//以下为可选的部分
//设置文字颜色
str.addAttribute(kCTForegroundColorAttributeName as String, value:UIColor.redColor() , range: NSMakeRange(0,1))
// 设置文字包括字体大小和字体
let fontRef = CTFontCreateWithName("ArialMT", 20, nil)
str.addAttribute(kCTFontAttributeName as String, value: fontRef, range:NSMakeRange(0, 1))
//设置行间距
var lineSpacing:CGFloat = 5;
let settings = [CTParagraphStyleSetting(spec: .LineSpacingAdjustment, valueSize: sizeof(CGFloat), value:&lineSpacing),CTParagraphStyleSetting(spec: .MaximumLineSpacing, valueSize: sizeof(CGFloat), value: &lineSpacing),CTParagraphStyleSetting(spec: .MinimumLineSpacing, valueSize: sizeof(CGFloat), value: &lineSpacing)]
let theParagraphRef = CTParagraphStyleCreate(settings, 3)
str.addAttribute(kCTParagraphStyleAttributeName as String, value: theParagraphRef, range: NSMakeRange(0,str.length))
//步骤4:创建绘制区域 ,就是在self.bounds中以何种方式展示,
let path = CGPathCreateMutable()
//不同的绘制方法
// CGPathAddRect(path, nil, self.bounds)
CGPathAddEllipseInRect(path, nil, self.bounds);
以下是图片的操作,还是放在drawRect
方法下。
//(可选)步骤7:绘制图片,
//7.1为图片设置CTRunDelegate,delegate决定留给图片的空间大小
var imageName = "read"
var imageCallback = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (refCon) -> Void in
NSLog("RunDelegate dealloc")
}, getAscent: { ( refCon) -> CGFloat in
let imageName = "read"
refCon.initialize()
let image = UIImage(named: imageName)
return image!.size.height
}, getDescent: { (refCon) -> CGFloat in
return 0
}) { (refCon) -> CGFloat in
let imageName = String("read")
let image = UIImage(named: imageName)
return image!.size.width
}
// 7.2设置CTRun的代理
let runDelegate = CTRunDelegateCreate(&imageCallback,&imageName)
let imageAttributedString = NSMutableAttributedString(string: " ");//空格用于给图片留位置
imageAttributedString.addAttribute(kCTRunDelegateAttributeName as String, value: runDelegate!, range: NSMakeRange(0, 1))
imageAttributedString.addAttribute("imageName", value: imageName, range: NSMakeRange(0, 1))//添加属性,在CTRun中可以识别出这个字符是图片
str.insertAttributedString(imageAttributedString, atIndex: 2)
//步骤5:根据AttributedString生成CTFramesetterRef
let frameSetter = CTFramesetterCreateWithAttributedString(str)
let ctFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,str.length), path, nil)
//步骤6:绘制除图片以外的部分
CTFrameDraw(ctFrame, context!)
//7.3处理绘制图片逻辑
let lines = CTFrameGetLines(ctFrame) as NSArray //存取frame中的ctlines
let nsLinesArray: NSArray = CTFrameGetLines(ctFrame) // Use NSArray to bridge to Array
let ctLinesArray = nsLinesArray as Array
var originsArray = [CGPoint](count:ctLinesArray.count, repeatedValue: CGPointZero)
let range: CFRange = CFRangeMake(0, 0)
CTFrameGetLineOrigins(ctFrame, range,&originsArray)
//7.2把ctFrame里每一行的初始坐标写到数组里
// CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), &origunsArray);
// 7.3遍历CTRun找出图片所在的CTRun并进行绘制,每一行可能有多个
for i in 0..<lines.count{
//遍历每一行CTLine
let line = lines[i]
var lineAscent = CGFloat()
var lineDescent = CGFloat()
var lineLeading = CGFloat()
CTLineGetTypographicBounds(line as! CTLineRef, &lineAscent, &lineDescent, &lineLeading)
let runs = CTLineGetGlyphRuns(line as! CTLine) as NSArray
for j in 0..<runs.count{
// 遍历每一个CTRun
var runAscent = CGFloat()
var runDescent = CGFloat()
let lineOrigin = originsArray[i]// 获取该行的初始坐标
let run = runs[j] // 获取当前的CTRun
let attributes = CTRunGetAttributes(run as! CTRun) as NSDictionary
let width = CGFloat( CTRunGetTypographicBounds(run as! CTRun, CFRangeMake(0,0), &runAscent, &runDescent, nil))
// runRect.size.width = CGFloat( CTRunGetTypographicBounds(run as! CTRun, CFRangeMake(0,0), &runAscent, &runDescent, nil))
// 这一段可参考Nimbus的NIAttributedLabel
let runRect = CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line as! CTLine, CTRunGetStringRange(run as! CTRun).location, nil), lineOrigin.y - runDescent, width, runAscent + runDescent)
let imageNames = attributes.objectForKey("imageName")
if imageNames is NSString {
let image = UIImage(named: imageName as String)
let imageDrawRect = CGRectMake(runRect.origin.x, lineOrigin.y, (image?.size.width)!, (image?.size.height)!)
print(imageDrawRect)
CGContextDrawImage(context, imageDrawRect, image!.CGImage)
}
}
}
}