系列文章:
今天呢,我們繼續把CoreText圖文混排的點選事件
補充上,這樣我們的圖文混排也算是圓滿了。
哦,上一篇的連結在這裡
CoreText實現圖文混排。所有需要用到的準備知識
都在上一篇,沒有趕上車的朋友可以去補個票~
上正文。
CoreText做圖文混排之點選事件
主要思路
我們知道,CoreText是基於UIView去繪製的,那麼既然有UIView,就有
-(void)touchesBegan:(NSSet)touches withEvent:(UIEvent )event方法,我們呢,就是基於這個方法去做點選事件的。
1 |
通過touchBegan方法拿到當前點選到的點,然後通過座標判斷這個點是否在某段文字上,如果在則觸發對應事件。 |
上面呢就是主要思路。接下來呢,我們來詳細講解一下。還是老規矩,先上程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch * touch = [touches anyObject]; CGPoint location = [self systemPointFromScreenPoint:[touch locationInView:self]]; if ([self checkIsClickOnImgWithPoint:location]) { return; } [self ClickOnStrWithPoint:location]; } -(BOOL)checkIsClickOnImgWithPoint:(CGPoint)location { if ([self isFrame:_imgFrm containsPoint:location]) { NSLog(@"您點選到了圖片"); return YES; } return NO; } -(void)ClickOnStrWithPoint:(CGPoint)location { NSArray * lines = (NSArray *)CTFrameGetLines(self.data.ctFrame); CFRange ranges[lines.count]; CGPoint origins[lines.count]; CTFrameGetLineOrigins(self.data.ctFrame, CFRangeMake(0, 0), origins); for (int i = 0; i = range.location)) { return YES; } return NO; } -(CGPoint)systemPointFromScreenPoint:(CGPoint)origin { return CGPointMake(origin.x, self.bounds.size.height - origin.y); } -(BOOL)isFrame:(CGRect)frame containsPoint:(CGPoint)point { return CGRectContainsPoint(frame, point); } -(CGRect)frameForCTRunWithIndex:(NSInteger)index CTLine:(CTLineRef)line origin:(CGPoint)origin { CGFloat offsetX = CTLineGetOffsetForStringIndex(line, index, NULL); CGFloat offsexX2 = CTLineGetOffsetForStringIndex(line, index + 1, NULL); offsetX += origin.x; offsexX2 += origin.x; CGFloat offsetY = origin.y; CGFloat lineAscent; CGFloat lineDescent; NSArray * runs = (__bridge NSArray *)CTLineGetGlyphRuns(line); CTRunRef runCurrent; for (int k = 0; k |
看上去也挺多的,我們還是分段講解吧。
分段解析
-touchesBegan
之所以把他放在首位,是因為他作為整個view響應點選事件的入口
扮演者十分重要的角色。
他負責接收點選事件
,根據條件將點選事件分發給不同的物件
去執行相應的響應。
1 2 3 4 5 6 7 8 9 10 |
///點選方法 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch * touch = [touches anyObject]; CGPoint location = [self systemPointFromScreenPoint:[touch locationInView:self]];//獲取點選位置的系統座標 if ([self checkIsClickOnImgWithPoint:location]) {//檢查是否點選在圖片上,如果在,優先響應圖片事件 return; } [self ClickOnStrWithPoint:location];//響應字串事件 } |
這裡老司機還是要解釋一下,為什麼我要設定成優先響應圖片的事件呢?
是這樣的,在我們使用的過程中,大部分的場景是如下過程:
- 給整段富文字新增屬性,事件等
- 插入圖片
- 給圖片設定點選事件
正是因為這樣,我們可以看出邏輯上圖片的響應事件的優先順序明顯是要高於文字的。即使是一段文字範圍我們賦值了文字的響應事件
,然後在範圍中插入了圖片並且賦予了圖片響應事件,我們往往是希望圖片響應其自己的事件
。同時,不知道你們是否還記得上一趟車我們已經求出了圖片的frame,如果優先判斷出點選的是圖片的話將會減少很多計算量
,提高執行效率
。所以我這裡將圖片的響應優先順序定義的高於文字,不過根據需要我們可以定義不同的響應優先順序。
搞明白這一點以後,其實邏輯就很簡單了。
- 首先呢,先取出當前點選的到螢幕座標的點。
- 將螢幕座標轉換為系統座標(不懂得同學快去上一節補課)
- 判斷是否點選在圖片上
- 如果未點選圖片執行點選文字
獲取點選座標
-touchesBegan事件給我們提供了touches這麼一個集合。裡面裝滿了UITouch物件。
因為集合是無序的,所以我們通過anyObject取出其中的一個UITouch物件。
UITouch物件的locationInView是專門用來給出UITouch物件在某個View中的座標的方法,因此我們可以用這個方法來求出當前點選位置的系統座標。這段比較基礎,想畫個重點都不知道畫哪。
座標轉換
這裡用到了第一個工具方法(老司機習慣把寫好的方法分類,這些中間方法老司機習慣叫他們工具方法),-(CGPoint)systemPointFromScreenPoint:(CGPoint)origin。
簡單的說一句,因為螢幕座標與系統座標的不同,我們要將座標系統一成系統座標
,這樣才能計算,所以才有了這個座標轉換的方法。其實很簡單
1 2 3 4 5 6 7 8 |
///座標轉換 /* 將螢幕座標轉換為系統座標 */ -(CGPoint)systemPointFromScreenPoint:(CGPoint)origin { return CGPointMake(origin.x, self.bounds.size.height - origin.y); } |
上一講有座標系的圖,這裡我就不細講了。直接進入下一話題。
點選圖片判斷
第二個工具方法
-(BOOL)checkIsClickOnImgWithPoint:(CGPoint)location
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
///圖片點選檢查 /* 遍歷圖片frame的陣列與點選位置比較,如果在 範圍內則響應的陣列中取出對應響應並執行,返 回yes,否則返回no */ -(BOOL)checkIsClickOnImgWithPoint:(CGPoint)location { if ([self isFrame:_imgFrm containsPoint:location]) { NSLog(@"您點選到了圖片"); return YES; } return NO; } |
這裡呢,我們用到了第三個工具方法,順便就說了吧
-(BOOL)isFrame:(CGRect)frame containsPoint:(CGPoint)point
1 2 3 4 5 |
///點包含檢測 -(BOOL)isFrame:(CGRect)frame containsPoint:(CGPoint)point { return CGRectContainsPoint(frame, point); } |
事實上也是呼叫了系統的一個方法CGRectContainsPoint()
。這個方法兩個引數,一個是frame,一個是point。可以返回point是否在frame中。
不過還是有一點需要注意的。由於傳入的point是系統座標
(本例中),所以frame我們一定要傳入系統座標系下的frame
才能正確對應。
這裡老司機偷了個懶,直接把上一講中求得的圖片frame改成了一個例項變數,這樣在這裡的方法中我就能直接呼叫了。這只是個demo,所以我就怎麼方便怎麼來了,實際使用中,你可以把frame儲存在陣列或字典中
。你問我怎麼在陣列或字典中儲存一個frame這樣的結構體?恩,有一個系統類叫NSValue
,專門針對這種結構體。
如果-(BOOL)isFrame:(CGRect)frame containsPoint:(CGPoint)point返回YES則說明在圖片範圍內,則響應圖片的點選事件
,
並且-(BOOL)checkIsClickOnImgWithPoint:(CGPoint)location返回YES
,否則返回NO。
回到上一層,如果-(BOOL)checkIsClickOnImgWithPoint:(CGPoint)location返回YES,則說明點選的是圖片並且已經執行完響應事件
,直接return結束方法即可
。否則則繼續檢查是否點選到了文字。
點選文字判斷
終於進入重中之重了,點選文字的邏輯了,不過你也別害怕,如果你對上一講的講解有了一定的理解的話,這裡將變得簡單一些。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
///字串點選檢查 /* 實際上接受所有非圖片的點選事件,將字串的每個 字元取出與點選位置比較,若在範圍內則點選到文字 ,進而檢測對應的文字是否響應事件,若存在響應 */ -(void)ClickOnStrWithPoint:(CGPoint)location { NSArray * lines = (NSArray *)CTFrameGetLines(self.data.ctFrame);//獲取所有CTLine CFRange ranges[lines.count];//初始化範圍陣列 CGPoint origins[lines.count];//初始化原點陣列 CTFrameGetLineOrigins(self.data.ctFrame, CFRangeMake(0, 0), origins);//獲取所有CTLine的原點 for (int i = 0; i |
看上去很多是吧?有沒有怕怕的。
仔細看你會發現,有很多程式碼跟昨天的有相似之處,就是這樣,因為這裡也遍歷
了每一個CTRun,只不過更加細化到CTRun中的每個字
。
1 2 3 4 |
NSArray * lines = (NSArray *)CTFrameGetLines(self.data.ctFrame);//獲取所有CTLine CFRange ranges[lines.count];//初始化範圍陣列 CGPoint origins[lines.count];//初始化原點陣列 CTFrameGetLineOrigins(self.data.ctFrame, CFRangeMake(0, 0), origins);//獲取所有CTLine的原點 |
這四句我就不多說了,獲取所有CTLine和其原點。
1 |
for (int i = 0; i |
獲取每個CTLine中包含的富文字在整串富文字中的範圍
。將所有CTLine中字串的範圍儲存下來放入陣列備用。
1 |
for (int i = 0; i |
這個for迴圈用來遍歷富文字中的每一個字元
。下面的程式碼都是在for迴圈中的迴圈體。
1 |
for (int j = 0; j |
這裡又是一層迴圈,通過當前字元序號i
與每個CTLine包含字元的範圍
比較來求得當前計算的是哪個CTLine中的字元
。
1 2 3 |
CTLineRef line = (__bridge CTLineRef)lines[lineNum];//取到字元對應的CTLine CGPoint origin = origins[lineNum]; CGRect CTRunFrame = [self frameForCTRunWithIndex:i CTLine:line origin:origin];//計算對應字元的frame |
取得當前字元所在的CTLine並取得該CTLine的原點,同時通過這裡的第五個工具方法
-(CGRect)frameForCTRunWithIndex:(NSInteger)index
CTLine:(CTLineRef)line
origin:(CGPoint)origin
計算當前字元的frame。
分解講一下這個方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
///字元frame計算 /* 返回索引字元的frame index:索引 line:索引字元所在CTLine origin:line的起點 */ -(CGRect)frameForCTRunWithIndex:(NSInteger)index CTLine:(CTLineRef)line origin:(CGPoint)origin { CGFloat offsetX = CTLineGetOffsetForStringIndex(line, index, NULL);//獲取字元起點相對於CTLine的原點的偏移量 CGFloat offsexX2 = CTLineGetOffsetForStringIndex(line, index + 1, NULL);//獲取下一個字元的偏移量,兩者之間即為字元X範圍 offsetX += origin.x; offsexX2 += origin.x;//座標轉換,將點的CTLine座標轉換至系統座標 CGFloat offsetY = origin.y;//取到CTLine的起點Y CGFloat lineAscent;//初始化上下邊距的變數 CGFloat lineDescent; NSArray * runs = (__bridge NSArray *)CTLineGetGlyphRuns(line);//獲取所有CTRun CTRunRef runCurrent; for (int k = 0; k |
根據註釋就能很輕易的看懂這段程式碼,不過可能有幾個方法不熟悉,我來介紹下。
- CTLineGetOffsetForStringIndex(,,)
獲取一行文字中,指定charIndex字元相對x原點的偏移量
,返回值與第三個引數同為一個值。如果charIndex超出一行的字元長度則反回最大長度結束位置的偏移量
,如一行文字共有17個字元,哪麼返回的是第18個字元的起始偏移,即第17個偏移+第17個字元佔有的寬度=第18個起始位置的偏移。因此想求一行字元所佔的畫素長度時,就可以使用此函式,將charIndex設定為大於字元長度即可。
因為求得的座標是相對於CTLine原點的偏移量,因此我們要加上CTLine原點的x座標獲得該點的絕對座標
。
- CTLineGetGlyphRuns()昨天有介紹過,拿到CTLine中的所有CTRun。
- CTRunGetStringRange()獲得CTRun在富文字中的範圍
- CTRunGetTypographicBounds(,,,,)獲得對應CTRun的尺寸資訊
中間用了第六個工具方法
-(BOOL)isIndex:(NSInteger)index inRange:(NSRange)range
1 2 3 4 5 6 7 8 9 10 11 |
///範圍檢測 /* 範圍內返回yes,否則返回no */ -(BOOL)isIndex:(NSInteger)index inRange:(NSRange)range { if ((index = range.location)) { return YES; } return NO; } |
這個程式碼很簡單我就不多說了。
通過以上方法,你就拿到了每一個字元的frame
了。
可以返回至上一層了=。=喘了一口氣。。。
接受到字元的frame,還是判斷點選位置是否在frame中
,如果在,則響應點選事件並結束方法。如果沒有不在任何一個字元的frame內,則說明沒有點選到文字,執行相應的點選事件。
大工告成,到了這裡,CoreText做圖文混排的點選事件也算是完成了。
最後放一張效果圖吧。
吶,了卻一樁心事。。。
你要是喜歡呢,麻煩你動一動你可愛的小手點選一下喜歡或者關注,畢竟老司機這麼愛慕虛榮的人,而且老司機會經常更新的。
哦,這段程式碼是我自己的解決方案,所以要轉載的同學,一定要註明出處哦,這次是一定哦。貌似你不註明我也攔不住你。。。嘖嘖嘖。。。
http://www.jianshu.com/p/51c47329203e
參考資料:
無
2016年05月16日23點52分
老司機Wicky