CoreText入坑一

CoreText是Mac OS和iOS系统中处理文本的low-level API, 不管是使用OC还是swift, 实际我们使用CoreText都还是间接或直接使用C语言在写代码。CoreText是iOS和Mac OS中文本处理的根基, TextKit和WebKit都是构建于其上。

一. 基础

1.在使用CoreText编写代码之前, 需要先了解一些基础知识。下图是CoreText的基础框架


  • CTFrame可以想象成画布, 画布的大小范围由CGPath决定
  • CTFrame由很多CTLine组成, CTLine表示为一行
  • CTLine由多个CTRun组成, CTRun相当于一行中的多个块, 但是CTRun不需要你自己创建, 由NSAttributedString的属性决定, 系统自动生成。每个CTRun对应不同属性
  • CTFramesetter是一个工厂, 创建CTFrame, 一个界面上可以有多个CTFrame

2.文字的样式包括很多, 而每个字符的显示要归功于字体, 而字体包括很多基础知识, 比如磅值, 样式, 基线, 连字等等, 这里就不做更多介绍, 推荐两篇文章阅读。
CoreText基础概念
CoreText入门

在这里, 贴出CoreText基础概念文中关于字体结构的图(图片版权归此文作者)


二. 使用基本步骤

新建一个UIView, 在view的drawRect函数中按步骤写入下面的代码

  • 1.获取当前上下文
let context = UIGraphicsGetCurrentContext()
  • 2.转换坐标系
CGContextSetTextMatrix(context, CGAffineTransformIdentity)
CGContextTranslateCTM(context, 0, self.bounds.size.height)
CGContextScaleCTM(context, 1.0, -1.0)
  • 3.初始化路径
let path = CGPathCreateWithRect(self.bounds, nil)
  • 4.初始化字符串
let attrString = NSMutableAttributedString(string: "Hello CoreText")
  • 5.初始化framesetter
let framesetter = CTFramesetterCreateWithAttributedString(attrString)
  • 6.绘制frame
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil)
CTFrameDraw(frame, context!)

绘制的步骤完成了, 然后在ViewController里面将此view加入到ViewController中, 记得将view的背景色设置为白色, 那么效果就应该如下了



文字绘制出来了, 这就是CoreText使用最基本的步骤了。

三.简单的富文本Label

上面简单的绘制步骤中, 最后一步绘制frame, 是将整个frame当做一块绘制, 至于什么换行, 行中的样式什么的都是系统自己决定了。在开始之前, 我们将这个绘制frame改成我们自己一行一行, 甚至一个run一个run的绘制

  • 按行绘制
// 1.获得CTLine数组
let lines = CTFrameGetLines(frame)
        
// 2.获得行数
let numberOfLines = CFArrayGetCount(lines)
   
// 3.获得每一行的origin, CoreText的origin是在字形的baseLine处的, 请参考字形图
var lineOrigins = [CGPoint](count: numberOfLines, repeatedValue: CGPointZero)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins)
   
// 4.遍历每一行进行绘制
for index in 0..<numberOfLines {
  let origin = lineOrigins[index]
  
  // 参考: http://swifter.tips/unsafe/
  let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, index), CTLine.self)
  
  // 设置每一行的位置
  CGContextSetTextPosition(context, origin.x, origin.y)
  
  // 开始一行的绘制
  CTLineDraw(line, context)
}

将最后一步改成按行绘制, 最终得到的效果也和按frame绘制一样的, 接下来看下按Run绘制

  • 按Run绘制
// 画一行
func drawLine(line: CTLine, context: CGContext) {
   let runs = CTLineGetGlyphRuns(line) as Array
    
   runs.forEach { run in
       CTRunDraw(run as! CTRun, context, CFRangeMake(0, 0))
       }
   }
}

用此函数替换CTLineDraw(line, context)这一句就可以了, 效果也如上面。

那么接下来实现一个简单的富文本Label, 将上面的view改名为TULabel

  • 声明一个富文本变量给此Label
var attributedText: NSAttributedString?
  • 将上面的第4步注释掉, 初始化framesetter的字符串直接传入此变量, 至于后面的绘制你可以用任意一种, 这样TULabel就可以实现一部分富文本了, 在controller中创建一个TULabel, 然后来个NSMutableAttributedString实例赋值给TULabel.attributedText, 下面列出此时可用的富文本样式
let attributedText = NSMutableAttributedString(string: ...)
        
// CoreText支持的属性

// 字体颜色
attributedText.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, 10))
   
// 下划线
let underlineStyles = [NSUnderlineStyleAttributeName: NSUnderlineStyle.StyleSingle.rawValue,
                     NSUnderlineColorAttributeName: UIColor.orangeColor()]
attributedText.addAttributes(underlineStyles, range: NSMakeRange(10, 10))
   
// 字体
attributedText.addAttribute(NSFontAttributeName, value: UIFont.boldSystemFontOfSize(50), range: NSMakeRange(20, 10))
   
// 描边(Stroke):组成字符的线或曲线。可以加粗或改变字符形状
let strokeStyles = [NSStrokeWidthAttributeName: 10,
                  NSStrokeColorAttributeName: UIColor.blueColor()]
attributedText.addAttributes(strokeStyles, range: NSMakeRange(40, 20))
   
// 横竖文本
attributedText.addAttribute(NSVerticalGlyphFormAttributeName, value: 0, range: NSMakeRange(70, 10))
   
// 字符间隔
attributedText.addAttribute(NSKernAttributeName, value: 5, range: NSMakeRange(90, 10))

// 段落样式
let paragraphStyle = NSMutableParagraphStyle()
   
//对齐模式
paragraphStyle.alignment = .Center
   
//换行裁剪模式
paragraphStyle.lineBreakMode = .ByWordWrapping
   
// 行间距
paragraphStyle.lineSpacing = 5.0
   
// 字符间距
paragraphStyle.paragraphSpacing = 2.0

attributedText.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSRange(location: 0, length: attributedText.length))

此时, 你就会看到如下效果


  • 效果出来了, 你是否就满足了。那其他平常可以使用的样式要怎么样使用CoreText来实现了? 我们就先实现一个样式--删除线, 将上面的drawLine函数改成如下
// 画一行
func drawLine(line: CTLine, context: CGContext) {
   let runs = CTLineGetGlyphRuns(line) as Array
    
   runs.forEach { run in
       CTRunDraw(run as! CTRun, context, CFRangeMake(0, 0))
       
       // 获得run的所有样式
       let attributes = CTRunGetAttributes(run as! CTRun) as NSDictionary
       
       // 判断是run是否含有删除线样式
       if nil != attributes[NSStrikethroughStyleAttributeName] {
            // 开始画删除线
           drawStrikethroughStyle(run as! CTRun, attributes: attributes, context: context)
       }
   }  
}
  • 当然, 你要将CTLineDraw(line, context)换成自定义的画行函数drawLine(line, context: context), 那么接下来就是画删除线了
// 画删除线, 这里涉及到字体相关知识, 请参考第二节, 画删除线实际画在字的中间, 而字体的高度不一样, 实际是画在x高度的一半位置
func drawStrikethroughStyle(run: CTRun, attributes: NSDictionary, context: CGContext) {
   // 1.获取删除线样式
   let styleRef = attributes[NSStrikethroughStyleAttributeName]
   var style: NSUnderlineStyle = .StyleNone
   CFNumberGetValue(styleRef as! CFNumber, CFNumberType.SInt64Type, &style)
   
   // 如果定义为none, 就不用画了
   guard style != .StyleNone else {
       return
   }
   
   // 2.获得画线的宽度
   var lineWidth: CGFloat = 1
   if (style.rawValue & NSUnderlineStyle.StyleThick.rawValue) == NSUnderlineStyle.StyleThick.rawValue {
       lineWidth *= 2
   }
   
   CGContextSetLineWidth(context, lineWidth)
   
   // 3.获取画线的起点
   var firstPosition = CGPointZero
   let firstGlyphPosition = CTRunGetPositionsPtr(run)
   if nil == firstGlyphPosition {
       let positions = UnsafeMutablePointer<CGPoint>.alloc(1)
       
       positions.initialize(CGPointZero)
       CTRunGetPositions(run, CFRangeMake(0, 0), positions)
       firstPosition = positions.memory
       
       positions.destroy()
   } else {
       firstPosition = firstGlyphPosition.memory
   }
   
   // 4.我们要开始画线了
   CGContextBeginPath(context)
   
   // 5.获取定义的线的颜色, 默认为黑色
   let lineColor = attributes[NSStrikethroughColorAttributeName]
   if nil == lineColor {
       CGContextSetStrokeColorWithColor(context, UIColor.blackColor().CGColor)
   } else {
       CGContextSetStrokeColorWithColor(context, (lineColor as! UIColor).CGColor)
   }
   
   // 6.字体高度, 中间位置为x高度的一半
   let font = attributes[NSFontAttributeName] ?? UIFont.systemFontOfSize(UIFont.systemFontSize())
   var strikeHeight: CGFloat = font.xHeight / 2.0 + firstPosition.y
   
   // 多行调整
   let pt = CGContextGetTextPosition(context)
   strikeHeight += pt.y
   
   // 画线的宽度
   let typographicWidth = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nil, nil, nil))
   
   // 7.开始画线
   CGContextMoveToPoint(context, pt.x + firstPosition.x, strikeHeight)
   CGContextAddLineToPoint(context, pt.x + firstPosition.x + typographicWidth, strikeHeight)
   
   CGContextStrokePath(context)
}
  • 然后在controller中给attributedText添加删除线样式
// 删除线
let strikethroughStyle = [NSStrikethroughStyleAttributeName: NSUnderlineStyle.StyleSingle.rawValue,
                        NSStrikethroughColorAttributeName: UIColor.cyanColor()]
attributedText.addAttributes(strikethroughStyle, range: NSMakeRange(150, 20))

这样就实现了删除线样式效果了

至此, 删除线的样式就完成了, 其他样式将可能在下一篇CoreText文章中实现。
源码在此, 请参考源码中的CoreText/1文件夹!!!

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

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

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

推荐阅读更多精彩内容