一、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開發進階》,本人花了點時間學習並做了一下整理和改動,希望對學習這方面知識的人有幫助。