Swift--CoreText

AttribuedString

AttribuedString对象包含很多的属性,每一个属性都有其对应的字符区域,使用NSRange来进行描述的。

  1. NSDictionary来存放属性值
  2. range内的字符指定属性
  3. 对于控件上字符都是通过NSTextStorage实例来存储
  4. isEqual比较每个字符以及他们的属性来判等
  5. 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实例

  1. 通过UIGraphisGetCurrentContext()获取当前的环境
  2. (可选)通过CGContextSetRGBFillColor,CGContextFillRect等方法在context上填充图形
  3. 通过CGContextConcatCTM(_:_:)来转换坐标轴。转换矩阵为(1,0;0,-1;0,self.bounds.height)
  4. 设置需要添加到context上NSMutableAttributedString属性
  5. 创建绘制区域CGPathCreateMutable(),CGPathAdd...
  6. 根据AttributedString生成CTFramesetterRef
  7. 进行绘制CTFrameDraw(_:_:)//文字的绘制,CTFrameDraw会因为行高不一致导致排版不美观

图片的插入

1-4同上

  1. 为图片设置CTRunDelegate,delegate决定留给图片的空间大小
  2. 设置CTRun代理CTRunDelegateCreate,并添加到一个NSMutableAttributedString实例中去。
  3. 将上述字符添加到文字中去使用NSMutableAttributedStringinsertAttributedString方法
  4. 根据AttributedString生成CTFramesetterRef,并生成相应的frame用于计算
  5. 这一步开始用来绘制图片,首先绘制图片以外的部分使用CTFrameDraw()
  6. 处理绘制图片的逻辑,从frame中获取lines ,CTFrameGetLines
  7. CTFrameGetLineOrigins把每一个CTLine的origin坐标写到数组里。//origin坐标是一个字字形的坐标,具体参考文献1
    1. 遍历CTRun找出图片所在的CTRun并进行绘制,包括获取每行的初始坐标
  8. 将当前的CTRun放入字典中来判断当前CTRun是否包含图片
  9. 如果有,那么就确定其所在位置
  10. 通过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)
                
            }

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

推荐阅读更多精彩内容

  • 苹果文档 https://developer.apple.com/documentation/coretext C...
    阳明先生_x阅读 421评论 0 4
  • 系列文章: CoreText实现图文混排 CoreText实现图文混排之点击事件 CoreText实现图文混排之文...
    老司机Wicky阅读 40,049评论 221 432
  • CoreText是一个进阶的比较底层的布局文本和处理字体的技术,CoreText API在OS X v10.5 和...
    smalldu阅读 13,342评论 18 129
  • blog.csdn.net CoreText实现图文混排 - 博客频道 CoreText实现图文混排 也好久没来写...
    K_Gopher阅读 592评论 0 0
  • iOS没有现成的支持图文混排的控件,而要用多个基础控件组合拼成图文混排这样复杂的排版,是件很苦逼的事情。对此的解决...
    清风沐沐阅读 667评论 0 2