【iOS】使用CoreText实现图文混排
iOS没有现成的支持图文混排的控件,而要用多个基础控件组合拼成图文混排这样复杂的排版,是件很苦逼的事情。对此的解决方案有使用CoreText进行绘制,或者使用TextKit。本文主要讲解对于CoreText的使用。
案例下载地址
https://github.com/ClavisJ/CoreTextDemo
环境信息:
Mac OS X 10.10.1
Xcode 6.1.1
iOS 8.1
正文:
一、Core Text简介
CoreText是基于IOS3.2及OSX10.5的用于文字精细排版的文本框架。它直接与Core Graphics(又称:Quartz)交互,将需要显示的文本内容,位置,字体,字形直接传递给Quartz,与其他UI组件相比,能更高效的进行渲染。
Core Text 架构图
二、CoreText与UIWebView在排版方面的优劣比较
UIWebView也常用于处理复杂的排版,对应排版他们之间的优劣如下(摘自 《iOS开发进阶》—— 唐巧):
- CoreText占用的内容更少,渲染速度更快。UIWebView占用的内存多,渲染速度慢。
- CoreText在渲染界面的前就可以精确地获得显示内容的高度(只要有了CTFrame即可),而WebView只有渲染出内容后,才能获得内容的高度(而且还需要用JavaScript代码来获取)。
- CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
- 基于CoreText可以做更好的原生交互效果,交互效果可以更加细腻。而UIWebView的交互效果都是用JavaScript来实现的,在 交互效果上会有一些卡顿的情况存在。例如,在UIWebView下,一个简单的按钮按下的操作,都无法做出原生按钮的即时和细腻的按下效果。
CoreText排版的劣势:
- CoreText渲染出来的内容不能像UIWebView那样方便地支持内容的复制。
- 基于CoreText来排版需要自己处理很多复制的逻辑,例如需要自己处理图片与文字混排相关的逻辑,也需要自己实现连接点击操作的支持。
在业界有很多应用都采用CoreText技术进行排版,例如新浪微博客户端,多看阅读客户端,猿题库等等。
三、绘制纯文本
我们创建一个继承于UIView的类,重写他的drawRect方法,来绘制纯文本。
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
// 步骤1:得到当前用于绘制画布的上下文,用于后续将内容绘制在画布上
// 因为Core Text要配合Core Graphic 配合使用的,如Core Graphic一样,绘图的时候需要获得当前的上下文进行绘制
CGContextRef context = UIGraphicsGetCurrentContext();
// 步骤2:翻转当前的坐标系(因为对于底层绘制引擎来说,屏幕左下角为(0,0))
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// 步骤3:创建绘制区域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, self.bounds);
// 步骤4:创建需要绘制的文字与计算需要绘制的区域
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:@"iOS程序在启动时会创建一个主线程,而在一个线程只能执行一件事情,如果在主线程执行某些耗时操作,例如加载网络图片,下载资源文件等会阻塞主线程(导致界面卡死,无法交互),所以就需要使用多线程技术来避免这类情况。iOS中有三种多线程技术 NSThread,NSOperation,GCD,这三种技术是随着IOS发展引入的,抽象层次由低到高,使用也越来越简单。"];
// 步骤5:根据AttributedString生成CTFramesetterRef
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, [attrString length]), path, NULL);
// 步骤6:进行绘制
CTFrameDraw(frame, context);
// 步骤7.内存管理
CFRelease(frame);
CFRelease(path);
CFRelease(frameSetter);
}
运行的效果如下图
CoreText绘制纯文本
四、关于坐标系
上诉代码的步骤2对绘图的坐标系进行了处理,因为在iOS UIKit中,UIView是以左上角为原点,而Core Text一开始的定位是使用与桌面应用的排版系统,桌面应用的坐标系是以左下角为原点,即Core Text在绘制的时候也是参照左下角为原点进行绘制的,所以需要对当前的坐标系进行处理。
实际上,Core Graphic 中的context也是以左下角为原点的, 但是为什么我们用Core Graphic 绘制一些简单的图形的时候不需要对坐标系进行处理喃,是因为通过这个方法UIGraphicsGetCurrentContext()来获得的当前context是已经被处理过的了,用下面方法可以查看指定的上下文的当前图形状态变换矩阵。
NSLog(@"当前context的变换矩阵 %@", NSStringFromCGAffineTransform(CGContextGetCTM(context)));
打印结果为[2, 0, 0, -2, 0, 654],可以发现变换矩阵与CGAffineTransformIdentity的值[1, 0, 0, 1, 0, 0]是 不相同的,并且与设备是否为Retina屏和设备尺寸相关。他的作用是将上下文空间坐标系进行翻转,并使原来的左下角原点变成右上角是原点,并将向上为正 y轴变为向下为正y轴。 所以在使用drawRect的时候,当前的context已经被做了一次翻转,如果不对当前的坐标系进行处理,会发现,绘制出来的文字是镜像上下颠倒的, 如图
不处理context
所以需要先重置当前的坐标系翻转状态,在进行一次翻转,处理之后的矩阵为[2, 0, -0, 2, 0, 0],函数CGContextTranslateCTM的作用变换坐标系中的原点,函数CGContextScaleCTM的作用是改变用户坐标系统的规模比例。
五、自定义文本的颜色,字体与行间距
可以看到我们使用了NSMutableAttributedString这个类来描述需要绘制的文字,而一个NSMutableAttributedString对象可以包含很多属性,每一个属性都有起对应的字符区域,我们可以用这些属性来描述文本中特殊的颜色和字体。
- (void)drawRect:(CGRect)rect {
// 省略前面的步骤1-4
// 步骤8:设置部分文字颜色
[attrString addAttribute:(id)kCTForegroundColorAttributeName value:[UIColor greenColor] range:NSMakeRange(10, 10)];
// 设置部分文字
CGFloat fontSize = 20;
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
[attrString addAttribute:(id)kCTFontAttributeName value:(__bridge id)fontRef range:NSMakeRange(15, 10)];
CFRelease(fontRef);
// 设置行间距
CGFloat lineSpacing = 10;
const CFIndex kNumberOfSettings = 3;
CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
{kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing}
};
CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
[attrString addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0, attrString.length)];
CFRelease(theParagraphRef);
// 省略之后的步骤5-7
}
自定义文本属性
提示:在配置NSMutableAttributedString?的Attribute的时候,用到了很多这样的(__bridge?id)标识,来解释下:这个因为addAttribute:是OC的方法,需要Object C 对象,而CTParagraphStyleRef这 些是由C语言实现的Core Foundation Framework 框架中的对象,这两种类型可以相互转换和操作。Core Foundation Framework 框架中的对象也有引用计数的概念,但是不是Cocoa Framework中的release/retain不同,而是使用自身的CFRetain/CFRelease接口,在使用的时候要多加注意引用和释放 的问题, 更加详细的解释可以参照这篇文章。
六、图文混排
终于要开始进行图文混排了,上面说了那么多,我们来进行一个小结,下图是CoreText绘制的流程图与CTFrame和CTLine,CTRun之间的关系:
CoreText绘制的流程图,CTFrame和CTLine CTRun之间的关系
我们来解释一下这些类:
CFAttributedStringRef :属性字符串,用于存储需要绘制的文字字符和字符属性
CTFramesetterRef:通过CFAttributedStringRef进行初始化,作为CTFrame对象的生产工厂,负责根据path创建对应的CTFrame
CTFrame:用于绘制文字的类,可以通过CTFrameDraw函数,直接将文字绘制到context上
CTLine:在CTFrame内部是由多个CTLine来组成的,每个CTLine代表一行
CTRun:每个CTLine又是由多个CTRun组成的,每个CTRun代表一组显示风格一致的文本
实际上CoreText是不直接支持绘制图片的,但是我们可以先在需要显示图片的地方用一个特殊的空白占位符代替,同时设置 该字体的CTRunDelegate信息为要显示的图片的宽度和高度,这样绘制文字的时候就会先把图片的位置留出来,再在drawRect方法里面用 CGContextDrawImage绘制图片。
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
// 省略步骤1-4 ,步骤8
// 步骤9:图文混排部分
// CTRunDelegateCallbacks:一个用于保存指针的结构体,由CTRun delegate进行回调
CTRunDelegateCallbacks callbacks;
memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;
// 图片信息字典
NSDictionary *imgInfoDic = @{@"width":@100,@"height":@30};
// 设置CTRun的代理
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)imgInfoDic);
// 使用0xFFFC作为空白的占位符
unichar objectReplacementChar = 0xFFFC;
NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
// 将创建的空白AttributedString插入进当前的attrString中,位置可以随便指定,不能越界
[attrString insertAttributedString:space atIndex:50];
// 省略步骤5-6
// 步骤10:绘制图片
UIImage *image = [UIImage imageNamed:@"coretext-img-1.png"];
CGContextDrawImage(context, [self calculateImagePositionInCTFrame:frame], image.CGImage);
// 省略步骤7
}
#pragma mark - CTRun delegate 回调方法
static CGFloat ascentCallback(void *ref) {
return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
}
static CGFloat descentCallback(void *ref) {
return 0;
}
static CGFloat widthCallback(void *ref) {
return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
}
/**
* 根据CTFrameRef获得绘制图片的区域
*
* @param ctFrame CTFrameRef对象
*
* @return绘制图片的区域
*/
- (CGRect)calculateImagePositionInCTFrame:(CTFrameRef)ctFrame {
// 获得CTLine数组
NSArray *lines = (NSArray *)CTFrameGetLines(ctFrame);
NSInteger lineCount = [lines count];
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins);
// 遍历每个CTLine
for (NSInteger i = 0 ; i < lineCount; i++) {
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
// 遍历每个CTLine中的CTRun
for (id runObj in runObjArray) {
CTRunRef run = (__bridge CTRunRef)runObj;
NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) {
continue;
}
NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
if (![metaDic isKindOfClass:[NSDictionary class]]) {
continue;
}
CGRect runBounds;
CGFloat ascent;
CGFloat descent;
runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
runBounds.origin.x = lineOrigins[i].x + xOffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent;
CGPathRef pathRef = CTFrameGetPath(ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
return delegateBounds;
}
}
return CGRectZero;
}
参考资料:
http://geeklu.com/2013/03/core-text/
http://blog.devtang.com/blog/2013/10/21/the-tech-detail-of-ape-client-3/
http://www.yifeiyang.net/development-of-the-iphone-simply-6/
【iOS】使用CoreText实现图文混排的更多相关文章
- CoreText 实现图文混排
CoreText 实现图文混排 相关博文推荐 IOS CoreText.framework - 基本用法 IOS CoreText.framework - 段落样子CTParagraphStyle h ...
- CoreText实现图文混排
CoreText的介绍 Core Text 是基于 iOS 3.2+ 和 OSX 10.5+ 的一种能够对文本格式和文本布局进行精细控制的文本引擎.它良好的结合了 UIKit 和 Core Graph ...
- CoreText实现图文混排之点击事件-b
CoreText实现图文混排之点击事件 主要思路 我们知道,CoreText是基于UIView去绘制的,那么既然有UIView,就有 -(void)touchesBegan:(NSSet<UIT ...
- CoreText实现图文混排之点击事件
今天呢,我们继续把CoreText图文混排的点击事件补充上,这样我们的图文混排也算是圆满了. 哦,上一篇的链接在这里 http://www.jianshu.com/p/6db3289fb05d Cor ...
- CoreText实现图文混排之文字环绕及点击算法
系列文章: CoreText实现图文混排:http://www.jianshu.com/p/6db3289fb05d CoreText实现图文混排之点击事件:http://www.jianshu.co ...
- Coretext实现图文混排及Gif图片播放
CoreText是iOS3.2推出的一套文字排版和渲染框架,可以实现图文混排,富文本显示等效果. CoreText中的几个重要的概念: CTFont CTFontCollection CTFontD ...
- iOS --有行距的图文混排
UILabel *label = [[UILabel alloc]init]; label.numberOfLines = ; [self.view addSubview:label]; label. ...
- ios图文混排
图文混排的形式 1. 富文本形式 2. core Text(文字排版) 3. TextKit 4. UIWebView 一.富文本 我们可以采用attributeString来进行图文混排.例如一个文 ...
- 简单的Coretext 图文混排
在很多新闻类或有文字展示的应用中现在都会出现图文混排的界面例如网易新闻等,乍一看去相似一个网页,其实这样效果并非由UIWebView 加载网页实现.现在分享一种比较简单的实现方式 iOS sdk中为我 ...
随机推荐
- nodejs学习笔记二——链接mongodb
a.安装mongoose库用来链接mongodb数据库 安装mongodb数据库参考mongodb安装 前言(怨言) 本来是想安装mongodb库来链接mongodb的,命令行到nodejs工程目录: ...
- 【Linux驱动】内核等待队列
在Linux中, 一个等待队列由一个"等待队列头"来管理,等待队列是双向链表结构. 应用场合:将等待同一资源的进程挂在同一个等待队列中. 数据结构 在include/linux/w ...
- 三、BLE(上)
1. BLE 1.1 模块构成与结构体层次关系 如上图所示,BLE模块有独立的application layer,这是因为该模块可以直接从BlueCore接收数据(通过GATT模 ...
- noip 模拟赛 匹配 //贪婪策略
匹配(match.pas/match.c/match.cpp) [题目描述] 到了新的学期,Mcx痛苦的发现通用技术课居然是有实验课的,这样的话他就不得不放弃写作业的想法而去做一件类似于搭积木的事情. ...
- 用php怎么写一个用户注册登录的页面呢?
想写就会尽快去写.如果用php写了就一定要用nodejs写出来啊,不写是小狗啊! 补充一下,想要实现的功能: 1.用户名重复检测 2.检测信息填写是否完整 3.邮箱是否已经被注册 4.实现ajax无刷 ...
- BUG集锦
1.0 jQuery 序列化表单数据 serialize() 得到的是 ""; 表单元素没有name属性.(用的美工的页面,没注意这个细节.)
- js无法对远程图片进行Base64转码
web端图片转成Base64码报错 Image from origin 'http://114.215.87.123:8900' has been blocked from loading by Cr ...
- Rebalance Customer Balances Utility的使用
用户不管是打开A/R Posted Transactions Detail还是A/R Posted Transactions Summary 窗口,均显示如下一个警示: 打开Currency Code ...
- 【FTP】在自己的电脑上建立FTP服务器
<1> 添加新用户: “计算机”点击右键 “管理”得到: 点击“本地用户和组”,点击“用户”: 右键 “新用户”: 设定用户名与密码,这是以后连FTP服务器时用的登录用户名与密码. < ...
- Ext.NET 4.1 系统框架的搭建(后台) 附源码
Ext.NET 4.1 系统框架的搭建(后台) 附源码 代码运行环境:.net 4.5 VS2013 (代码可直接编译运行) 预览图: 分析图: 上面系统的构建包括三块区域:North.West和C ...