iOS:基于CoreText的排版引擎

时间:2021-09-10 11:44:15

一、CoreText的简介

CoreText是用于处理文字和字体的底层技术。它直接和Core Graphics(又被称为Quartz)打交道。Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。

下面是CoreText的架构图,可以看到,CoreText处在非常底层的位置,上层的UI控件(包含UILable、UITextField及UITextView)和UIWebView都是基于CoreText来实现的。

iOS:基于CoreText的排版引擎

UIWebview也是处理复杂的文字排版的备选方案。对于排版,基于CoreText和基于UIWebView相比,具有以下不同点:

  • CoreText占用内存更少,渲染速度更快,UIWebView占用内存多,渲染速度慢。
  • CoreText在渲染界面前就可以精确地获得显示内容的高度(只要有了CTFrame即可),而UIWebView只有渲染出内容后,才能获得内容的高度(而且还需要通过JavaScript代码来获取)。
  • CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
  • 基于CoreText可以做更好的原生交互效果,交互效果可以更细腻。而UIWebView的交互效果都是利用JavaScript来实现的,在交互效果上会有一些卡顿情况存在。例如,在UIWebView下,一个简单的按钮按下操作,都无法做出原生按钮的即时和细腻的按下效果。

当然,基于CoreText的排版方案也有那么一些劣势:

  • CoreText渲染出来的内容不能像UIWebView那样方便的支付内容的复制。
  • 基于CoreText来排版需要自己处理很多复杂逻辑,例如需要自己处理图片和文字混排相关的逻辑,也需要自己实现链接点击操作的支持。
、图文混排
CTFrameRef  textFrame     // coreText 的 frame
CTLineRef   line          // coreText 的 line
CTRunRef    run           // line  中的部分文字 、相关方法:
CFArrayRef CTFrameGetLines(CTFrameRef frame) //获取包含CTLineRef的数组
void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[])//获取所有CTLineRef的原点
CFRange CTLineGetStringRange(CTLineRef line) //获取line中文字在整段文字中的Range
CFArrayRef CTLineGetGlyphRuns(CTLineRef line)//获取line中包含所有run的数组
CFRange CTRunGetStringRange(CTRunRef run)//获取run在整段文字中的Range
CFIndex CTLineGetStringIndexForPosition(CTLineRef line,CGPoint position)//获取点击处position文字在整段文字中的index
CGFloat CTLineGetOffsetForStringIndex(CTLineRef line,CFIndex charIndex,CGFloat* secondaryOffset)//获取整段文字中charIndex位置的字符相对line的原点的x值

二、基于CoreText的基础排版引擎

简单实现步骤:

a.自定义View,重写drawRect方法,后面的操作均在其中进行

b.得到当前绘图上下问文,用于后续将内容绘制在画布上

c.将坐标系翻转

d.创建绘制的区域,写入要绘制的内容

示例1:不带图片的排版引擎,只是显示文本内容,而且不设置文字的属性信息

自定义的CTDispalyView.m

//  CTDispalyView.m
// CoreTextDemo
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved. #import "CTDispalyView.h" //导入CoreText系统框架
#import <CoreText/CoreText.h> @implementation CTDispalyView //重写drawRect方法
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; //1.获取当前绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext(); //2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, , self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0); //3.创建绘制局域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds); //4.设置绘制内容
NSAttributedString *attString = [[NSAttributedString alloc] initWithString:
@"CoreText是用于处理文字和字体的底层技术。"
"它直接和Core Graphics(又被称为Quartz)打交道。"
"Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。"
"Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。"
"因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。"
"与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。"];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(, [attString length]), path, NULL); //5.开始绘制
CTFrameDraw(frame, context); //6.释放资源
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
}
@end

在ViewController.m实现显示

//  ViewController.m
// CoreTextDemo
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved. #import "ViewController.h"
#import "CTDispalyView.h" @interface ViewController ()
@end @implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor]; //显示内容
CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:CGRectMake(, , , )];
dispaleView.center = self.view.center;
dispaleView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:dispaleView];
}
@end

演示结果截图

iOS:基于CoreText的排版引擎

三、基于CoreText的基本封装

发现,虽然上面效果确实达到了我们的要求,但是,很有局限性,因为它仅仅是展示了CoreText排版的基本功能而已。要制作一个比较完善的排版引擎,我们不能简单的将所有的代码都放到CTDisplayView的drawRect方法中。根据设计模式的“单一功能原则”,我们应该把功能拆分,把不同的功能都放到各自不同的类里面进行。

对于一个复杂的排版引擎来说,可以将功能拆分为以下几个类来完成:

1、一个显示用的类,仅仅负责显示内容,不负责排版

2、一个模型类,用于承载显示所需要的所有数据

3、一个排版类,用于实现文字内容的排版

4、一个配置类,用于实现一些排版时的可配置项

例如定义的4个类分别为:

CTFrameParserConfig类:用于配置绘制的参数,例如文字颜色、大小、行间距等

CTFrameParser类:用于生成最后绘制界面需要的CTFrameRef实例

CoreTextData类:用于保存由CTFrameParser类生成的CTFrameRef实例,以及CTFrameRef实际绘制需要的高度

CTDisplayView类:持有CoreTextData类实例,负责将CFFrameRef绘制在界面上。

关于这4个类的关键代码如下:

CTFrameParserConfig

//  CTFrameParserConfig.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h> @interface CTFrameParserConfig : NSObject //配置属性
@property (nonatomic ,assign)CGFloat width;
@property (nonatomic, assign)CGFloat fontSize;
@property (nonatomic, assign)CGFloat lineSpace;
@property (nonatomic, strong)UIColor *textColor; @end
//  CTFrameParserConfig.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTFrameParserConfig.h" @implementation CTFrameParserConfig //初始化
-(instancetype)init{
self = [super init];
if (self) {
_width = .f;
_fontSize = 16.0f;
_lineSpace = 8.0f;
_textColor = RGB(, , );
}
return self;
} @end

CTFrameParser

//  CTFrameParser.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h>
#import "CoreTextData.h" @class CTFrameParserConfig;
@interface CTFrameParser : NSObject /**
* 给内容设置配置信息
*
* @param content 内容
* @param config 配置信息
*
*/
+(CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config; @end
//  CTFrameParser.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTFrameParser.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h" @implementation CTFrameParser //给内容设置配置信息
+(CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config{ NSDictionary *attributes = [self attributesWithConfig:config];
NSAttributedString *contextString = [[NSAttributedString alloc] initWithString:content attributes:attributes]; //创建CTFrameStterRef实例
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)contextString); //获得要绘制的区域的高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(, ), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height; //生成CTFrameRef实例
CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight]; //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
CoreTextData *data = [[CoreTextData alloc] init];
data.ctFrame = frame;
data.height = textHeight; //释放内存
CFRelease(framesetter);
CFRelease(frame); return data;
} //配置信息格式化
+(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config{ CGFloat fontSize = config.fontSize;
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
CGFloat lineSpcing = config.lineSpace;
const CFIndex kNumberOfSettings = ;
CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
{kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpcing},
{kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpcing},
{kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpcing},
}; CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
UIColor *textColor = config.textColor; NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef; CFRelease(fontRef);
CFRelease(theParagraphRef);
return dict;
} //创建CTFrameRef绘制路径实例
+(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{ CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(, , config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(, ), path, NULL);
CFRelease(path);
return frame;
} @end

CoreTextData

//  CoreTextData.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h> @interface CoreTextData : NSObject @property (assign,nonatomic)CTFrameRef ctFrame;
@property (assign,nonatomic)CGFloat height; @end
//  CoreTextData.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CoreTextData.h" @implementation CoreTextData //CoreFoundation不支持ARC,需要手动去管理内存的释放
-(void)setCtFrame:(CTFrameRef)ctFrame{
if (_ctFrame != ctFrame) {
if (_ctFrame !=nil) {
CFRelease(_ctFrame);
}
}
CFRetain(ctFrame);
_ctFrame = ctFrame;
} -(void)dealloc{
if (_ctFrame != nil) {
CFRelease(_ctFrame);
_ctFrame = nil;
}
} @end

CTDisplayView

//  CTDispalyView.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <UIKit/UIKit.h>
#import "CoreTextData.h" @interface CTDispalyView : UIView
@property(strong,nonatomic)CoreTextData *data;
@end
//  CTDispalyView.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTDispalyView.h" //导入CoreText系统框架
#import <CoreText/CoreText.h> @implementation CTDispalyView //重写drawRect方法
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; //1.获取当前绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext(); //2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, , self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0); //3.绘制内容
if (self.data) {
CTFrameDraw(self.data.ctFrame, context);
}
} @end

除了这4个类外,在代码中还创建了基本的宏定义和分类Category,分别是CoreTextDemo.pch、UIView+Frame.h(快速访问view的尺寸)

CoreTextDemo.pch

//  CoreTextDemo.pch
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #ifndef CoreTextDemo_pch
#define CoreTextDemo_pch #ifdef DEBUG
#define debugLog(...) NSLog(__VA_ARGS__)
#define debugMethod() NSLog(@"%s",__func__)
#else
#define debugLog(...)
#define debugMethod()
#endif #define RGB(R,G,B) [UIColor colorWithRed:R/255.0 green:G/255.0 blue:B/255.0 alpha:1.0] #import <Foundation/Foundation.h>
#import "UIView+Frame.h"
#import <CoreText/CoreText.h> #endif

UIView+Frame.h

//  UIView+Frame.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <UIKit/UIKit.h>
#import <Foundation/Foundation.h> @interface UIView (Frame) -(CGFloat)x;
-(void)setX:(CGFloat)x; -(CGFloat)y;
-(void)setY:(CGFloat)y; -(CGFloat)height;
-(void)setHeight:(CGFloat)height; -(CGFloat)width;
-(void)setWidth:(CGFloat)width; @end
//  UIView+Frame.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "UIView+Frame.h" @implementation UIView (Frame) -(CGFloat)x{
return self.frame.origin.x;
}
-(void)setX:(CGFloat)x{
self.frame = CGRectMake(x, self.y, self.width, self.height);
} -(CGFloat)y{
return self.frame.origin.y;
}
-(void)setY:(CGFloat)y{
self.frame = CGRectMake(self.x, y, self.width, self.height);
} -(CGFloat)height{
return self.frame.size.height;
}
-(void)setHeight:(CGFloat)height{
self.frame = CGRectMake(self.x, self.y, self.width, height);
} -(CGFloat)width{
return self.frame.size.width;
}
-(void)setWidth:(CGFloat)width{
self.frame = CGRectMake(self.x, self.y, width, self.height);
} @end

示例2:不带图片的排版引擎,只是显示文本内容,设置文字的一些简单的属性信息

//  ViewController.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "ViewController.h"
#import "CTDispalyView.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h"
#import "CTFrameParser.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor]; //创建画布
CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:CGRectMake(, , , )];
dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-);
dispaleView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:dispaleView]; //设置配置信息
CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
config.textColor = [UIColor redColor];
config.width = dispaleView.width; //设置内容
CoreTextData *data = [CTFrameParser parseContent:@"CoreText是用于处理文字和字体的底层技术。"
"它直接和Core Graphics(又被称为Quartz)打交道。"
"Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。"
"Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。"
"因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。"
"与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。" config:config];
dispaleView.data = data;
dispaleView.height = data.height;
dispaleView.backgroundColor = [UIColor yellowColor];
} @end

演示结果截图

iOS:基于CoreText的排版引擎

好了,效果确实是实现了,现在来看看本框架的UML示意图,这4个类的关系是这样的:

1、CTFrameParser通过CTFrameParserConfig实例来生成CoreTextData实例;

2、CTDisplayView通过持有CoreTextData实例来获取绘制所需要的所有信息;

3、ViewController类通过配置CTFrameParserConfig实例,进而获得生成的CoreTextData实例,最后将其赋值给CTDisplayView成员,达到将指定内容显示在界面的效果。

iOS:基于CoreText的排版引擎

四、定制排版文件格式

对于上面的例子,我们给CTFrameParser增加了一个将NSString转换为CoreTextData的方法。但是这样的实现方式有很多的局限性,因为整个内容虽然可以定制字体大小、颜色、行高等信息,但是却不能支持定制内容中某一个部分。例如,如果我们只想让内容的某几个字显示成红色并将字体变大,而让其他的文字显示成黑色而且字体不变,那么就办不到了。

解决办法:让CTFrameParser支持接受NSAttributeString作为参数,然后在ViewController中设置我们想要的NSAttributeString信息。

更改后的CTFrameParser

//  CTFrameParser.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h>
#import "CoreTextData.h" @class CTFrameParserConfig;
@interface CTFrameParser : NSObject /**
* 给内容设置配置信息
*
* @param content 内容
* @param config 配置信息
*
*/
+(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config; /**
* 配置信息格式化
*
* @param config 配置信息
*/
+(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config; @end
//  CTFrameParser.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTFrameParser.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h" @implementation CTFrameParser //给内容设置配置信息
+(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config{ //创建CTFrameStterRef实例
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content); //获得要绘制的区域的高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(, ), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height; //生成CTFrameRef实例
CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight]; //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
CoreTextData *data = [[CoreTextData alloc] init];
data.ctFrame = frame;
data.height = textHeight; //释放内存
CFRelease(framesetter);
CFRelease(frame); return data;
} //配置信息格式化
+(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config{ CGFloat fontSize = config.fontSize;
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
CGFloat lineSpcing = config.lineSpace;
const CFIndex kNumberOfSettings = ;
CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
{kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpcing},
{kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpcing},
{kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpcing},
}; CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
UIColor *textColor = config.textColor; NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef; CFRelease(fontRef);
CFRelease(theParagraphRef);
return dict;
} //创建CTFrameRef绘制路径实例
+(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{ CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(, , config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(, ), path, NULL);
CFRelease(path);
return frame;
} @end

示例3:不带图片的排版引擎,只是显示文本内容,通过富文本更改文字的一些简单的属性信息

//  ViewController.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "ViewController.h"
#import "CTDispalyView.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h"
#import "CTFrameParser.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor]; //创建画布
CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:CGRectMake(, , , )];
dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-);
dispaleView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:dispaleView]; //设置配置信息
CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
config.textColor = [UIColor blackColor];
config.width = dispaleView.width; //内容
NSString *content =
@"CoreText是用于处理文字和字体的底层技术。"
"它直接和Core Graphics(又被称为Quartz)打交道。"
"Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。"
"Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。"
"因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。"
"与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。"; //设置富文本
NSDictionary *attr = [CTFrameParser attributesWithConfig:config];
NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:content attributes:attr];
[attributeString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:] range:NSMakeRange(, )];
[attributeString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(, )]; //创建绘制数据实例
CoreTextData *data = [CTFrameParser parseAttributedContent:attributeString config:config];
dispaleView.data = data;
dispaleView.height = data.height;
dispaleView.backgroundColor = [UIColor yellowColor];
} @end

演示结果截图

iOS:基于CoreText的排版引擎

更进一步,实际工作中,我们更希望通过一个排版文件,来设置需要排版的文字的内容、颜色、字体大小等信息。我们规定排版的模板文件为JSON格式。排版格式示例文件如下:

iOS:基于CoreText的排版引擎

[
{
"color":"blue",
"content":"CoreText是用于处理文字和字体的底层技术。",
"size":16,
"type":"txt"
},
{
"color":"red",
"content":"它直接和Core Graphics(又被称为Quartz)打交道。",
"size":22,
"type":"txt"
},
{
"color":"black",
"content":"Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。",
"size":16,
"type":"txt"
},
{
"color":"blue",
"content":"Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。",
"size":16,
"type":"txt"
},
{
"color":"default",
"content":"因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。",
"type":"txt"
}
]

通过苹果提供的NSJSONSeriallization类,我们可以将上面的模板文件转换成NSArray数组,每一个数组元素是一个Dictionary,代表一段相同设置的文字。为了简单,我们配置文件只支持配置颜色和字号,但是以后可以根据同样的思想,很方便地增加其他配置信息。

现在修改CTFrameParser类,增加如下的这些方法,让其可以从如上格式的模板文件中生成CoreTextData。最终实现代码如下:

更改后的CTFrameParser:

//  CTFrameParser.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h>
#import "CoreTextData.h" @class CTFrameParserConfig;
@interface CTFrameParser : NSObject /**
* 给内容设置配置信息
*
* @param content 内容
* @param config 配置信息
*
*/
+(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config; /**
* 给内容设置配置信息
*
* @param path 模板文件路径
* @param config 配置信息
*
*/
+(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config; @end
//  CTFrameParser.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTFrameParser.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h" @implementation CTFrameParser //方法一:用于提供对外的接口,调用方法二实现从一个JSON的模板文件中读取内容,然后调用方法五生成的CoreTextData
+(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config{ NSAttributedString *content = [self loadTemplateFile:path config:config];
return [self parseAttributedContent:content config:config];
} //方法二:读取JSON文件内容,并且调用方法三获得从NSDcitionay到NSAttributedString的转换结果
+(NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config{
NSData *data = [NSData dataWithContentsOfFile:path];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
if (data) {
NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if ([array isKindOfClass:[NSArray class]]) {
for (NSDictionary *dict in array) {
NSString *type = dict[@"type"];
if ([type isEqualToString:@"txt"]) {
NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
[result appendAttributedString:as];
}
}
}
}
return result;
} //方法三:将NSDcitionay内容转换为NSAttributedString
+(NSAttributedString *)parseAttributeContentFromNSDictionary:(NSDictionary*)dict config:(CTFrameParserConfig *)config{ NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]]; //设置颜色
UIColor *color = [self colorFromTemplate:dict[@"color"]];
if (color) {
attributes[(id)kCTForegroundColorAttributeName] = (id)color.CGColor;
} //设置字号
CGFloat fontSize = [dict[@"size"] floatValue];
if (fontSize>) {
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
attributes[(id)kCTFontAttributeName] = (__bridge id)fontRef;
CFRelease(fontRef);
} NSString *content = dict[@"content"];
return [[NSAttributedString alloc] initWithString:content attributes:attributes];
} //方法四:提供将NSString转换为UIColor的功能
+(UIColor *)colorFromTemplate:(NSString *)name{ if ([name isEqualToString:@"blue"]) {
return [UIColor blueColor];
}else if ([name isEqualToString:@"red"]){
return [UIColor redColor];
}else if ([name isEqualToString:@"black"]){
return [UIColor blackColor];
}else{
return nil;
}
} //方法五:接受一个NSAttributedString和一个Config参数,将NSAttributedString转换成CoreTextData返回
+(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config{ //创建CTFrameStterRef实例
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content); //获得要绘制的区域的高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(, ), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height; //生成CTFrameRef实例
CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight]; //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
CoreTextData *data = [[CoreTextData alloc] init];
data.ctFrame = frame;
data.height = textHeight; //释放内存
CFRelease(framesetter);
CFRelease(frame); return data;
} //方法六:方法五的一个辅助函数,供方法五调用
+(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{ CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(, , config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(, ), path, NULL);
CFRelease(path);
return frame;
} @end

示例4:不带图片的排版引擎,只是显示文本内容,通过排版文件格式更改文字的一些简单的属性信息

//
// ViewController.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "ViewController.h"
#import "CTDispalyView.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h"
#import "CTFrameParser.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor]; //创建画布
CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:CGRectMake(, , , )];
dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-);
dispaleView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:dispaleView]; //设置配置信息
CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
config.width = dispaleView.width; //获取模板文件
NSString *path = [[NSBundle mainBundle] pathForResource:@"JsonTemplate" ofType:@"json"]; //创建绘制数据实例
CoreTextData *data = [CTFrameParser parseTemplateFile:path config:config];
dispaleView.data = data;
dispaleView.height = data.height;
dispaleView.backgroundColor = [UIColor yellowColor];
} @end

演示结果截图

iOS:基于CoreText的排版引擎

可以看到,通过一个简单的模板文件,我们可以很方便地定义排版的配置信息了。

五、支持图文混排的排版引擎

在上面的示例中,我们在设置模板文件的时候,就专门在模板文件里面预留了一个名为type的字段,用于表示内容的类型。之前的type的值都是txt,这次,我们增加一个img的值,用于表示图片。同时给img类型的内容还需要配置3个属性如下:

1、width:用于设置图片显示的宽度

2、height:用于设置图片显示的高度

3、name:用于设置图片的资源名

也即文件格式如下:

iOS:基于CoreText的排版引擎

在改造代码之前,先来了解一下CTFrame内部的CTLine和CTRun。

在CTFrame内部,是有多个CTLine类组成的,每一个CTLine代表一行,每个CTLine又是由多个CTRun来组成,每一个CTRun代表一组显示风格一致的文本。我们不用手工管理CTLine和CTRun的创建过程。

CTLine和CTRun示意图如下:

iOS:基于CoreText的排版引擎

示意图解释:

可以看到,第一行的CTLine是由两个CTRun构成的,第一个CTRun为红色大字号的左边部分,第二个CTRun为右边黑色小字号部分。

虽然我们不用管理CTRun的创建过程,但是我们可以设置某一个具体的CTRun的CTRunDelegate来指定该文本在绘制时的高度、宽度、排列对齐方式等信息。

对于图片的排版,其实,CoreText本质上是不支持的,但是,可以在显示文本的地方,用一个特殊的空白字符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度信息,这样最后生成的CTFrame实例,就会在绘制时将图片的位置预留出来。以后,在CTDisplayView的drawRect方法中使CGContextDrawImage方法直接绘制出来就行了。

改造模板解析类,要做的工作有:

  • 增加一个CoreTextImageData类,寄存图片信息
  • 改造CTFrameParser的parserTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config方法,使其支持type为omg的节点解析。并且对type为omg的节点,设置其CTRunDelegate信息,使其在绘制时,为图片预留相应的空白位置。
  • 改造CoreTextData类,增加图片相关的信息,并且增加计算图片绘制局域的逻辑。
  • 改造CTDisplayView类,增加绘制图片的相关的逻辑。

具体的改造如下:

新添加CoreTextImageData类:

//  CoreTextImageData.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/26.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h> @interface CoreTextImageData : NSObject //图片资源名称
@property (copy,nonatomic)NSString *name;
//图片位置的起始点
@property (assign,nonatomic)CGFloat position;
//图片的尺寸
@property (assign,nonatomic)CGRect imagePostion; @end
//  CoreTextImageData.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/26.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CoreTextImageData.h" @implementation CoreTextImageData @end

修改CTFrameParser解析类:

//  CTFrameParser.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h>
#import "CoreTextData.h" @class CTFrameParserConfig;
@interface CTFrameParser : NSObject /**
* 配置信息格式化
*
* @param config 配置信息
*/
+(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config; /**
* 给内容设置配置信息
*
* @param content 内容
* @param config 配置信息
*/
+(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config; /**
* 给内容设置配置信息
*
* @param path 模板文件路径
* @param config 配置信息
*/
+(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config; @end
//  CTFrameParser.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTFrameParser.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h"
#import "CoreTextImageData.h" @implementation CTFrameParser //配置信息格式化
+(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config{ CGFloat fontSize = config.fontSize;
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
CGFloat lineSpcing = config.lineSpace;
const CFIndex kNumberOfSettings = ;
CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
{kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpcing},
{kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpcing},
{kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpcing},
}; CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
UIColor *textColor = config.textColor; NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef; CFRelease(fontRef);
CFRelease(theParagraphRef);
return dict;
} #pragma mark - 新增的方法 //方法一:用于提供对外的接口,调用方法二实现从一个JSON的模板文件中读取内容,然后调用方法五生成的CoreTextData
+(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config{ NSMutableArray *imageArray = [NSMutableArray array];
NSAttributedString *content = [self loadTemplateFile:path config:config imageArray:imageArray];
CoreTextData *data = [self parseAttributedContent:content config:config];
data.imageArray = imageArray; return data;
} //方法二:读取JSON文件内容,并且调用方法三获得从NSDcitionay到NSAttributedString的转换结果
+(NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{
NSData *data = [NSData dataWithContentsOfFile:path];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
if (data) { NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; if ([array isKindOfClass:[NSArray class]]) {
for (NSDictionary *dict in array) { NSString *type = dict[@"type"]; if ([type isEqualToString:@"txt"]) { NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
[result appendAttributedString:as]; }else if ([type isEqualToString:@"img"]){ //创建CoreTextImageData,保存图片到imageArray数组中
CoreTextImageData *imageData = [[CoreTextImageData alloc] init];
imageData.name = dict[@"name"];
imageData.position = [result length];
[imageArray addObject:imageData]; //创建空白占位符,并且设置它的CTRunDelegate信息
NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
[result appendAttributedString:as];
}
}
}
}
return result;
} //方法三:将NSDcitionay内容转换为NSAttributedString
+(NSAttributedString *)parseAttributeContentFromNSDictionary:(NSDictionary*)dict config:(CTFrameParserConfig *)config{ NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]]; //设置颜色
UIColor *color = [self colorFromTemplate:dict[@"color"]];
if (color) {
attributes[(id)kCTForegroundColorAttributeName] = (id)color.CGColor;
} //设置字号
CGFloat fontSize = [dict[@"size"] floatValue];
if (fontSize>) {
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
attributes[(id)kCTFontAttributeName] = (__bridge id)fontRef;
CFRelease(fontRef);
} NSString *content = dict[@"content"];
return [[NSAttributedString alloc] initWithString:content attributes:attributes];
} //方法四:提供将NSString转换为UIColor的功能
+(UIColor *)colorFromTemplate:(NSString *)name{ if ([name isEqualToString:@"blue"]) {
return [UIColor blueColor];
}else if ([name isEqualToString:@"red"]){
return [UIColor redColor];
}else if ([name isEqualToString:@"black"]){
return [UIColor blackColor];
}else{
return nil;
}
} //方法五:接受一个NSAttributedString和一个Config参数,将NSAttributedString转换成CoreTextData返回
+(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config{ //创建CTFrameStterRef实例
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content); //获得要绘制的区域的高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(, ), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height; //生成CTFrameRef实例
CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight]; //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
CoreTextData *data = [[CoreTextData alloc] init];
data.ctFrame = frame;
data.height = textHeight; //释放内存
CFRelease(framesetter);
CFRelease(frame); return data;
} //方法六:方法五的一个辅助函数,供方法五调用
+(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{ CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(, , config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(, ), path, NULL);
CFRelease(path);
return frame;
} #pragma mark - 添加设置CTRunDelegate信息的方法
static CGFloat ascentCallback(void *ref){ return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
}
static CGFloat descentCallback(void *ref){ return ;
}
static CGFloat widthCallback(void *ref){ return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
}
+(NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config{ CTRunDelegateCallbacks callbacks;
memset(&callbacks, , sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict); //使用0xFFFC作为空白占位符
unichar objectReplacementChar = 0xFFFC;
NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:];
NSDictionary *attributes = [self attributesWithConfig:config];
NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(, ), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
return space;
} @end

改造CoreTextData类:

//  CoreTextData.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h> @interface CoreTextData : NSObject @property (assign,nonatomic)CTFrameRef ctFrame;
@property (assign,nonatomic)CGFloat height; //新增加的成员
@property (strong,nonatomic)NSArray *imageArray; @end
//  CoreTextData.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CoreTextData.h"
#import "CoreTextImageData.h" @implementation CoreTextData //CoreFoundation不支持ARC,需要手动去管理内存的释放
-(void)setCtFrame:(CTFrameRef)ctFrame{
if (_ctFrame != ctFrame) {
if (_ctFrame !=nil) {
CFRelease(_ctFrame);
}
}
CFRetain(ctFrame);
_ctFrame = ctFrame;
} -(void)dealloc{
if (_ctFrame != nil) {
CFRelease(_ctFrame);
_ctFrame = nil;
}
} -(void)setImageArray:(NSArray *)imageArray{
_imageArray = imageArray;
[self fillImagePosition]; }
//填充图片
-(void)fillImagePosition{
if (self.imageArray.count==) {
return;
}
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
NSInteger lineCount = [lines count];
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(, ), lineOrigins); int imgIndex = ;
CoreTextImageData *imageData = self.imageArray[];
for (int i=; i<lineCount; i++) {
if (imageData==nil) {
break;
}
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
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(, ), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent; CGFloat x0ffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
runBounds.origin.x = lineOrigins[i].x + x0ffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent; CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y); imageData.imagePostion = delegateBounds;
imgIndex ++;
if (imgIndex == self.imageArray.count) {
imageData = nil;
break;
}else{
imageData = self.imageArray[imgIndex];
}
}
}
} @end

改造CTDisplayView类:

//  CTDispalyView.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <UIKit/UIKit.h>
#import "CoreTextData.h" @interface CTDispalyView : UIView
@property(strong,nonatomic)CoreTextData *data;
@end
//  CTDispalyView.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTDispalyView.h"
#import "CoreTextImageData.h" //导入CoreText系统框架
#import <CoreText/CoreText.h> @implementation CTDispalyView //重写drawRect方法
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; //1.获取当前绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext(); //2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, , self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0); if (self.data) { CTFrameDraw(self.data.ctFrame, context);
for (CoreTextImageData *imageData in self.data.imageArray) { UIImage *image = [UIImage imageNamed:imageData.name];
CGContextDrawImage(context, imageData.imagePostion, image.CGImage);
}
}
} @end

示例5:带图片的排版引擎,显示文本内容和图片,通过排版文件格式更改文字的一些简单的属性信息

//  ViewController.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "ViewController.h"
#import "CTDispalyView.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h"
#import "CTFrameParser.h" @interface ViewController () @end @implementation ViewController - (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor]; //创建画布
CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:self.view.bounds];
dispaleView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:dispaleView]; //设置配置信息
CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
config.width = dispaleView.width; //获取模板文件
NSString *path = [[NSBundle mainBundle] pathForResource:@"JsonTemplate" ofType:@"json"]; //创建绘制数据实例
CoreTextData *data = [CTFrameParser parseTemplateFile:path config:config];
dispaleView.data = data;
dispaleView.height = data.height;
dispaleView.backgroundColor = [UIColor yellowColor];
} @end

测试效果图如下:

iOS:基于CoreText的排版引擎

六、添加对图片的点击支持

实现方式

为了实现对图片的点击支持,我们需要给CTDisplayView类增加用户点击操作的检测函数,在检测函数中,判断当前用户点击的局域是否在图片上,如果在图片上,则触发点击图片的逻辑。拼过提供的UITapGestureRecognizer可以很好地满足我们的要求,所以我们这里用它来检测用户的点击操作。

这里我们实现的是点击图片后,显示图片。实际开发中,可以根据业务需求去调整点击后的效果。

CTDisplayView类实现如下,增加点击手势:

//  CTDispalyView.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTDispalyView.h"
#import "CoreTextImageData.h" //导入CoreText系统框架
#import <CoreText/CoreText.h> @interface CTDispalyView ()<UIGestureRecognizerDelegate>
@property (strong,nonatomic)UIImageView *tapImgeView;
@property (strong,nonatomic)UIView *coverView;
@end @implementation CTDispalyView //初始化方法
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
[self setupEvents];
}
return self;
} //添加点击手势
-(void)setupEvents{ UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(userTapGestureDetected:)];
tapRecognizer.delegate = self;
[self addGestureRecognizer:tapRecognizer];
self.userInteractionEnabled = YES;
} //增加UITapGestureRecognizer的回调函数
-(void)userTapGestureDetected:(UITapGestureRecognizer *)recognizer{ CGPoint point = [recognizer locationInView:self];
for (CoreTextImageData *imagData in self.data.imageArray) { //翻转坐标系,因为ImageData中的坐标是CoreText的坐标系
CGRect imageRect = imagData.imagePostion;
CGPoint imagePosition = imageRect.origin;
imagePosition.y = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
CGRect rect = CGRectMake(imagePosition.x, imagePosition.y, imageRect.size.width, imageRect.size.height); //检测点击位置Point是否在rect之内
if (CGRectContainsPoint(rect, point)) { //在这里处理点击后的逻辑
[self showTapImage:[UIImage imageNamed:imagData.name]];
break;
}
}
} //显示图片
-(void)showTapImage:(UIImage *)tapImage{ UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; //图片
_tapImgeView = [[UIImageView alloc] initWithImage:tapImage];
_tapImgeView.frame = CGRectMake(, , , );
_tapImgeView.center = keyWindow.center; //蒙版
_coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
[_coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]];
_coverView.backgroundColor = [UIColor colorWithRed:/255.0 green:/255.0 blue:/255.0 alpha:0.6];
_coverView.userInteractionEnabled = YES; [keyWindow addSubview:_coverView];
[keyWindow addSubview:_tapImgeView];
} -(void)cancel{
[_tapImgeView removeFromSuperview];
[_coverView removeFromSuperview];
} //重写drawRect方法
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; //1.获取当前绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext(); //2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, , self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0); if (self.data) { CTFrameDraw(self.data.ctFrame, context);
for (CoreTextImageData *imageData in self.data.imageArray) { UIImage *image = [UIImage imageNamed:imageData.name];
CGContextDrawImage(context, imageData.imagePostion, image.CGImage);
}
}
} @end

点击图片演示截图:

iOS:基于CoreText的排版引擎 iOS:基于CoreText的排版引擎

七、添加对链接的点击支持

实现方式:需要修改模板文件,增加一个名为”link”的类型,用于表示链接内容。格式如下:

iOS:基于CoreText的排版引擎

首先增加一个CoreTextLinkData类,用于记录解析JSON文件时的链接信息:

CoreTextLinkData

//  CoreTextLinkData.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/26.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h> @interface CoreTextLinkData : NSObject @property (copy, nonatomic)NSString *title;
@property (copy, nonatomic)NSString *url;
@property (assign, nonatomic)NSRange range; @end
//  CoreTextLinkData.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/26.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CoreTextLinkData.h" @implementation CoreTextLinkData @end

接着增加一个工具类CoreTextUtils类,用于检测链接是否被点击:

CoreTextUtils:

//  CoreTextUtils.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/26.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h>
#import "CoreTextLinkData.h"
#import "CoreTextData.h" @interface CoreTextUtils : NSObject /**
* 检测点击位置是否在链接上
*
* @param view 点击区域
* @param point 点击坐标
* @param data 数据源
*/
+(CoreTextLinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CoreTextData *)data; @end
//  CoreTextUtils.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/26.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CoreTextUtils.h" @implementation CoreTextUtils //检测点击位置是否在链接上
+(CoreTextLinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CoreTextData *)data{ CTFrameRef textFrame = data.ctFrame;
CFArrayRef lines = CTFrameGetLines(textFrame);
if (!lines) return nil;
CFIndex count = CFArrayGetCount(lines);
CoreTextLinkData *foundLink = nil; //获得每一行的origin坐标
CGPoint origins[count];
CTFrameGetLineOrigins(textFrame, CFRangeMake(, ), origins); //翻转坐标系
CGAffineTransform tranform = CGAffineTransformMakeTranslation(, view.bounds.size.height);
tranform = CGAffineTransformScale(tranform, .f, -.f);
for (int i=; i<count; i++) {
CGPoint linePoint = origins[i];
CTLineRef line = CFArrayGetValueAtIndex(lines, i); //获取每一行的CGRect信息
CGRect flippedRect = [self getLineBounds:line point:linePoint];
CGRect rect = CGRectApplyAffineTransform(flippedRect, tranform); if (CGRectContainsPoint(rect, point)) {
//将点击的坐标转换成相对于当前行的坐标
CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect), point.y-CGRectGetMinY(rect)); //获得当前点击坐标对应的字符串偏移
CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint); //判断这个偏移是否在我们的链接列表中
foundLink = [self linkAtIndex:idx linkArray:data.linkArray]; return foundLink;
}
}
return nil;
} //获取每一行的CGRect信息
+(CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point{
CGFloat ascent = 0.0f;
CGFloat descent = 0.0f;
CGFloat leading = 0.0f;
CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;
return CGRectMake(point.x, point.y, width, height);
} //判断这个偏移是否在我们的链接列表中
+(CoreTextLinkData *)linkAtIndex:(CFIndex)i linkArray:(NSArray *)linkArray{ CoreTextLinkData *link = nil;
for (CoreTextLinkData *data in linkArray) {
if (NSLocationInRange(i, data.range)) {
link = data;
break;
}
}
return link;
} @end

然后依次改造CTFrameParser类,CoreTextData类,CTDisplayView类

CTFrameParser:

//  CTFrameParser.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h>
#import "CoreTextData.h" @class CTFrameParserConfig;
@interface CTFrameParser : NSObject /**
* 给内容设置配置信息
*
* @param content 内容
* @param config 配置信息
*
*/
+(CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config; /**
* 配置信息格式化
*
* @param config 配置信息
*/
+(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config; //=======================================================================================================// /**
* 给内容设置配置信息
*
* @param content 内容
* @param config 配置信息
*/
+(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config; /**
* 给内容设置配置信息
*
* @param path 模板文件路径
* @param config 配置信息
*/
+(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config; @end
//  CTFrameParser.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTFrameParser.h"
#import "CTFrameParserConfig.h"
#import "CoreTextData.h"
#import "CoreTextImageData.h"
#import "CoreTextLinkData.h" @implementation CTFrameParser //给内容设置配置信息
+(CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config{ NSDictionary *attributes = [self attributesWithConfig:config];
NSAttributedString *contextString = [[NSAttributedString alloc] initWithString:content attributes:attributes]; //创建CTFrameStterRef实例
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)contextString); //获得要绘制的区域的高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(, ), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height; //生成CTFrameRef实例
CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight]; //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
CoreTextData *data = [[CoreTextData alloc] init];
data.ctFrame = frame;
data.height = textHeight; //释放内存
CFRelease(framesetter);
CFRelease(frame); return data;
} //配置信息格式化
+(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config{ CGFloat fontSize = config.fontSize;
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
CGFloat lineSpcing = config.lineSpace;
const CFIndex kNumberOfSettings = ;
CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
{kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpcing},
{kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpcing},
{kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpcing},
}; CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
UIColor *textColor = config.textColor; NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef; CFRelease(fontRef);
CFRelease(theParagraphRef);
return dict;
} #pragma mark - 新增的方法 //方法一:用于提供对外的接口,调用方法二实现从一个JSON的模板文件中读取内容,然后调用方法五生成的CoreTextData
+(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config{ NSMutableArray *imageArray = [NSMutableArray array];
NSMutableArray *linkArray = [NSMutableArray array];
NSAttributedString *content = [self loadTemplateFile:path config:config imageArray:imageArray linkArray:linkArray];
CoreTextData *data = [self parseAttributedContent:content config:config];
data.imageArray = imageArray;
data.linkArray = linkArray;
return data;
} //方法二:读取JSON文件内容,并且调用方法三获得从NSDcitionay到NSAttributedString的转换结果
+(NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config
imageArray:(NSMutableArray *)imageArray
linkArray:(NSMutableArray *)linkArray{
NSData *data = [NSData dataWithContentsOfFile:path];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
if (data) { NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; if ([array isKindOfClass:[NSArray class]]) {
for (NSDictionary *dict in array) { NSString *type = dict[@"type"]; if ([type isEqualToString:@"txt"]) { NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
[result appendAttributedString:as]; }else if ([type isEqualToString:@"img"]){ //创建CoreTextImageData,保存图片到imageArray数组中
CoreTextImageData *imageData = [[CoreTextImageData alloc] init];
imageData.name = dict[@"name"];
imageData.position = [result length];
[imageArray addObject:imageData]; //创建空白占位符,并且设置它的CTRunDelegate信息
NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
[result appendAttributedString:as];
}
else if ([type isEqualToString:@"link"]){ NSUInteger startPos = result.length;
NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
[result appendAttributedString:as]; //创建CoreTextLinkData
NSUInteger length = result.length - startPos;
NSRange linkRange = NSMakeRange(startPos, length);
CoreTextLinkData *linkData = [[CoreTextLinkData alloc] init];
linkData.title = dict[@"content"];
linkData.url = dict[@"url"];
linkData.range = linkRange;
[linkArray addObject:linkData];
}
}
}
}
return result;
} //方法三:将NSDcitionay内容转换为NSAttributedString
+(NSAttributedString *)parseAttributeContentFromNSDictionary:(NSDictionary*)dict config:(CTFrameParserConfig *)config{ NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]]; //设置颜色
UIColor *color = [self colorFromTemplate:dict[@"color"]];
if (color) {
attributes[(id)kCTForegroundColorAttributeName] = (id)color.CGColor;
} //设置字号
CGFloat fontSize = [dict[@"size"] floatValue];
if (fontSize>) {
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
attributes[(id)kCTFontAttributeName] = (__bridge id)fontRef;
CFRelease(fontRef);
} NSString *content = dict[@"content"];
return [[NSAttributedString alloc] initWithString:content attributes:attributes];
} //方法四:提供将NSString转换为UIColor的功能
+(UIColor *)colorFromTemplate:(NSString *)name{ if ([name isEqualToString:@"blue"]) {
return [UIColor blueColor];
}else if ([name isEqualToString:@"red"]){
return [UIColor redColor];
}else if ([name isEqualToString:@"black"]){
return [UIColor blackColor];
}else{
return nil;
}
} //方法五:接受一个NSAttributedString和一个Config参数,将NSAttributedString转换成CoreTextData返回
+(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config{ //创建CTFrameStterRef实例
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content); //获得要绘制的区域的高度
CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(, ), nil, restrictSize, nil);
CGFloat textHeight = coreTextSize.height; //生成CTFrameRef实例
CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight]; //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
CoreTextData *data = [[CoreTextData alloc] init];
data.ctFrame = frame;
data.height = textHeight; //释放内存
CFRelease(framesetter);
CFRelease(frame); return data;
} //方法六:方法五的一个辅助函数,供方法五调用
+(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{ CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(, , config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(, ), path, NULL);
CFRelease(path);
return frame;
} #pragma mark - 添加设置CTRunDelegate信息的方法
static CGFloat ascentCallback(void *ref){ return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height"] floatValue];
}
static CGFloat descentCallback(void *ref){ return ;
}
static CGFloat widthCallback(void *ref){ return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue];
}
+(NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config{ CTRunDelegateCallbacks callbacks;
memset(&callbacks, , sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict); //使用0xFFFC作为空白占位符
unichar objectReplacementChar = 0xFFFC;
NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:];
NSDictionary *attributes = [self attributesWithConfig:config];
NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(, ), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
return space;
} @end

CoreTextData:

//
// CoreTextData.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <Foundation/Foundation.h> @interface CoreTextData : NSObject @property (assign,nonatomic)CTFrameRef ctFrame;
@property (assign,nonatomic)CGFloat height; //新增加的成员
@property (strong,nonatomic)NSArray *imageArray;
@property (strong,nonatomic)NSArray *linkArray; @end
//
// CoreTextData.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CoreTextData.h"
#import "CoreTextImageData.h" @implementation CoreTextData //CoreFoundation不支持ARC,需要手动去管理内存的释放
-(void)setCtFrame:(CTFrameRef)ctFrame{
if (_ctFrame != ctFrame) {
if (_ctFrame !=nil) {
CFRelease(_ctFrame);
}
}
CFRetain(ctFrame);
_ctFrame = ctFrame;
} -(void)dealloc{
if (_ctFrame != nil) {
CFRelease(_ctFrame);
_ctFrame = nil;
}
} -(void)setImageArray:(NSArray *)imageArray{
_imageArray = imageArray;
[self fillImagePosition]; }
//填充图片
-(void)fillImagePosition{
if (self.imageArray.count==) {
return;
}
NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
NSInteger lineCount = [lines count];
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(, ), lineOrigins); int imgIndex = ;
CoreTextImageData *imageData = self.imageArray[];
for (int i=; i<lineCount; i++) {
if (imageData==nil) {
break;
}
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
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(, ), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent; CGFloat x0ffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
runBounds.origin.x = lineOrigins[i].x + x0ffset;
runBounds.origin.y = lineOrigins[i].y;
runBounds.origin.y -= descent; CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
CGRect colRect = CGPathGetBoundingBox(pathRef);
CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y); imageData.imagePostion = delegateBounds;
imgIndex ++;
if (imgIndex == self.imageArray.count) {
imageData = nil;
break;
}else{
imageData = self.imageArray[imgIndex];
}
}
}
} @end

CTDisplayView

//
// CTDispalyView.h
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import <UIKit/UIKit.h>
#import "CoreTextData.h" @interface CTDispalyView : UIView
@property(strong,nonatomic)CoreTextData *data;
@end
//
// CTDispalyView.m
// CoreTextDemo
//
// Created by 夏远全 on 16/12/25.
// Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
// #import "CTDispalyView.h"
#import "CoreTextImageData.h"
#import "CoreTextLinkData.h"
#import "CoreTextUtils.h" //导入CoreText系统框架
#import <CoreText/CoreText.h> @interface CTDispalyView ()<UIGestureRecognizerDelegate>
@property (strong,nonatomic)UIImageView *tapImgeView;
@property (strong,nonatomic)UIView *coverView;
@property (strong,nonatomic)UIWebView *webView;
@end @implementation CTDispalyView //初始化方法
-(instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
[self setupEvents];
}
return self;
} //添加点击手势
-(void)setupEvents{ UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(userTapGestureDetected:)];
tapRecognizer.delegate = self;
[self addGestureRecognizer:tapRecognizer];
self.userInteractionEnabled = YES;
} //增加UITapGestureRecognizer的回调函数
-(void)userTapGestureDetected:(UITapGestureRecognizer *)recognizer{ CGPoint point = [recognizer locationInView:self]; //点击图片
for (CoreTextImageData *imagData in self.data.imageArray) { //翻转坐标系,因为ImageData中的坐标是CoreText的坐标系
CGRect imageRect = imagData.imagePostion;
CGPoint imagePosition = imageRect.origin;
imagePosition.y = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
CGRect rect = CGRectMake(imagePosition.x, imagePosition.y, imageRect.size.width, imageRect.size.height); //检测点击图片的位置Point是否在rect之内
if (CGRectContainsPoint(rect, point)) { //在这里处理点击后的逻辑
[self showTapImage:[UIImage imageNamed:imagData.name]];
break;
}
} //点击链接
CoreTextLinkData *linkData = [CoreTextUtils touchLinkInView:self atPoint:point data:self.data];
if (linkData) {
[self showTapLink:linkData.url];
return;
}
} //显示图片
-(void)showTapImage:(UIImage *)tapImage{ UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; //图片
_tapImgeView = [[UIImageView alloc] initWithImage:tapImage];
_tapImgeView.frame = CGRectMake(, , , );
_tapImgeView.center = keyWindow.center; //蒙版
_coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
[_coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]];
_coverView.backgroundColor = [UIColor colorWithRed:/255.0 green:/255.0 blue:/255.0 alpha:0.6];
_coverView.userInteractionEnabled = YES; [keyWindow addSubview:_coverView];
[keyWindow addSubview:_tapImgeView];
} -(void)cancel{
[_tapImgeView removeFromSuperview];
[_coverView removeFromSuperview];
} //显示链接网页
-(void)showTapLink:(NSString *)urlStr{ UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; //网页
_webView = [[UIWebView alloc] initWithFrame:CGRectMake(, , , )];
_webView.center = keyWindow.center;
[_webView setScalesPageToFit:YES];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
[_webView loadRequest:request]; //蒙版
_coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
[_coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)]];
_coverView.backgroundColor = [UIColor colorWithRed:/255.0 green:/255.0 blue:/255.0 alpha:0.6];
_coverView.userInteractionEnabled = YES; [keyWindow addSubview:_coverView];
[keyWindow addSubview:_webView];
}
-(void)hide{
[_webView removeFromSuperview];
[_coverView removeFromSuperview];
} //重写drawRect方法
- (void)drawRect:(CGRect)rect { [super drawRect:rect]; //1.获取当前绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext(); //2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, , self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0); if (self.data) { CTFrameDraw(self.data.ctFrame, context);
for (CoreTextImageData *imageData in self.data.imageArray) { UIImage *image = [UIImage imageNamed:imageData.name];
CGContextDrawImage(context, imageData.imagePostion, image.CGImage);
}
}
} @end

测试截图:

iOS:基于CoreText的排版引擎

源码链接https://github.com/xiayuanquan/CoreTextKit.git

本博文摘自唐巧《iOS开发进阶》,本人花了点时间学习并做了一下整理和改动,希望对学习这方面知识的人有帮助。