系列文章:
終于我來完成我CoreText圖文混排的最后一章了。
先說一下我為什么會來補發這一章呢?
1.老司機最開始沒有留demo,以至于這個博客老司機從發出來到現在整整維護了半年了=。=其實博客里面就是全部代碼,但是寶寶們任性的要demo。
2.時間長了,閱讀量也上去了,老司機覺得自己有必要對粉絲們負責了
3.有很多同學詢問是否能做出文字環繞的效果,老司機之前的確也沒有寫過,這一篇是要補上的。
4.關于點擊事件,老司機在第二篇文章中有提到過一個思路,是每次遍歷所有CTRun去做的。后期老司機考慮到遍歷的實現效率似乎有些低,所以老司機研究了一下,重新整理思路,優化了一下算法。
基于以上原因,以及一個陰謀
,老司機又來更文了。
在這篇文章中你可以看到以下內容:
- 圖片環繞的實現方式
- 點擊事件獲取的優化算法
看了本篇博客,老司機能夠幫你實現如下效果
這篇博客是以前兩篇博客作為知識鋪墊的,如果沒有看過前兩篇博客的童靴建議你去補票。當然本身你就了解CoreText相關知識的話也可以直接看本篇文章。
全部代碼
優化算法以后,代碼有些許改變,不過主體思路是一致的。下面是全部代碼。
@interface CoreTextV ()
{
CTFrameRef _frame;
NSInteger _length;
CGRect _imgFrm;
NSMutableArray * arrText;
}
@end
@implementation CoreTextV
-(void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
arrText = [NSMutableArray array];
NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"];
[attributeStr addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0, attributeStr.length)];
CTRunDelegateCallbacks callBacks;
memset(&callBacks, 0, sizeof(CTRunDelegateCallbacks));
callBacks.version = kCTRunDelegateVersion1; callBacks.getAscent = ascentCallBacks; callBacks.getDescent = descentCallBacks; callBacks.getWidth = widthCallBacks;
NSDictionary * dicPic = @{@"height":@90,@"width":@160};
CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);
unichar placeHolder = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate); CFRelease(delegate);
[attributeStr insertAttributedString:placeHolderAttrStr atIndex:300];
NSDictionary * activeAttr = @{NSForegroundColorAttributeName:[UIColor redColor],@"click":NSStringFromSelector(@selector(click))};
[attributeStr addAttributes:activeAttr range:NSMakeRange(100, 30)];
[attributeStr addAttributes:activeAttr range:NSMakeRange(400, 100)];
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
UIBezierPath * path = [UIBezierPath bezierPathWithRect:self.bounds];
UIBezierPath * cirP = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 100, 200)];
[path appendPath:cirP];
_length = attributeStr.length;
_frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, _length), path.CGPath, NULL);
CTFrameDraw(_frame, context);
UIImage * image = [UIImage imageNamed:@"1.jpg"];
[self handleActiveRectWithFrame:_frame];
CGContextDrawImage(context,_imgFrm, image.CGImage);
CGContextDrawImage(context, cirP.bounds, [[UIImage imageNamed:@"1.jpg"] dw_ClipImageWithPath:cirP mode:(DWContentModeScaleAspectFill)].CGImage);
CFRelease(_frame);
CFRelease(frameSetter);
}
static CGFloat ascentCallBacks(void * ref)
{
return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue];
}
static CGFloat descentCallBacks(void * ref)
{
return 0;
}
static CGFloat widthCallBacks(void * ref)
{
return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue];
}
-(void)handleActiveRectWithFrame:(CTFrameRef)frame
{
NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);
NSInteger count = [arrLines count];
CGPoint points[count];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
for (int i = 0; i < count; i ++) {
CTLineRef line = (__bridge CTLineRef)arrLines[i];
NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);
for (int j = 0; j < arrGlyphRun.count; j ++) {
CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];
NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
CGPoint point = points[i];
if (delegate == nil) {
NSString * string = attributes[@"click"];
if (string) {
[arrText addObject:[NSValue valueWithCGRect:[self getLocWithFrame:frame CTLine:line CTRun:run origin:point]]];
}
continue;
}
NSDictionary * metaDic = CTRunDelegateGetRefCon(delegate);
if (![metaDic isKindOfClass:[NSDictionary class]]) {
continue;
}
_imgFrm = [self getLocWithFrame:frame CTLine:line CTRun:run origin:point];
}
}
}
-(CGRect)getLocWithFrame:(CTFrameRef)frame CTLine:(CTLineRef)line CTRun:(CTRunRef)run origin:(CGPoint)origin
{
CGFloat ascent;
CGFloat descent;
CGRect boundsRun;
boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
boundsRun.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
boundsRun.origin.x = origin.x + xOffset;
boundsRun.origin.y = origin.y - descent;
CGPathRef path = CTFrameGetPath(frame);
CGRect colRect = CGPathGetBoundingBox(path);
CGRect deleteBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);
return deleteBounds;
}
-(CGRect)convertRectFromLoc:(CGRect)rect
{
return CGRectMake(rect.origin.x, self.bounds.size.height - rect.origin.y - rect.size.height, rect.size.width, rect.size.height);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UITouch * touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
CGRect imageFrmToScreen = [self convertRectFromLoc:_imgFrm];
if (CGRectContainsPoint(imageFrmToScreen, location)) {
[[[UIAlertView alloc] initWithTitle:nil message:@"你點擊了圖片" delegate:nil cancelButtonTitle:@"好的" otherButtonTitles:nil] show];
return;
}
[arrText enumerateObjectsUsingBlock:^(NSValue * rectV, NSUInteger idx, BOOL * _Nonnull stop) {
CGRect textFrmToScreen = [self convertRectFromLoc:[rectV CGRectValue]];
if (CGRectContainsPoint(textFrmToScreen, location)) {
[self click];
*stop = YES;
}
}];
}
-(void)click
{
[[[UIAlertView alloc] initWithTitle:nil message:@"你點擊了文字" delegate:nil cancelButtonTitle:@"好的" otherButtonTitles:nil] show];
}
@end
只關心結果或者著急寫項目的童靴看到這里就足夠了,因為所有代碼都在,想找demo的話就去文章末尾找吧。因為接下來老司機要開始扯淡了
。。。跟你們講講一切的實現思路
。
圖片環繞的實現方式
由于我只是給個demo,所以一切代碼均從簡寫。實際過程中,代碼應進行封裝分塊。
我們將視線集中到drawRect
方法中吧。
之前的文章老司機講過,我們在drawRect中繪制文本的時候主要是根據Path去繪制
的。
UIBezierPath * path = [UIBezierPath bezierPathWithRect:self.bounds];
UIBezierPath * cirP = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 100, 200)];
[path appendPath:cirP];
_length = attributeStr.length;
_frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, _length), path.CGPath, NULL);
CTFrameDraw(_frame, context);
我們可以看到,我們是以path和frameSetter去生成我們繪制文本的frame的。所以說,只要在這個地方我們傳入的path中將特殊區域排除
我們獲得的frame就不包含該區域
,從而繪制的文本也不會在該區域中繪制
。
所以說上述的代碼你看到的應該是這樣子的文字區域
這里你可能會有個疑問,問什么我cirP的rect是CGRectMake(100, 100, 100, 200),這個排除的區域卻在那里。這里你還記得老司機在第一篇文章里就說過屏幕坐標系統跟系統坐標系統的區別
呢,原因就在這。
也就是說,到了這里,我們只要繪制出這個橢圓形的圖片就可以了。這你可能需要借助老司機之前寫好的工具類,在這個倉庫里的DWImageUtils就是了。如果好用記得給我個star
吧。
有了這個工具類,你就可以這樣生成橢圓圖片
了
[image dw_ClipImageWithPath:cirP mode:(DWContentModeScaleAspectFill)]
有了圖片了,情況基本就變成了我們熟悉的狀況了,繪制圖片
CGContextDrawImage(context, cirP.bounds, [[UIImage imageNamed:@"1.jpg"] dw_ClipImageWithPath:cirP mode:(DWContentModeScaleAspectFill)].CGImage);
至此,我們就繪制出環繞的文本了。也算真正的實現所謂的圖文混排了。
點擊事件獲取的優化算法
首先老司機來講一下目前老司機了解到的幾種獲取點擊事件的方式。
主流方式:CTLineGetStringIndexForPosition
主流方式就是當前大部分基于CoreText封裝的富文本展示類(包括TTTAttributedLabel
、NIAttributedLabel
和FTCoreTextView
)中使用的方法 CTLineGetStringIndexForPosition
。這個方法是獲取當前點在所在文字處于當前繪制文本的索引值
。事實上如果沒有一些其他因素的話,能使用這個方法是最簡便快捷的。然而老司機為什么沒有使用這個方法去獲取點擊事件呢?請看下面的動圖??
這里老司機是以TTTAttributedLabel為樣本做了一個點擊事件的Demo。
先明確一點,有下劃線的區域應該為實際點擊響應區域。可以看到,實際的響應區域相比預期響應區域x坐標會整體向左偏移一定區域
。
實際使用中CTLineGetStringIndexForPosition這個方法獲取一個字的index范圍是這個字前面大概半個字開始到這個字中間的位置
。從這個字的中間到這個字的后半個字就會獲得下一個字的index。舉個例子:(勾選的勾字index應該為0,當點擊勾字左半部分的時候返回0,右半部分返回1)。
老司機查閱了很多資料,有的資料說這個方法在當有段前縮進或者首行縮進的時候,并不準確,不會跟著縮進而進行偏移。然而老司機在將段前縮進設為0仍然有這個問題。老司機也不知道具體問題在哪,然而老司機有強迫癥的不能允許這半個字的誤差
,所以老司機當時決定不用這個方法,自己另辟蹊徑。就有了老司機當時的遍歷每個CTRun的算法。
多說一句,CTLineGetStringIndexForPosition這個方法還有另一個作用還是很好用的。這個方法最好的用處就是判斷一行CTLine最多容納多少的字符,只需把這個point的x位置調很大(超過CTFrame path的寬度)就可以了
。
另辟蹊徑:遍歷CTRun比較法。
老司機當時覺得半個字的誤差實在是難以容忍,所以老司機舍近求遠想出了這套遍歷CTRun
的算法。因為執行效率上一個屏幕內能展示的文字所包含的CTRun的數量在遍歷過程中并不會造成過多的性能浪費,所以老司機當時也沒有在意。
直到后來老司機的項目中由于要盡量少的使用三方SDK,所以自告奮勇的把自己寫的coreText的可點擊label引入到工程里面。然而項目經理看了源碼后表示雖然他沒用過CoreText,但是遍歷真的很蛋疼,決定引入一個TTT。老司機的心情瞬間跌入谷底。老司機當即決定,我要優化算法
。
就在身邊卻一直被我忽略的一個點
之所以說優化算法,沒有說不用遍歷是因為CoreText就那么些東西,獲取圖片的_frame還是需要遍歷整個CTFrame中的所有CTRun的。所以老司機花了整整一個禮拜也沒找到替代遍歷的方法
。
終于有一天,感受到了月亮的召喚,老司機變!身!了!
順便想到了一個思路,避免不了遍歷我就只遍歷一次就好了
。一次遍歷中拿到所有活動圖片和活動文字的frame,然后事情就簡單多了,按照點擊圖片的處理方式處理文字就好了。
所以老司機就想了一個辦法期望在遍歷的時候可以拿到活動文字的特征點,從而獲取活動范圍。老司機順理成章的就給想要添加點擊事件的活動文本加了click這么一個屬性
。(demo中老司機就隨便寫了,實際要慎重考慮叫什么名字不會被覺得太Low??)
其實實現上的思路很簡單,只是之前沒想到,感謝月亮的召喚吧
還是。
NSDictionary * activeAttr = @{NSForegroundColorAttributeName:[UIColor redColor],@"click":NSStringFromSelector(@selector(click))};
[attributeStr addAttributes:activeAttr range:NSMakeRange(100, 30)];
[attributeStr addAttributes:activeAttr range:NSMakeRange(400, 100)];
這里老司機很隨意的添加了一個click屬性。
[arrText addObject:[NSValue valueWithCGRect:[self getLocWithFrame:frame CTLine:line CTRun:run origin:point]]];
然后這里老司機就把活動文本的frame計算出來了。
思路就是這么簡單。本著一年內保證售后服務的原則,老司機會給在前兩篇博客中要demo的童鞋再發一份最新的demo通知新算法。
另外老司機這里要提醒你一點的就是,文字frame不同于獲取圖片的frame
。由于圖片
是在一個空白占位符上繪制文字,所以一定是以一個CTRun進行繪制的
。但是第一篇文章中老司機說過,每個CTRun是所有具有相同屬性的連續同行文字的集合
。針對CTRun的特性,我們不難想到,文字由于可能出現兩行,也有可能會活動文本的字體字號等其他屬性不盡相同導致一段文字由兩個CTRun進行繪制
,所以不能單純的保存一個frame,而是要以一個數組容納他
。再通過一些邏輯將不同的活動文本區別開來。由于是demo所以一切從簡老司機沒有說怎么區分活動文本,給個思路,就是你給click屬性綁定的value就可以用作區分
(這句好拗口)。至此,全部的文字點擊及圖片點擊事件的大體思路講述完畢。
為了吸取教訓,這次老司機會留下demo。恩,也要去前兩篇通知他們第三篇有demo。
去這里下載demo
然后,
廣告時間了:
痛定思痛的老司機寫了一個真正支持圖文混排點擊事件的Label控件。你可以實現如下的效果。
插入圖片、繪制圖片、添加事件統統一句話實現~
盡可能保持系統Label屬性讓你可以無縫過渡使用~
恩,說了這么多,老司機放一下地址:DWCoreTextLabel,寶寶們給個star吧愛你喲
文章到最后都是一樣的,喜歡點贊吧關注吧乖話說老司機超愛兔子的