神刀安全网

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入坑二

二. 自动识别链接

富文本中插入链接, 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)    }      ...       }

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

CoreText入坑二

三. 链接跳转

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

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入坑二

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

四. 图文混排

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))    } }

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

CoreText入坑二

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

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

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

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

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » CoreText入坑二

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址