
本章前言
在上一篇《基于 CoreText 的排版引擎:基礎(chǔ)》中,我們學(xué)會(huì)了排版的基礎(chǔ)知識(shí),現(xiàn)在我們來增加復(fù)雜性,讓我們的排版引擎支持圖片和鏈接的點(diǎn)擊。
支持圖文混排的排版引擎
改造模版文件
下面我們來進(jìn)一步改造,讓排版引擎支持對(duì)于圖片的排版。在上一小節(jié)中,我們?cè)谠O(shè)置模版文件的時(shí)候,就專門在模板文件里面留了一個(gè)名為type的字段,用于表示內(nèi)容的類型。之前的type的值都是txt,這次,我們?cè)黾右粋€(gè)值為img的值,用于表示圖片。
我們將上一節(jié)的content.json文件修改為如下內(nèi)容,增加了 2 個(gè)type值為img的配置項(xiàng)。由于是圖片的配置項(xiàng),所以我們不需要設(shè)置顏色,字號(hào)這些圖片不具有的屬性,但是,我們另外增加了 3 個(gè)圖片的配置屬性:
一個(gè)名為width的屬性,用于設(shè)置圖片顯示的寬度。
一個(gè)名為height的屬性,用于設(shè)置圖片顯示的高度。
一個(gè)名為name的屬性,用于設(shè)置圖片的資源名。
1234567891011121314151617181920212223242526272829303132
[ {
? ? "type" : "img",
? ? "width" : 200,
? ? "height" : 108,
? ? "name" : "coretext-image-1.jpg"
? },
? { "color" : "blue",
? ? "content" : " 更進(jìn)一步地,實(shí)際工作中,我們更希望通過一個(gè)排版文件,來設(shè)置需要排版的文字的 ",
? ? "size" : 16,
? ? "type" : "txt"
? },
? { "color" : "red",
? ? "content" : " 內(nèi)容、顏色、字體 ",
? ? "size" : 22,
? ? "type" : "txt"
? },
? { "color" : "black",
? ? "content" : " 大小等信息。\n",
? ? "size" : 16,
? ? "type" : "txt"
? },
? {
? ? "type" : "img",
? ? "width" : 200,
? ? "height" : 130,
? ? "name" : "coretext-image-2.jpg"
? },
? { "color" : "default",
? ? "content" : " 我在開發(fā)猿題庫(kù)應(yīng)用時(shí),自己定義了一個(gè)基于 UBB 的排版模版,但是實(shí)現(xiàn)該排版文件的解析器要花費(fèi)大量的篇幅,考慮到這并不是本章的重點(diǎn),所以我們以一個(gè)較簡(jiǎn)單的排版文件來講解其思想。",
? ? "type" : "txt"
? }
]
按理說,圖片本身的內(nèi)容信息中,是包含寬度和高度信息的,為什么我們要在這里指定圖片的寬高呢?這主要是因?yàn)椋谡鎸?shí)的開發(fā)中,應(yīng)用的模版和圖片通常是通過服務(wù)器獲取的,模版是純文本的內(nèi)容,獲取速度比圖片快很多,而圖片不但獲取速度慢,而且為了省流量,通常的做法是直到需要顯示圖片的時(shí)候,再加載圖片內(nèi)容。
如果我們不將圖片的寬度和高度信息設(shè)置在模板里面,那么 CoreText 在排版的時(shí)候就無法知道繪制所需要的高度,我們就無法設(shè)置CoreTextData類中的height信息,沒有高度信息,就會(huì)對(duì) UITableView 一類的控件排版造成影響。所以,除非你的應(yīng)用圖片能夠保證在繪制前都能全部在本地,否則就應(yīng)該另外提前提供圖片寬度和高度信息。
在完成模板文件修改后,我們選取兩張測(cè)試用的圖片,分別將其命名為coretext-image-1.jpg和coretext-image-2.jpg(和模板中的值一致),將其拖動(dòng)增加到工程中。向 Xcode 工程增加圖片資源是基礎(chǔ)知識(shí),在此就不詳細(xì)介紹過程了。
CTLine 與 CTRun
接下來我們需要改造的是CTFrameParser類,讓解析模板文件的方法支持type為img的配置。
在改造前,我們先來了解一下CTFrame內(nèi)部的組成。通過之前的例子,我們可以看到,我們首先通過NSAttributeString和配置信息創(chuàng)建CTFrameSetter, 然后,再通過CTFrameSetter來創(chuàng)建CTFrame。
在CTFrame內(nèi)部,是由多個(gè)CTLine來組成的,每個(gè)CTLine代表一行,每個(gè)CTLine又是由多個(gè)CTRun來組成,每個(gè)CTRun代表一組顯示風(fēng)格一致的文本。我們不用手工管理CTLine和CTRun的創(chuàng)建過程。
下圖是一個(gè)CTLine和CTRun的示意圖,可以看到,第三行的CTLine是由 2 個(gè)CTRun構(gòu)成的,第一個(gè)CTRun為紅色大字號(hào)的左邊部分,第二個(gè)CTRun為右邊字體較小的部分。

雖然我們不用管理CTRun的創(chuàng)建過程,但是我們可以設(shè)置某一個(gè)具體的CTRun的CTRunDelegate來指定該文本在繪制時(shí)的高度、寬度、排列對(duì)齊方式等信息。
對(duì)于圖片的排版,其實(shí) CoreText 本質(zhì)上不是直接支持的,但是,我們可以在要顯示文本的地方,用一個(gè)特殊的空白字符代替,同時(shí)設(shè)置該字體的CTRunDelegate信息為要顯示的圖片的寬度和高度信息,這樣最后生成的CTFrame實(shí)例,就會(huì)在繪制時(shí)將圖片的位置預(yù)留出來。
因?yàn)槲覀兊腃TDisplayView的繪制代碼是在drawRect里面的,所以我們可以方便地把需要繪制的圖片,用CGContextDrawImage方法直接繪制出來就可以了。
改造模版解析類
在了解了以上原理后,我們就可以開始進(jìn)行改造了。
我們需要做的工作包括:
改造CTFrameParser的parseTemplateFile:(NSString *)path config:(CTFrameParserConfig*)config;方法,使其支持對(duì)type為img的節(jié)點(diǎn)解析。并且對(duì)type為img的節(jié)點(diǎn),設(shè)置其CTRunDelegate信息,使其在繪制時(shí),為圖片預(yù)留相應(yīng)的空白位置。
改造CoreTextData類,增加圖片相關(guān)的信息,并且增加計(jì)算圖片繪制區(qū)域的邏輯。
改造CTDisplayView類,增加繪制圖片相關(guān)的邏輯。
首先介紹對(duì)于CTFrameParser的改造:
我們修改了parseTemplateFile方法,增加了一個(gè)名為imageArray的參數(shù)來保存解析時(shí)的圖片信息。
1234567
+ (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;
}
接著我們修改loadTemplateFile方法,增加了對(duì)于type是img的節(jié)點(diǎn)處理邏輯,該邏輯主要做 2 件事情:
保存當(dāng)前圖片節(jié)點(diǎn)信息到imageArray變量中
新建一個(gè)空白的占位符。
1234567891011121314151617181920212223242526272829303132
+ (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 parseAttributedContentFromNSDictionary:dict
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? config:config];
? ? ? ? ? ? ? ? ? ? [result appendAttributedString:as];
? ? ? ? ? ? ? ? } else if ([type isEqualToString:@"img"]) {
? ? ? ? ? ? ? ? ? ? // 創(chuàng)建 CoreTextImageData
? ? ? ? ? ? ? ? ? ? CoreTextImageData *imageData = [[CoreTextImageData alloc] init];
? ? ? ? ? ? ? ? ? ? imageData.name = dict[@"name"];
? ? ? ? ? ? ? ? ? ? imageData.position = [result length];
? ? ? ? ? ? ? ? ? ? [imageArray addObject:imageData];
? ? ? ? ? ? ? ? ? ? // 創(chuàng)建空白占位符,并且設(shè)置它的 CTRunDelegate 信息
? ? ? ? ? ? ? ? ? ? NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
? ? ? ? ? ? ? ? ? ? [result appendAttributedString:as];
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? return result;
}
最后我們新建一個(gè)最關(guān)鍵的方法:parseImageDataFromNSDictionary,生成圖片空白的占位符,并且設(shè)置其CTRunDelegate信息。其代碼如下:
1234567891011121314151617181920212223242526272829303132333435
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;
}
接著我們對(duì)CoreTextData進(jìn)行改造,增加了imageArray成員變量,用于保存圖片繪制時(shí)所需的信息。
1234567891011
#import
#import "CoreTextImageData.h"
@interface CoreTextData : NSObject
@property (assign, nonatomic) CTFrameRef ctFrame;
@property (assign, nonatomic) CGFloat height;
// 新增加的成員
@property (strong, nonatomic) NSArray * imageArray;
@end
在設(shè)置imageArray成員時(shí),我們還會(huì)調(diào)一個(gè)新創(chuàng)建的fillImagePosition方法,用于找到每張圖片在繪制時(shí)的位置。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
- (void)setImageArray:(NSArray *)imageArray {
? ? _imageArray = imageArray;
? ? [self fillImagePosition];
}
- (void)fillImagePosition {
? ? if (self.imageArray.count == 0) {
? ? ? ? return;
? ? }
? ? NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
? ? int 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 xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
? ? ? ? ? ? runBounds.origin.x = lineOrigins[i].x + xOffset;
? ? ? ? ? ? runBounds.origin.y = lineOrigins[i].y;
? ? ? ? ? ? runBounds.origin.y -= descent;
? ? ? ? ? ? CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
? ? ? ? ? ? CGRect colRect = CGPathGetBoundingBox(pathRef);
? ? ? ? ? ? CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
? ? ? ? ? ? imageData.imagePosition = delegateBounds;
? ? ? ? ? ? imgIndex++;
? ? ? ? ? ? if (imgIndex == self.imageArray.count) {
? ? ? ? ? ? ? ? imageData = nil;
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? imageData = self.imageArray[imgIndex];
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
添加對(duì)圖片的點(diǎn)擊支持
實(shí)現(xiàn)方式
為了實(shí)現(xiàn)對(duì)圖片的點(diǎn)擊支持,我們需要給CTDisplayView類增加用戶點(diǎn)擊操作的檢測(cè)函數(shù),在檢測(cè)函數(shù)中,判斷當(dāng)前用戶點(diǎn)擊的區(qū)域是否在圖片上,如果在圖片上,則觸發(fā)點(diǎn)擊圖片的邏輯。蘋果提供的UITapGestureRecognizer可以很好的滿足我們的要求,所以我們這里用它來檢測(cè)用戶的點(diǎn)擊操作。
我們這里實(shí)現(xiàn)的是點(diǎn)擊圖片后,先用NSLog打印出一行日志。實(shí)際應(yīng)用中,讀者可以根據(jù)業(yè)務(wù)需求自行調(diào)整點(diǎn)擊后的效果。
我們先為CTDisplayView類增加UITapGestureRecognizer:
12345678910111213141516
- (id)initWithCoder:(NSCoder *)aDecoder {
? ? self = [super initWithCoder:aDecoder];
? ? if (self) {
? ? ? ? [self setupEvents];
? ? }
? ? return self;
}
- (void)setupEvents {
? ? UIGestureRecognizer * tapRecognizer =
? ? ? ? ? [[UITapGestureRecognizer alloc] initWithTarget:self
? ? ? ? ? ? ? ? ? ? action:@selector(userTapGestureDetected:)];
? ? tapRecognizer.delegate = self;
? ? [self addGestureRecognizer:tapRecognizer];
? ? self.userInteractionEnabled = YES;
}
然后增加UITapGestureRecognizer的回調(diào)函數(shù):
1234567891011121314151617
- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer {
? ? CGPoint point = [recognizer locationInView:self];
? ? for (CoreTextImageData * imageData in self.data.imageArray) {
? ? ? ? // 翻轉(zhuǎn)坐標(biāo)系,因?yàn)?imageData 中的坐標(biāo)是 CoreText 的坐標(biāo)系
? ? ? ? CGRect imageRect = imageData.imagePosition;
? ? ? ? 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);
? ? ? ? // 檢測(cè)點(diǎn)擊位置 Point 是否在 rect 之內(nèi)
? ? ? ? if (CGRectContainsPoint(rect, point)) {
? ? ? ? ? ? // 在這里處理點(diǎn)擊后的邏輯
? ? ? ? ? ? NSLog(@"bingo");
? ? ? ? ? ? break;
? ? ? ? }
? ? }
}
事件處理
在界面上,CTDisplayView通常在UIView的樹形層級(jí)結(jié)構(gòu)中,一個(gè) UIView 可能是最外層 View Controller 的 View 的孩子的孩子的孩子(如下圖所示)。在這種多級(jí)層次結(jié)構(gòu)中,很難通過delegate模式將圖片點(diǎn)擊的事件一層一層往外層傳遞,所以最好使用NSNotification,來處理圖片點(diǎn)擊事件。

在 Demo 中,我們?cè)谧钔鈱拥?View Controller 中監(jiān)聽圖片點(diǎn)擊的通知,當(dāng)收到通知后,進(jìn)入到一個(gè)新的界面來顯示圖片點(diǎn)擊內(nèi)容。
注:讀者可以將 demo 工程切換到image_click分支,查看示例代碼。
添加對(duì)鏈接的點(diǎn)擊支持
修改模板文件
我們修改模版文件,增加一個(gè)名為 link 的類型,用于表示鏈接內(nèi)容。如下所示:
123456789101112131415
[
? { "color" : "default",
? ? "content" : " 這在這里嘗試放一個(gè)參考鏈接:",
? ? "type" : "txt"
? },
? { "color" : "blue",
? ? "content" : " 鏈接文字 ",
? ? "url" : "http://blog.devtang.com",
? ? "type" : "link"
? },
? { "color" : "default",
? ? "content" : " 大家可以嘗試點(diǎn)擊一下 ",
? ? "type" : "txt"
? }
]
解析模版中的鏈接信息
我們首先增加一個(gè)CoreTextLinkData類,用于記錄解析 JSON 文件時(shí)的鏈接信息:
1234567
@interface CoreTextLinkData : NSObject
@property (strong, nonatomic) NSString * title;
@property (strong, nonatomic) NSString * url;
@property (assign, nonatomic) NSRange range;
@end
然后我們修改 CTFrameParser 類,增加解析鏈接的邏輯:
12345678910111213141516171819202122232425262728293031323334353637
+ (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"]) {
? ? ? ? ? ? ? ? ? ? // 省略
? ? ? ? ? ? ? ? } else if ([type isEqualToString:@"img"]) {
? ? ? ? ? ? ? ? ? ? // 省略
? ? ? ? ? ? ? ? } else if ([type isEqualToString:@"link"]) {
? ? ? ? ? ? ? ? ? ? NSUInteger startPos = result.length;
? ? ? ? ? ? ? ? ? ? NSAttributedString *as =
? ? ? ? ? ? ? ? ? ? ? [self parseAttributedContentFromNSDictionary:dict
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? config:config];
? ? ? ? ? ? ? ? ? ? [result appendAttributedString:as];
? ? ? ? ? ? ? ? ? ? // 創(chuàng)建 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;
}
然后,我們?cè)黾右粋€(gè) Utils 類來專門處理檢測(cè)用戶點(diǎn)擊是否在鏈接上。主要的方法是使用 CTLineGetStringIndexForPosition 函數(shù)來獲得用戶點(diǎn)擊的位置與 NSAttributedString 字符串上的位置的對(duì)應(yīng)關(guān)系。這樣就知道是點(diǎn)擊的哪個(gè)字符了。然后判斷該字符串是否在鏈接上即可。該 Util 在實(shí)現(xiàn)邏輯如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// 檢測(cè)點(diǎn)擊位置是否在鏈接上
+ (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 坐標(biāo)
? ? CGPoint origins[count];
? ? CTFrameGetLineOrigins(textFrame, CFRangeMake(0,0), origins);
? ? // 翻轉(zhuǎn)坐標(biāo)系
? ? CGAffineTransform transform =? CGAffineTransformMakeTranslation(0, view.bounds.size.height);
? ? transform = CGAffineTransformScale(transform, 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, transform);
? ? ? ? if (CGRectContainsPoint(rect, point)) {
? ? ? ? ? ? // 將點(diǎn)擊的坐標(biāo)轉(zhuǎn)換成相對(duì)于當(dāng)前行的坐標(biāo)
? ? ? ? ? ? CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? point.y-CGRectGetMinY(rect));
? ? ? ? ? ? // 獲得當(dāng)前點(diǎn)擊坐標(biāo)對(duì)應(yīng)的字符串偏移
? ? ? ? ? ? CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
? ? ? ? ? ? // 判斷這個(gè)偏移是否在我們的鏈接列表中
? ? ? ? ? ? foundLink = [self linkAtIndex:idx linkArray:data.linkArray];
? ? ? ? ? ? return foundLink;
? ? ? ? }
? ? }
? ? return nil;
}
+ (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 - descent, 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;
}
最后改造一下CTDisplayView,使其在檢測(cè)到用戶點(diǎn)擊后,調(diào)用上面的 Util 方法即可。我們這里實(shí)現(xiàn)的是點(diǎn)擊鏈接后,先用NSLog打印出一行日志。實(shí)際應(yīng)用中,讀者可以根據(jù)業(yè)務(wù)需求自行調(diào)整點(diǎn)擊后的效果。
12345678910
- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer {
? ? CGPoint point = [recognizer locationInView:self];
? ? // 此處省略上一節(jié)中介紹的,對(duì)圖片點(diǎn)擊檢測(cè)的邏輯
? ? CoreTextLinkData *linkData = [CoreTextUtils touchLinkInView:self atPoint:point data:self.data];
? ? if (linkData) {
? ? ? ? NSLog(@"hint link!");
? ? ? ? return;
? ? }
}
注:在 Demo 中工程中,我們實(shí)現(xiàn)了點(diǎn)擊鏈接跳轉(zhuǎn)到一個(gè)新的界面,然后用 UIWebView 來顯示鏈接內(nèi)容的邏輯。讀者可以將 demo 工程切換到link_click分支,查看示例代碼。
Demo 工程的 Gif 效果圖如下,讀者可以將示例工程用git checkout image_support切換到當(dāng)前章節(jié)狀態(tài),查看相關(guān)代碼邏輯。