CoreText入坑二

微信应用号(实际叫微信小程序)今天内测了, 好像也不关我啥事。继续入坑, CoreText入坑一实现了CoreText的基本步骤, 以及删除线的绘制。这篇主要实现绘制背景色, 自动识别链接, 点击链接跳转, 图文混排。

一. 背景色填充

先来个简单点的, 上篇文章TULabel绘制了删除线, 那么填充背景色也是照那个步骤开始。
首先需要像识别删除线样式一样识别出背景色样式, 所以在drawRun函数添加判断代码

// 画样式
func drawRun(run: CTRun, attributes: NSDictionary, context: CGContext) {
   if nil != attributes[NSStrikethroughStyleAttributeName] { // 删除线
       CTRunDraw(run, context, CFRangeMake(0, 0))
       drawStrikethroughStyle(run, attributes: attributes, context: context)
   } else if nil != attributes[NSBackgroundColorAttributeName] { // 背景色
       fillBackgroundColor(run, attributes: attributes, context: context)
       CTRunDraw(run, context, CFRangeMake(0, 0))
   } else {
       CTRunDraw(run, context, CFRangeMake(0, 0))
   }
}

注意跟之前不太一样的地方是CTRunDraw的调用需要在填充颜色之后。

然后再来看下怎样填充背景色

// 填充背景色
func fillBackgroundColor(run: CTRun, attributes: NSDictionary, context: CGContext) {
     // 获取设置的背景色
   let backgroundColor = attributes[NSBackgroundColorAttributeName]
   guard let color = backgroundColor else {
       return
   }
   
   // 获取画线的起点, getRunOrigin就是删除线里面获取Run原点的代码提取的函数
   let origin = getRunOrigin(run)
   
   // 获取Run的宽度, ascent, descent
   var ascent = CGFloat(), descent = CGFloat(), leading = CGFloat()
   let typographicWidth = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading))
   
   let pt = CGContextGetTextPosition(context)
   
   // 需要填充颜色的区域
   let rect = CGRectMake(origin.x + pt.x, pt.y + origin.y - descent, typographicWidth, ascent + descent)
   
   // 开始填充颜色
   let components = CGColorGetComponents(color.CGColor)
   CGContextSetRGBFillColor(context, components[0], components[1], components[2], components[3])
   CGContextFillRect(context, rect)
}

使用的时候就跟系统Label使用方式一致

// 背景色
attributedText.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellowColor(), range: NSMakeRange(20, 10))

这样就完成了背景色的填充, 效果如下


二. 自动识别链接

富文本中插入链接, CoreText是不能自动识别的, 所以就需要我们自己识别了。先看下怎么识别链接

// 检测到的链接
private var detectLinkList: [NSTextCheckingResult]?

// 检测链接
func detectLinks() {
   guard let text = self.attributedText else {
       return
   }
   
   // 定义识别器类型
   let linkDetector = try! NSDataDetector(types: NSTextCheckingType.Link.rawValue)
   
   // 将匹配的类型存储到一个数组中
   let content = text.string
   self.detectLinkList = linkDetector.matchesInString(content, options: NSMatchingOptions.ReportProgress, range: NSMakeRange(0, content.characters.count))
}

链接识别出来了, 按我们平常看到的链接样式需要跟普通文本不一样, 所以还要给链接添加样式以区别

// 链接显示颜色, 可外部自定义, 默认为蓝色
var linkColor = UIColor.blueColor()

// 给链接增加样式
func addLinkStyle(attributedText: NSAttributedString?, links: [NSTextCheckingResult]?) -> NSAttributedString? {
   guard let linkList = links else {
       return attributedText
   }
   
   guard let text = attributedText else {
       return attributedText
   }
   
   // 遍历链接列表, 增加指定样式
   let attrText = NSMutableAttributedString(attributedString: text)
   linkList.forEach { [unowned self] result in
       attrText.addAttributes([NSForegroundColorAttributeName: self.linkColor,
           NSUnderlineStyleAttributeName: NSUnderlineStyle.StyleSingle.rawValue,
           NSUnderlineColorAttributeName: self.linkColor], range: result.range)
   }
   return attrText
}

剩下的就是只有调用这两个函数了

// 是否自动检测链接, default is false, 可开启自动识别
var autoDetectLinks = false

override func drawRect(rect: CGRect) {
   if self.autoDetectLinks {
      // 检测链接
       detectLinks()  
       
       // 给链接添加样式
       self.attributedText = addLinkStyle(self.attributedText, links: self.detectLinkList)
   }

    ...      
}

外部调用的时候就只需要开启自动识别即可, 效果如下


三. 链接跳转

要让链接可以跳转, 就需要先识别点击的是否为链接, 然后才可以进行跳转。
先来看看怎么实现获取点击的坐标

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
   if self.autoDetectLinks {
      let touch: UITouch = touches.first!
      let point = touch.locationInView(self)
      
      // 获取点击位置对应富文本的位置 
      let index = attributedIndexAtPoint(point)
       
      // 根据index找链接 
      let foundLink = linkAtIndex(index)
           
      if nil != foundLink.foundLink  {
          guard let link = foundLink.link else {
              return
          }
          
          // 抛出回调
          if let touchLink = self.touchLinkCallback {
              touchLink(link: link)
          }
         }
}

重写touchesBegan函数来实现获取点击坐标, 根据坐标获取对应的富文本索引

private var ctframe: CTFrame?

// 获取点击位置对应的富文本的位置index
func attributedIndexAtPoint(point: CGPoint) -> CFIndex {
    // 记住CTFrame, 需要通过frame找点击位置
   guard let frame = self.ctframe else {
       return -1
   }
   
   let lines = CTFrameGetLines(frame)
   
   // 获得行数
   let numberOfLines = CFArrayGetCount(lines)
   
   // 获得每一行的origin, CoreText的origin是在字形的baseLine处的
   var lineOrigins = [CGPoint](count: numberOfLines, repeatedValue: CGPointZero)
   CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins)
   
   //坐标变换
   let transform = CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1, -1);
   
   for index in 0..<numberOfLines {
       let origin = lineOrigins[index]
       
       // 参考: http://swifter.tips/unsafe/
       let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, index), CTLine.self)
       
       // getLineRect是获得一行的区域
       let flippedRect = getLineRect(line, origin: origin)
       // 然后需要翻转到UI坐标系
       let rect = CGRectApplyAffineTransform(flippedRect, transform)
       
       if CGRectContainsPoint(rect, point) { // 找到了是哪一行
           let relativePoint = CGPointMake(point.x - CGRectGetMinX(rect), point.y - CGRectGetMinY(rect))
           return CTLineGetStringIndexForPosition(line, relativePoint)
       }
   }
   
   return -1
}

这个函数CTLineGetStringIndexForPosition是核心, 获取到索引后就可以根据索引来查找当前点击位置是不是链接了

// 判断点击的位置是不是链接
func linkAtIndex(index: CFIndex) -> (foundLink: NSTextCheckingResult?, link: String?) {
   if self.autoDetectLinks {
       guard let links = self.detectLinkList else {
           return (nil, nil)
       }
       
       var foundLink: NSTextCheckingResult?
       var link: String?
       // 遍历所有之前检测出的链接来匹配index, 查找到对应链接
       links.forEach({ result in
           if NSLocationInRange(index, result.range) {
               foundLink = result
               link = self.attributedText!.attributedSubstringFromRange(result.range).string
               return
           }
       })
       return (foundLink, link)
   }
   
   return (nil, nil)
}

这样就实现了链接点击跳转了, 但是如果不希望链接直接出现在文本中, 而是用特定的文字替代链接, 但是照样要能特别显示, 也需要可以点击, 那又如何实现了?

获取点击索引还是上面的函数attributedIndexAtPoint, 主要是换成查找特定的文字来添加样式, 实际源码请看文末附加链接, 这里就直接上效果了。


这是点击后的效果, 图中蓝色带下划线的即为链接

四. 图文混排

CoreText为在文本中插入图片做了一些事情, 其实我们就是通过CTRunDelegateCallbacks这个类的回调来计算图片所在布局, 相当于把图片也当做一个Run来处理。

我们先定义一个类来表示一个图片的一些相关信息

public let TUImageAttachmentAttributeName: String = "TUImageAttachmentAttributeName"

class TUImageAttachment {
    init(name: String, location: Int) {
        self.name = name
        self.location = location
        
        self.image = UIImage(named: name)
        if let img = self.image {
            self.bounds = CGRect(x: 0, y: 0, width: img.size.width, height: img.size.height)
        }
    }

    var name: String  // 图片名字
    var image: UIImage? // 图片本身
    var location: Int // 图片插入的位置
    var bounds: CGRect? //图片所占区域
}

然后我们在使用的时候就需要用到这个类

// 图片附件
let imageName = "catanddog"
let image = UIImage(named: imageName)
let imageAttachment = TUImageAttachment(name: imageName, location: 230)
   
// 调整图片位置到中间
imageAttachment.bounds = CGRect(x: 0, y: -image!.size.height / 2, width: image!.size.width, height: image!.size.height)
// 给TULabel添加一个属性, 图片附件数组
view.imageAttachments = [imageAttachment]

到此时, 还没有开始实现TULabel的绘制图片, 现在来看看。先检查是否插入了图片附件, 如果有就给每个图片附件添加一个RunDelegate来占个位

// 检测是否有图片
func checkImage(attributedText: NSAttributedString?) -> NSAttributedString? {
   guard let attrText = attributedText else {
       return attributedText
   }
   
   guard let attachments = self.imageAttachments else {
       return attrText
   }
   
   let text = NSMutableAttributedString(attributedString: attrText)
   
   // 遍历图片附件列表
   attachments.forEach { attach in
       text.insertAttributedString(imageAttribute(attach), atIndex: attach.location)
   }
   
   return text
}

插入RunDelegate的方法

// 插入图片样式
func imageAttribute(attachment: TUImageAttachment) -> NSAttributedString {
    // 定义RunDelegateCallback并实现
   var imageCallback = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { pointer in
           pointer.dealloc(1)
       }, getAscent: { pointer -> CGFloat in
           return UnsafePointer<UIImage>(pointer).memory.size.height / 2
       }, getDescent: { pointer -> CGFloat in
           return UnsafePointer<UIImage>(pointer).memory.size.height / 2
       }, getWidth: { pointer -> CGFloat in
           return UnsafePointer<UIImage>(pointer).memory.size.width
   })
   
   // 创建RunDelegate, 传入callback中图片数据
   let pointer = UnsafeMutablePointer<UIImage>.alloc(1)
   pointer.initialize(attachment.image!)
   let runDelegate = CTRunDelegateCreate(&imageCallback, pointer)
   
   // 为每个图片创建一个空的string占位
   let imageAttributedString = NSMutableAttributedString(string: " ")
   imageAttributedString.addAttribute(kCTRunDelegateAttributeName as String, value: runDelegate!, range: NSMakeRange(0, 1))
   // 将附件作为指定属性的值
   imageAttributedString.addAttribute(TUImageAttachmentAttributeName, value: attachment, range: NSMakeRange(0, 1))
   
   return imageAttributedString
}

到此, 还只是为图片占了个坑, 所以这个调用要放到drawRect方法绘制之前

override func drawRect(rect: CGRect) {
        ...
        
        if let attributedString = checkImage(self.attributedText) {
            self.attributedText = attributedString
        }
        
        ...
}

占坑完毕了, 那么就是绘制图片了

// 画图片
func drawImage(run: CTRun, attributes: NSDictionary, context: CGContext) {
    // 获取对应图片属性的附件
   let imageAttachment = attributes[TUImageAttachmentAttributeName]
   guard let attachment = imageAttachment else {
       return
   }
   
   // 计算绘制图片的区域
   let origin = getRunOrigin(run)
   
   var ascent = CGFloat(), descent = CGFloat(), leading = CGFloat()
   let typographicWidth = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading))
   
   let pt = CGContextGetTextPosition(context)
   
   var rect = CGRect(x: origin.x + pt.x, y: pt.y + origin.y - descent, width: typographicWidth, height: ascent + descent)
   
   let image = (attachment as! TUImageAttachment).image
   rect.size = image!.size
   
   // 绘制图片
   CGContextDrawImage(context, rect, image!.CGImage!)
}

绘制完了, 在drawRun函数中加入绘制图片的方法

// 画样式
func drawRun(run: CTRun, attributes: NSDictionary, context: CGContext) {
   if nil != attributes[NSStrikethroughStyleAttributeName] { // 删除线
       CTRunDraw(run, context, CFRangeMake(0, 0))
       drawStrikethroughStyle(run, attributes: attributes, context: context)
   } else if nil != attributes[NSBackgroundColorAttributeName] { // 背景色
       fillBackgroundColor(run, attributes: attributes, context: context)
       CTRunDraw(run, context, CFRangeMake(0, 0))
   } else if nil != attributes[TUImageAttachmentAttributeName] { // 绘制图片
       drawImage(run, attributes: attributes, context: context)
   } else {
       CTRunDraw(run, context, CFRangeMake(0, 0))
   }
}

图片绘制就完成了, 来看看效果


至此, 我们已经完成了删除线, 背景色, 链接, 图片混排四种样式。因为中间正逢iPhone7发布, 带来了iOS10, Swift3.0, Xcode8, 所以就理所当然的转移到新阵地了。本篇文章还是使用Swift2.3编写, 但是另外又开了一个工程适配了Swift3.0。Swift2.x源码, Swift3.0源码, 请自取。

话说应该去研究微信小程序了, 不然就out了!

参考:
CoreText基础概念
CoreText入门
Nimbus

本文由啸寒原创, 转载请注明出处!!!

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,613评论 4 59
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • w 写好认知报告 o 十二点之前把报告写完 o 上完三节课很累,不想去写报告 p 如果不想写报告了,就去整理书桌后再去写
    小风在路上阅读 109评论 0 0
  • 已经到了每想你一次, 每朵花都滑落一滴水的地步。 我很想知道, 这于花是福是劫? 其实我想问的是你, 何去何从了?...
    野派阅读 158评论 0 2
  • 文件对接器侧重于对文件内容的管理,可以使用它完成文件的继续写入,部分读取功能,使用之前必须保证文件是存在的 打印结果
    我爱吃豆芽阅读 213评论 0 1