前言
当喜悦、愤怒、疑惑、懵逼等等这些情绪都能使用表情表达时,我干嘛还要打字
这是一个移动端快速发展的时代,不管你承不承认,作为一个app开发者,社交属性总是或多或少出现在我们开发的业务需求中,其中作为IM最重要的组成元素——表情,如何进行文字和表情混合编程是一门重要的技术。
本文将使用iOS中的coreText框架来完成我们的图文混编之旅,除此之外,还实现文本超链接效果。在开始本篇的代码之前,我们先通过iOS框架结构图来了解CoreText
所处的位置:
coreText基础
首先我们要知道图文混编的原理 —— 在需要显示图片的文本位置使用特殊的字符显示,然后在绘制这些文本的将图片直接绘制显示在这些特殊文本的位置上。因此,图文混编的任务离不开一个重要的角色——NSAttributedString
这个对比NSString
多了各种类似粗斜体、下划线、背景色等文本属性的NSAttributedString
,每个属性都有其对应的字符区域。这意味着你可以将前几个字符设置为粗体,而后面的字符为斜体且带着下划线。在iOS6之后已经有能够设置控件的富文本属性了,但如果想要实现我们的图文混编,我们需要使用coreText
来对属性字符串进行绘制。在coreText
绘制字符的过程中,最重要的两个概念是CTFramesetterRef
跟CTFrameRef
,他们的概念如下:
在创建好要绘制的富文本字符串之后,我们用它来创建一个CTFramesetterRef
变量,这个变量可以看做是CTFrameRef
的一个工厂,用来辅助我们创建后者。在传入一个CGPathRef
的变量之后我们可以创建相应的CTFrameRef
然后将富文本渲染在对应的路径区域内。这段创建代码如下(由于coreText
基于C语言的库,所有对象都需要我们手动释放内存):
1 2 3 4 5 6 7 8 9 10 11 12 |
CGContextRef ctx = UIGraphicsGetCurrentContext(); NSAttributedString * content = [[NSAttributedString alloc] initWithString: @"这是一个测试的富文本,这是一个测试的富文本"]; CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content); CGMutablePathRef paths = CGPathCreateMutable(); CGPathAddRect(paths, NULL, CGRectMake(0, 0, 100, 100)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, content), paths, NULL); CTFrameDraw(frame, ctx); //绘制文字 // 释放内存 CFRelease(paths); CFRelease(frame); CFRelease(framesetter); |
除此之外,每一个创建的CTFrameRef
中存在一个或者更多的CTLineRef
变量,这个变量表示绘制文本中的每一行文本。每个CTLineRef
变量中存在一个或者更多个CTRunRef
变量,在文本绘制过程中,我们并不关心CTLineRef
或者CTRunRef
变量具体对应的是什么字符,这些工作在更深层次系统已经帮我们完成了创建。创建过程的图如下:
通过CTFrameRef
获取文本内容的行以及字符串组的代码如下:
1 2 3 4 |
CFArrayRef lines = CTFrameGetLines(frame); CGPoint lineOrigins[CFArrayGetCount(lines)]; CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), lineOrigins); for (int idx = 0; idx |
图文混编的做法就是在我们需要插入表情的富文本位置插入一个空字符占位,然后实现自定义的CTRunDelegateCallbacks
来设置这个占位字符的宽高位置信息等,为占位字符添加一个自定义的文本属性用来存储对应的表情图片名字。接着我们通过CTFrameRef
获取渲染的文本行以及文本字符,判断是否存在存储的表情图片,如果是就将图片绘制在占位字符的位置上。
下面代码是创建一个CTRunDelegate
的代码,用来设置这个字符组的大小尺寸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void RunDelegateDeallocCallback(void * refCon) {} CGFloat RunDelegateGetAscentCallback(void * refCon) { return 20; } CGFloat RunDelegateGetDescentCallback(void * refCon) { return 0; } CGFloat RunDelegateGetWidthCallback(void * refCon) { return 20; } CTRunDelegateCallbacks imageCallbacks; imageCallbacks.version = kCTRunDelegateVersion1; imageCallbacks.dealloc = RunDelegateDeallocCallback; imageCallbacks.getWidth = RunDelegateGetWidthCallback; imageCallbacks.getAscent = RunDelegateGetAscentCallback; imageCallbacks.getDescent = RunDelegateGetDescentCallback; CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, "这是回调函数的参数"); |
文字排版
文字排版属于又臭又长的理论概念,但是对于我们更好的使用coreText
框架,这些理论知识却是不可缺少的。
- 字体
与我们所认知的字体不同的是,在计算机中字体指的是一系列的相同样式、相同大小的字形的集合,即14号宋体跟15号宋体在计算机看来是两种字体。而我们所说的字体指 宋体 / 楷体 这些字体类型 - 字符与字形
文本排版的过程实际上是从字符到字形之间的转换。字符表示的是文字本身的信息意义,而字形表示的是这个文字的图形表现格式。同一个字符由于大小、体形之间的差别,存在着不同字形。由于连写的存在,多个字符可能只对应一个字形:连写对应单个字形 - 字形描述集
描述了字形表现的多个参数,包括:
1、边框(Bounding box):一个假想的边框,尽可能的将整个字形容纳
2、基线(Baseline):一条假想的参考线,以此为基础渲染字形。正常来说字母x、m、s最下方的位置就是参考线所在y坐标
3、基础原点(Origin):基线最左侧的坐标点
4、行间距(Leading):行与行之间的间距
5、字间距(Kerning):字与字之间的间距
6、上行高度(Ascent):字形最高点到基线的距离,正数。同一行取字符最大的上行高度为该行的上行高度
7、下行高度(Descent):字形最低点到基线的距离,负数。同一行取字符最小的下行高度为该行的下行高度字形描述属性下图中绿色线条表示基线,黄色线条表示下行高度,绿色线条到红框最顶部的距离为上行高度,而黄色线条到红框底部的距离为行间距。因此行高的计算公式是
lineHeight = Ascent + |Descent| + Leading
字符描述属性
图文混编
前文讲了诸多的理论知识,终于来到了实战的阶段,先放上本文的demo地址和效果图:
由于富文本的绘制需要用到一个CGContextRef
类型的上下文,那么创建一个继承自UIView
的自定义控件并且在drawRect:
方法中完成富文本绘制是最方便的方式,我给自己创建的类命名为LXDTextView
在CoreText
绘制文本的时候,坐标系的原点位于左下角,因此我们需要在绘制文字之前对坐标系进行一次翻转。并且在绘制富文本之前,我们需要构建好渲染的富文本并在方法里返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
- (NSMutableAttributedString *)buildAttributedString { //创建富文本,并且将超链接文本设置为蓝色+下划线 NSMutableAttributedString * content = [[NSMutableAttributedString alloc] initWithString: @"这是一个富文本内容"]; NSString * hyperlinkText = @"@这是链接"; NSRange range = NSMakeRange(content.length, hyperlinkText.length); [content appendAttributedString: [[NSAttributedString alloc] initWithString: hyperlinkText]]; [content addAttributes: @{ NSForegroundColorAttributeName: [UIColor blueColor] } range: range]; [content addAttributes: @{ NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle) } range: range]; //创建CTRunDelegateRef并设置回调函数 CTRunDelegateCallbacks imageCallbacks; imageCallbacks.version = kCTRunDelegateVersion1; imageCallbacks.dealloc = RunDelegateDeallocCallback; imageCallbacks.getWidth = RunDelegateGetWidthCallback; imageCallbacks.getAscent = RunDelegateGetAscentCallback; imageCallbacks.getDescent = RunDelegateGetDescentCallback; NSString * imageName = @"emoji"; CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void *)imageName); //插入空白表情占位符 NSMutableAttributedString * imageAttributedString = [[NSMutableAttributedString alloc] initWithString: @" "]; [imageAttributedString addAttribute: (NSString *)kCTRunDelegateAttributeName value: (__bridge id)runDelegate range: NSMakeRange(0, 1)]; [imageAttributedString addAttribute: @"imageNameKey" value: imageName range: NSMakeRange(0, 1)]; [content appendAttributedString: imageAttributedString]; CFRelease(runDelegate); return content; } - (void)drawRect: (CGRect)rect { //获取图形上下文并且翻转坐标系 CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(ctx, CGAffineTransformIdentity); CGContextConcatCTM(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, self.bounds.size.height)); NSMutableAttributedString * content = [self buildAttributedString]; //创建CTFramesetterRef和CTFrameRef变量 CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content); CGMutablePathRef paths = CGPathCreateMutable(); CGPathAddRect(paths, NULL, self.bounds); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, content.length), paths, NULL); CTFrameDraw(frame, ctx); //遍历文本行以及CTRunRef,将表情文本对应的表情图片绘制到图形上下文 CFArrayRef lines = CTFrameGetLines(_frame); CGPoint lineOrigins[CFArrayGetCount(lines)]; CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins); for (int idx = 0; idx |
代码运行之后,代码的运行图应该是这样:
现在富文本像模像样了,但我们怎样才能在点击超链接文本的时候发生响应回调呢?像图片那样判断点击是否处在rect范围内的判断是不可取的,因为超链接文本可能刚好处在换行的位置,从而存在多个rect。对此,CoreText
同样提供了一个函数CTLineGetStringIndexForPosition(CTLineRef, CGPoint)
方法来获取点击坐标位于文本行的字符的下标位置。但在此之前,我们必须先获取点击点所在的文本行数位置,为了达到获取文本行的目的,绘制文本的CTFrameRef
变量必须保存下来,因此我定义了一个实例变量存储文本渲染中生成的CTFrameRef
。同样的,对于超链接文本所在的位置,我们应该把这个位置转换成字符串作为key
值,文本对应的链接作为value
值存到一个实例字典中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |