iOS:基于CoreText的排版引擎
一、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来实现的。
UIWebview也是处理复杂的文字排版的备选方案。对于排版,基于CoreText和基于UIWebView相比,具有以下不同点:
- CoreText占用内存更少,渲染速度更快,UIWebView占用内存多,渲染速度慢。
- CoreText在渲染界面前就可以精确地获得显示内容的高度(只要有了CTFrame即可),而UIWebView只有渲染出内容后,才能获得内容的高度(而且还需要通过JavaScript代码来获取)。
- CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
- 基于CoreText可以做更好的原生交互效果,交互效果可以更细腻。而UIWebView的交互效果都是利用JavaScript来实现的,在交互效果上会有一些卡顿情况存在。例如,在UIWebView下,一个简单的按钮按下操作,都无法做出原生按钮的即时和细腻的按下效果。
当然,基于CoreText的排版方案也有那么一些劣势:
- CoreText渲染出来的内容不能像UIWebView那样方便的支付内容的复制。
- 基于CoreText来排版需要自己处理很多复杂逻辑,例如需要自己处理图片和文字混排相关的逻辑,也需要自己实现链接点击操作的支持。
1、图文混排
CTFrameRef textFrame // coreText 的 frame
CTLineRef line // coreText 的 line
CTRunRef run // line 中的部分文字
2、相关方法:
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, 0, 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(0, [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(0, 0, 300, 200)]; dispaleView.center = self.view.center; dispaleView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:dispaleView]; } @end
演示结果截图
三、基于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 = 200.f; _fontSize = 16.0f; _lineSpace = 8.0f; _textColor = RGB(108, 108, 108); } 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(0, 0), 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 = 3; 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(0, 0, config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), 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, 0, 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(0, 0, 300, 200)]; dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-100); 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
演示结果截图
好了,效果确实是实现了,现在来看看本框架的UML示意图,这4个类的关系是这样的:
1、CTFrameParser通过CTFrameParserConfig实例来生成CoreTextData实例;
2、CTDisplayView通过持有CoreTextData实例来获取绘制所需要的所有信息;
3、ViewController类通过配置CTFrameParserConfig实例,进而获得生成的CoreTextData实例,最后将其赋值给CTDisplayView成员,达到将指定内容显示在界面的效果。
四、定制排版文件格式
对于上面的例子,我们给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(0, 0), 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 = 3; 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(0, 0, config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), 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(0, 0, 300, 200)]; dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-100); 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:26] range:NSMakeRange(0, 15)]; [attributeString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 15)]; //创建绘制数据实例 CoreTextData *data = [CTFrameParser parseAttributedContent:attributeString config:config]; dispaleView.data = data; dispaleView.height = data.height; dispaleView.backgroundColor = [UIColor yellowColor]; } @end
演示结果截图
更进一步,实际工作中,我们更希望通过一个排版文件,来设置需要排版的文字的内容、颜色、字体大小等信息。我们规定排版的模板文件为JSON格式。排版格式示例文件如下:
[ { "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>0) { 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(0, 0), 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(0, 0, config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), 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(0, 0, 300, 200)]; dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-100); 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
演示结果截图
可以看到,通过一个简单的模板文件,我们可以很方便地定义排版的配置信息了。
五、支持图文混排的排版引擎
在上面的示例中,我们在设置模板文件的时候,就专门在模板文件里面预留了一个名为type的字段,用于表示内容的类型。之前的type的值都是txt,这次,我们增加一个img的值,用于表示图片。同时给img类型的内容还需要配置3个属性如下:
1、width:用于设置图片显示的宽度
2、height:用于设置图片显示的高度
3、name:用于设置图片的资源名
也即文件格式如下:
在改造代码之前,先来了解一下CTFrame内部的CTLine和CTRun。
在CTFrame内部,是有多个CTLine类组成的,每一个CTLine代表一行,每个CTLine又是由多个CTRun来组成,每一个CTRun代表一组显示风格一致的文本。我们不用手工管理CTLine和CTRun的创建过程。
CTLine和CTRun示意图如下:
示意图解释:
可以看到,第一行的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 = 3; 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>0) { 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(0, 0), 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(0, 0, config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), 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 0; } static CGFloat widthCallback(void *ref){ return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue]; } +(NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config{ CTRunDelegateCallbacks callbacks; memset(&callbacks, 0, 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:1]; NSDictionary *attributes = [self attributesWithConfig:config]; NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), 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==0) { return; } NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame); NSInteger lineCount = [lines count]; CGPoint lineOrigins[lineCount]; CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins); int imgIndex = 0; CoreTextImageData *imageData = self.imageArray[0]; for (int i=0; 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(0, 0), &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, 0, 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
测试效果图如下:
六、添加对图片的点击支持
实现方式
为了实现对图片的点击支持,我们需要给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(0, 0, 300, 200); _tapImgeView.center = keyWindow.center; //蒙版 _coverView = [[UIView alloc] initWithFrame:keyWindow.bounds]; [_coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]]; _coverView.backgroundColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/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, 0, 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
点击图片演示截图:
七、添加对链接的点击支持
实现方式:需要修改模板文件,增加一个名为”link”的类型,用于表示链接内容。格式如下:
首先增加一个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(0, 0), origins); //翻转坐标系 CGAffineTransform tranform = CGAffineTransformMakeTranslation(0, view.bounds.size.height); tranform = CGAffineTransformScale(tranform, 1.f, -1.f); for (int i=0; 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(0, 0), 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 = 3; 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>0) { 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(0, 0), 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(0, 0, config.width, height)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), 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 0; } static CGFloat widthCallback(void *ref){ return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width"] floatValue]; } +(NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config{ CTRunDelegateCallbacks callbacks; memset(&callbacks, 0, 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:1]; NSDictionary *attributes = [self attributesWithConfig:config]; NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), 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==0) { return; } NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame); NSInteger lineCount = [lines count]; CGPoint lineOrigins[lineCount]; CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins); int imgIndex = 0; CoreTextImageData *imageData = self.imageArray[0]; for (int i=0; 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(0, 0), &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(0, 0, 300, 200); _tapImgeView.center = keyWindow.center; //蒙版 _coverView = [[UIView alloc] initWithFrame:keyWindow.bounds]; [_coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]]; _coverView.backgroundColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/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(0, 0, 300, 400)]; _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:0/255.0 green:0/255.0 blue:0/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, 0, 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
测试截图:
源码链接:https://github.com/xiayuanquan/CoreTextKit.git
本博文摘自唐巧《iOS开发进阶》,本人花了点时间学习并做了一下整理和改动,希望对学习这方面知识的人有帮助。