這篇主要講利用coreText來實現(xiàn)圖文混排和點擊事件
圖文混排思路
文字的繪制只需要知道文字的大小就夠了,而圖片的繪制不一樣,需要知道圖片的坐標,高度和寬度。在CoreText中,我們可以把插入的圖片當做一個特殊的CTRun
,通過delegate
來設置圖片的寬度和高度,這樣就解決了圖片的高度和寬度問題,但是CoreText不會自動的對圖片進行繪制,因此需要我們自己找到圖片的顯示位置(原點坐標),然后自己進行繪制
具體實現(xiàn)
根據(jù)上一篇的 iOS CoreText(一)中的代碼,我們需要在需要顯示圖片的地方,插入一個空白字符,然后設置CTRun
的代理
- (void)drawTextAndImage:(CGContextRef)context size:(CGSize)size {
NSMutableAttributedString *astring = _textString;
//設置坐標系
//設置字形的變換矩陣為不做圖形變換
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
//平移方法,將畫布向上平移一個屏幕高度
CGContextTranslateCTM(context, 0, size.height);
//縮放方法,x軸縮放系數(shù)為1,則不變,y軸縮放系數(shù)為-1,則相當于以x軸為軸旋轉(zhuǎn)180度
CGContextScaleCTM(context, 1, -1);
//這次的重點
//設置CTRun代理
CTRunDelegateCallbacks callBacks;
memset(&callBacks, 0, sizeof(CTRunDelegateCallbacks));
callBacks.version = kCTRunDelegateVersion1;
callBacks.getAscent = ascentCallbacks;
callBacks.getDescent = descentCallbacks;
callBacks.getWidth = widthCallbacks;
CTRunDelegateRef delegate = CTRunDelegateCreate(&callBacks, (void *)astring);
//創(chuàng)建空白字符
unichar placeHolder = 0xFFFC;
NSString *placeHolderString = [NSString stringWithCharacters:&placeHolder length:1];
NSMutableAttributedString *placeHolderAttributedString = [[NSMutableAttributedString alloc]initWithString:placeHolderString];
NSDictionary *attributedDic = [NSDictionary dictionaryWithObjectsAndKeys:(__bridge id)delegate, kCTRunDelegateAttributeName,nil];
[placeHolderAttributedString setAttributes:attributedDic range:NSMakeRange(0, 1)];
CFRelease(delegate);
//將圖片插入
[astring insertAttributedString:placeHolderAttributedString atIndex:astring.length/2];
//創(chuàng)建path
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
//繪文字
CTFramesetterRef frameRef = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)astring);
CTFrameRef fref = CTFramesetterCreateFrame(frameRef, CFRangeMake(0, astring.length), path, NULL);
CTFrameDraw(fref, context);
//繪圖
UIImage *image = [UIImage imageNamed:@"tj_Image"];
CGRect imageRect = [self calculateImageRect:fref];
CGContextDrawImage(context, imageRect, image.CGImage);
CFRelease(path);
CFRelease(fref);
CFRelease(frameRef);
}
#pragma mark ---CTRUN代理---
CGFloat ascentCallbacks (void *ref) {
return 11;
}
CGFloat descentCallbacks (void *ref) {
return 7;
}
CGFloat widthCallbacks (void *ref) {
return 36;
}
和上次一樣的我們就不講了,從設置代理開始說起
先要設置代理的回調(diào)CTRunDelegateCallbacks callBacks;
,包括了四個需要設置的屬性,版本號、上邊距、下邊距和寬度,后面對應的是C的函數(shù)名,負責確定圖片的寬度和高度。
設置完成后,穿件一個空白字符unichar placeHolder = 0xFFFC
將空白字符轉(zhuǎn)換成NSString
,再轉(zhuǎn)換成NSMutableAttributedString
創(chuàng)建一個字典,key為kCTRunDelegateAttributeName
,value為我們創(chuàng)建的delegate
(__bridge為OC對象和CF對象之間的橋接),本質(zhì)上addAttributes:
就是add一個字典,這點要理解
然后將空白字符插入要顯示的位置
//繪圖
//我們要顯示的圖片
UIImage *image = [UIImage imageNamed:@"tj_Image"];
//自己定義的方法,為了得到圖片的坐標和大小
CGRect imageRect = [self calculateImageRect:fref];
//將圖片繪制到指定的地方
CGContextDrawImage(context, imageRect, image.CGImage);
我們現(xiàn)在主要來講講calculateImageRect
這個方法
先來說說這個方法的思路:
我們先獲取到所有的CTLine
,然后遍歷每個CTLine
中的CTRun
的,取出CTRun
對應的Attributes
字典,判斷字典中是否有key為kCTRunDelegateAttributeName
的value,如果有,就是我們插入的圖片的位置。
- (CGRect)calculateImageRect:(CTFrameRef)frame {
//先找CTLine的原點,再找CTRun的原點
NSArray *allLine = (NSArray *)CTFrameGetLines(frame);
NSInteger lineCount = [allLine count];
//獲取CTLine原點坐標
CGPoint points[lineCount];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
CGRect imageRect = CGRectMake(0, 0, 0, 0);
for (int i = 0; i < lineCount; i++) {
CTLineRef line = (__bridge CTLineRef)allLine[i];
//獲取所有的CTRun
CFArrayRef allRun = CTLineGetGlyphRuns(line);
CFIndex runCount = CFArrayGetCount(allRun);
//獲取line原點
CGPoint lineOrigin = points[i];
for (int j = 0; j < runCount; j++) {
CTRunRef run = CFArrayGetValueAtIndex(allRun, j);
NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);
CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
if (delegate == nil) {
//暫時不用關注這部分代碼,主要用于點擊事件
NSString *textClickString = [attributes valueForKey:@"textClick"];
if (textClickString != nil) {
[textFrameArray addObject:[NSValue valueWithCGRect:[self getLocWith:frame line:line run:run origin:lineOrigin]]];
}
continue;
}
//獲取圖片的Rect
imageRect = [self getLocWith:frame line:line run:run origin:lineOrigin];
}
}
return imageRect;
}
對照上面的思路來看,代碼不復雜
- (CGRect)getLocWith:(CTFrameRef)frame line:(CTLineRef)line run:(CTRunRef)run origin:(CGPoint)point {
CGRect boundRect;
CGFloat ascent = 0.0f;
CGFloat descent = 0.0f;
CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
boundRect.size.width = width;
boundRect.size.height = ascent + descent;
//獲取x偏移量
CGFloat xoffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
boundRect.origin.x = point.x + xoffset;
boundRect.origin.y = point.y - descent;
//獲取BoundingBox
CGPathRef path = CTFrameGetPath(frame);
CGRect colRect = CGPathGetBoundingBox(path);
return CGRectOffset(boundRect, colRect.origin.x, colRect.origin.y);
}
CGFloat ascent = 0.0f;
CGFloat descent = 0.0f;
CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
boundRect.size.width = width;
boundRect.size.height = ascent + descent;
這里獲取的width
,ascent
和descent
,其實就是我們在CTRunDelegateCallbacks callBacks
中設置的那幾個函數(shù)
CGFloat xoffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
這句代碼就是獲取該CTRun
對于CTLine
的x軸偏移,知道偏移量和CTLine
的原點,我們就可以計算出圖片的原點。
boundRect.origin.y = point.y - descent;
主要是為了讓圖片能居中顯示(可以自己調(diào)節(jié)試試)
這樣圖片的Rect
就獲取到了
現(xiàn)在來說說點擊事件
當我們?yōu)槟扯挝淖只蛘吣硰垐D片設置點擊事件,主要利用了本質(zhì)上addAttributes:
就是add一個字典這一特性,這樣我們就可以直接在字典中定義一個特殊的key,用來判斷該CTRun
是否是具有點擊事件的CTRun
。
LJTextView *textView = [[LJTextView alloc] initWithFrame:CGRectMake(0, 64, width, height - 64)];
textView.backgroundColor = [UIColor whiteColor];
textView.textString = [[NSMutableAttributedString alloc]initWithString:@"123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123"];
NSDictionary *dic = @{@"textClick":@"click",NSBackgroundColorAttributeName:[UIColor redColor]};
[textView.textString addAttributes:dic range:NSMakeRange(24, 4)];
上面的代碼我們就定義了已一個textClick
的特殊key
NSString *textClickString = [attributes valueForKey:@"textClick"];
if (textClickString != nil) {
[textFrameArray addObject:[NSValue valueWithCGRect:[self getLocWith:frame line:line run:run origin:lineOrigin]]];
}
獲取圖片Rect的時候,我們同時獲取了擁有點擊事件的CTRun
的Rect并把他記錄在textFrameArray
數(shù)組中,方面后面進行判斷
- (CGRect)convertRectToWindow:(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 point = [touch locationInView:self];
[textFrameArray enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL * _Nonnull stop) {
CGRect rect = [value CGRectValue];
CGRect convertRect = [self convertRectToWindow:rect];
if (CGRectContainsPoint(convertRect, point)) {
NSString *message = [NSString stringWithFormat:@"點擊了%lu",(unsigned long)idx];
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"提示" message:message delegate:self cancelButtonTitle:@"確定" otherButtonTitles:nil];
[alert show];
}
}];
}
convertRectToWindow
主要用于轉(zhuǎn)換坐標系,上面代碼主要用于判斷點擊的位置在不在坐標范圍內(nèi),在則響應,不在則不處理
其實CoreText并不復制,只要了解,后面就好在工作中運用了
下一篇我們介紹YYLable的實現(xiàn)機制