本例子是實作類別似於微博的富文字效果,可以實現圖文混排和處理點擊事件觸發。使用CoreText進行圖文混排的核心思想是把需要擺放圖片的位置用Null 字元替換原來的字元,並且實現CTRunDelegate,用於動態設定Null 字元的高度和寬度(代表圖片的大小),並且對這些Null 字元設定一個屬性名稱來區別於其他CTRun,之後進行圖片渲染的時候就能通過該屬性來區分哪些Null 字元是代表圖片的預留位置,哪些是普通的Null 字元。使用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);
//這裡為了簡化解析文字,所以直接認為最後一個字元是需要顯示圖片的位置,對需要顯示圖片的位置,都用Null 字元來替換原來的字元,空格用於給圖片留位置
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];
}
}
如下: