2019独角兽企业重金招聘Python工程师标准>>>

OS没有现成的支持图文混排的控件,而要用多个基础控件组合拼成图文混排这样复杂的排版,是件很苦逼的事情。对此的解决方案有使用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生成CTFramesetterRefCTFramesetterRef 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);    // 遍历每个CTLinefor (NSInteger i = 0 ; i < lineCount; i++) {CTLineRef line = (__bridge CTLineRef)lines[i];        NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);        // 遍历每个CTLine中的CTRunfor (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;}

至此我们就完成了使用CoreText进行图文混排,上面获得图片位置的方法只能获得第一张图片位置,大家可以自行完善一下,用数组来进行存储图片绘制区域。唐巧在《iOS开发进阶》一书中更多的介绍了对CoreText的封装,感兴趣的可以看看。

转载于:https://my.oschina.net/u/2361492/blog/526814

使用CoreText实现图文混排相关推荐

  1. [Swift通天遁地]八、媒体与动画-(13)CoreText框架实现图文混排

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ ➤微信公众号:山青咏芝(shanqingyongzhi) ➤博客园地址:山青咏芝(https://www.cnblog ...

  2. iOS - 图文混排技术方案分享

    前言 不少同学在工作中都能遇到图文混排的需求.但是实现图文混排的技术方案有好几种,相应的方案优劣也有差别.今天和大家一起分享一下图文混排的技术方案以及我的选择. Demo和解析工具已经开源 GitHu ...

  3. android多媒体图文混排,干货!!!Android富文本实现图文混排

    效果图 rich.jpg 像图中的效果,大家在开发并不少见,大家可能不知道android提供了实现图文混排的类.大家或许会写一个布局或者使用drawableLeft这个属性实现文本的左侧图标. and ...

  4. Android TextView中图文混排设置行间距导致高度不一致问题解决

    Android TextView中图文混排设置行间距导致高度不一致问题解决 参考文章: (1)Android TextView中图文混排设置行间距导致高度不一致问题解决 (2)https://www. ...

  5. android富文本图片自适应,Android Span富文本图文混排 - ImageSpan(图文垂直居中)...

    ###为文字实现很丰富的特殊效果,当然少不了图文混排 so... 直接上效果(有直接使用和自定义垂直居中效果) ##1 ImageSpan: ImageSpan(context, resourceId ...

  6. TextView图文混排,显示添加的图片,三种常用方法,亲测

    图文混排,文字就不说了,主要是显示图片的方法 1.TextView使用ImageSpan显示图片 [java] view plaincopy <span style="font-siz ...

  7. 利用ListView实现新闻客户端的新闻内容图文混排

    如图: 布局文件: <LinearLayout xmlns:android="<a href="http://schemas.android.com/apk/res/a ...

  8. 计算机基础知识与基本操作文档,计算机基础知识与基本操作——图文混排课件...

    版权所有-中职教学资源网 第25-28课次 图文混排 [本节要点] 本节主要要求掌握图片的插入.图片格式的设置.图形的绘制.艺术字的使用和文本框的使用. [老师寄语] 图文混排是WORD的特色功能之一 ...

  9. NGUI-制作位图字体以及图文混排

    制作字体过程 首先得下载一个位图制作工具Bitmap font generator,可以点击这里下载 1.新建txt文件,输入字体里面包含的文字 2.保存为utf-8格式:点击文件另存为,选择编码格式 ...

最新文章

  1. word中使用MathType能做什么
  2. 经典C语言程序100例之四八
  3. git status中文显示乱码
  4. python导入csv报错_Python Pandas read_csv报错
  5. versa max_如何从Mac(和Vice Versa)打开或关闭iPhone的Safari选项卡
  6. 在阿里云服务器Windows Server 2012r IIS 上部署.NET网站
  7. mysql数据库优化语句_MySQL优化之三:SQL语句优化
  8. chrome开发工具指南之综述
  9. asp.net的条形码
  10. linux 自学系列:文件压缩
  11. 【干货分享】如何使用英文字体做出高逼格的杂志封面
  12. 怎么用ps整合html图片,如何用PS把两张图片合并在一起?
  13. 视频分割神器-MP4文件随意分割
  14. 一炉真香起静中开鸿蒙翻译,【真 香】_古籍全文检索_诗词名句网
  15. XAMP下tomcat无法启动:Make sure you have Java JDK or JRE installed and the required ports are free解决方法
  16. python中 math.isfinite返回值为false_带有Python示例的math.isfinite()方法
  17. c语言复制粘贴快捷键_大家还知道哪些快捷键方法?如:ctrl+c复制,ctrl+v粘贴
  18. Google Earth Engine(GEE)——NDWI水体阈值的监测
  19. 【新学期、新Flag】例文:我的新学期Flag
  20. C语言+windows API仿写类酷狗播放器(1)

热门文章

  1. 能否用痰盂盛饭——谈谈在头文件中定义外部变量
  2. oracle中使用sys_connect_by_path进行表中行值连接
  3. SpringBoot 实战:如何从零开发 “淘宝”
  4. Sentinel隔离和降级
  5. RabbitMQ快速入门--简单队列模型
  6. 方法区中的无用类回收
  7. MyBatis 源码解读-执行SQL
  8. mybatis-翻页
  9. ViewResolvers
  10. SpringCloud(Gateway网关使用)