基于 CoreText 的排版引擎:進(jìn)階


本章前言

在上一篇《基于 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)代碼邏輯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,477評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,617評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,807評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,049評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評(píng)論 1 281
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,432評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,769評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容