本例子是實現類似於微博的富文本效果,可以實現圖文混排和處理點擊事件觸發。使用CoreText進行圖文混排的核心思想是把需要擺放圖片的位置用空字符替換原來的字符,並且實現CTRunDelegate,用於動態設置空字符的高度和寬度(代表圖片的大小),並且對這些空字符設置一個屬性名來區別於其他CTRun,之後進行圖片渲染的時候就能通過該屬性來區分哪些空字符是代表圖片的占位符,哪些是普通的空字符。使用CoreText處理點擊事件的關鍵是判斷點擊的位置是本文內容中的第幾個字符,然後通過判斷該字符是否在需要處理點擊事件的字符串范圍內。
//創建NSMutableAttributedString,解析所有觸發點擊事件和替換所有需要顯示圖片的位置
-(void)buildAttribute{
content = [[NSMutableAttributedString alloc]initWithString:originalStr];
//創建圖片的名字
NSString *imgName = @"smile.png";
//設置CTRun的回調,用於針對需要被替換成圖片的位置的字符,可以動態設置圖片預留位置的寬高
CTRunDelegateCallbacks imageCallbacks;
imageCallbacks.version = kCTRunDelegateVersion1;
imageCallbacks.dealloc = RunDelegateDeallocCallback;
imageCallbacks.getAscent = RunDelegateGetAscentCallback;
imageCallbacks.getDescent = RunDelegateGetDescentCallback;
imageCallbacks.getWidth = RunDelegateGetWidthCallback;
//創建CTRun回調
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, imgName);
//這裡為了簡化解析文字,所以直接認為最後一個字符是需要顯示圖片的位置,對需要顯示圖片的位置,都用空字符來替換原來的字符,空格用於給圖片留位置
NSMutableAttributedString *imageAttributedString = [[NSMutableAttributedString alloc] initWithString:@" "];
//設置圖片預留字符使用CTRun回調
[imageAttributedString addAttribute:(NSString *)kCTRunDelegateAttributeName value:(id)runDelegate range:NSMakeRange(0, 1)];
CFRelease(runDelegate);
//設置圖片預留字符使用一個imageName的屬性,區別於其他字符
[imageAttributedString addAttribute:@"imageName" value:imgName range:NSMakeRange(0, 1)];
[content appendAttributedString:imageAttributedString];
//換行模式,設置段落屬性
CTParagraphStyleSetting lineBreakMode;
CTLineBreakMode lineBreak = kCTLineBreakByCharWrapping;
lineBreakMode.spec = kCTParagraphStyleSpecifierLineBreakMode;
lineBreakMode.value = &lineBreak;
lineBreakMode.valueSize = sizeof(CTLineBreakMode);
CTParagraphStyleSetting settings[] = {
lineBreakMode
};
CTParagraphStyleRef style = CTParagraphStyleCreate(settings, 1);
NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithObject:(id)style forKey:(id)kCTParagraphStyleAttributeName ];
[content addAttributes:attributes range:NSMakeRange(0, [content length])];
//這裡對需要進行點擊事件的字符heightlight效果,這裡簡化解析過程,直接hard code需要heightlight的范圍
[content addAttribute:(id)kCTForegroundColorAttributeName value:(id)[[UIColor blueColor]CGColor] range:NSMakeRange(0, 10)];
}
//CTRun的回調,銷毀內存的回調
void RunDelegateDeallocCallback( void* refCon ){
}
//CTRun的回調,獲取高度
CGFloat RunDelegateGetAscentCallback( void *refCon ){
NSString *imageName = (NSString *)refCon;
return 30;//[UIImage imageNamed:imageName].size.height;
}
CGFloat RunDelegateGetDescentCallback(void *refCon){
return 0;
}
//CTRun的回調,獲取寬度
CGFloat RunDelegateGetWidthCallback(void *refCon){
NSString *imageName = (NSString *)refCon;
return 30;//[UIImage imageNamed:imageName].size.width;
}
- (void)drawRect:(CGRect)rect
{
//設置NSMutableAttributedString的所有屬性
[self buildAttribute];
NSLog(@"rect:%@",NSStringFromCGRect(rect));
CGContextRef context = UIGraphicsGetCurrentContext();
//設置context的ctm,用於適應core text的坐標體系
CGContextSaveGState(context);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, rect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//設置CTFramesetter
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, rect.size.width, rect.size.height));
//創建CTFrame
_frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, content.length), path, NULL);
//把文字內容繪制出來
CTFrameDraw(_frame, context);
//獲取畫出來的內容的行數
CFArrayRef lines = CTFrameGetLines(_frame);
//獲取每行的原點坐標
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), lineOrigins);
NSLog(@"line count = %ld",CFArrayGetCount(lines));
for (int i = 0; i < CFArrayGetCount(lines); i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGFloat lineAscent;
CGFloat lineDescent;
CGFloat lineLeading;
//獲取每行的寬度和高度
CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);
NSLog(@"ascent = %f,descent = %f,leading = %f",lineAscent,lineDescent,lineLeading);
//獲取每個CTRun
CFArrayRef runs = CTLineGetGlyphRuns(line);
NSLog(@"run count = %ld",CFArrayGetCount(runs));
for (int j = 0; j < CFArrayGetCount(runs); j++) {
CGFloat runAscent;
CGFloat runDescent;
CGPoint lineOrigin = lineOrigins[i];
//獲取每個CTRun
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run);
CGRect runRect;
//調整CTRun的rect
runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0,0), &runAscent, &runDescent, NULL);
NSLog(@"width = %f",runRect.size.width);
runRect=CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y - runDescent, runRect.size.width, runAscent + runDescent);
NSString *imageName = [attributes objectForKey:@"imageName"];
//圖片渲染邏輯,把需要被圖片替換的字符位置畫上圖片
if (imageName) {
UIImage *image = [UIImage imageNamed:imageName];
if (image) {
CGRect imageDrawRect;
imageDrawRect.size = CGSizeMake(30, 30);
imageDrawRect.origin.x = runRect.origin.x + lineOrigin.x;
imageDrawRect.origin.y = lineOrigin.y;
CGContextDrawImage(context, imageDrawRect, image.CGImage);
}
}
}
}
CGContextRestoreGState(context);
}
//接受觸摸事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
//獲取UITouch對象
UITouch *touch = [touches anyObject];
//獲取觸摸點擊當前view的坐標位置
CGPoint location = [touch locationInView:self];
NSLog(@"touch:%@",NSStringFromCGPoint(location));
//獲取每一行
CFArrayRef lines = CTFrameGetLines(_frame);
CGPoint origins[CFArrayGetCount(lines)];
//獲取每行的原點坐標
CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), origins);
CTLineRef line = NULL;
CGPoint lineOrigin = CGPointZero;
for (int i= 0; i < CFArrayGetCount(lines); i++)
{
CGPoint origin = origins[i];
CGPathRef path = CTFrameGetPath(_frame);
//獲取整個CTFrame的大小
CGRect rect = CGPathGetBoundingBox(path);
NSLog(@"origin:%@",NSStringFromCGPoint(origin));
NSLog(@"rect:%@",NSStringFromCGRect(rect));
//坐標轉換,把每行的原點坐標轉換為uiview的坐標體系
CGFloat y = rect.origin.y + rect.size.height - origin.y;
NSLog(@"y:%f",y);
//判斷點擊的位置處於那一行范圍內
if ((location.y <= y) && (location.x >= origin.x))
{
line = CFArrayGetValueAtIndex(lines, i);
lineOrigin = origin;
break;
}
}
location.x -= lineOrigin.x;
//獲取點擊位置所處的字符位置,就是相當於點擊了第幾個字符
CFIndex index = CTLineGetStringIndexForPosition(line, location);
NSLog(@"index:%ld",index);
//判斷點擊的字符是否在需要處理點擊事件的字符串范圍內,這裡是hard code了需要觸發事件的字符串范圍
if (index>=1&&index<=10) {
UIAlertView* alert = [[UIAlertView alloc]initWithTitle:@"click event" message:[originalStr substringWithRange:NSMakeRange(0, 10)] delegate:self cancelButtonTitle:@"cancel" otherButtonTitles:@"ok", nil];
[alert show];
}
}
效果圖如下: