介绍CoreText的一些概念
Core Text基础
CoreText 是用于处理文字和字体的底层技术。它直接和 Core Graphics(又被称为 Quartz)打交道。
UIWebView 也是处理复杂的文字排版的备选方案。对于排版,基于 CoreText 和基于 UIWebView 相比,前者有以下好处:
CoreText 占用的内存更少,渲染速度快,UIWebView 占用的内存更多,渲染速度慢。
CoreText 在渲染界面前就可以精确地获得显示内容的高度(只要有了 CTFrame 即可),而 UIWebView 只有渲染出内容后,才能获得内容的高度(而且还需要用 javascript 代码来获取)
CoreText 的 CTFrame 可以在后台线程渲染,UIWebView 的内容只能在主线程(UI 线程)渲染。
基于 CoreText 可以做更好的原生交互效果,交互效果可以更细腻。而 UIWebView 的交互效果都是用 javascript 来实现的,在交互效果上会有一些卡顿存在。例如,在 UIWebView 下,一个简单的按钮按下效果,都无法做到原生按钮的即时和细腻的按下效果。
当然,基于 CoreText 的排版方案也有一些劣势:
CoreText 渲染出来的内容不能像 UIWebView 那样方便地支持内容的复制。
基于 CoreText 来排版需要自己处理很多复杂逻辑,例如需要自己处理图片与文字混排相关的逻辑,也需要自己实现链接点击操作的支持。
以下是Core Text的构成:
CTFrame可以想象成画布, 画布的大小范围由CGPath决定
CTFrame由很多CTLine组成, CTLine表示为一行
CTLine由多个CTRun组成, CTRun相当于一行中的多个块, 但是CTRun不需要你自己创建, 由NSAttributedString的属性决定, 系统自动生成。每个CTRun对应不同属性
CTFramesetter是一个工厂, 创建CTFrame, 一个界面上可以有多个CTFrame
字体
字体(Font):是一系列字号、样式和磅值相同的字符(例如:10磅黑体Palatino)。现多被视为字样的同义词
字面(Face):是所有字号的磅值和格式的综合
字体集(Font family):是一组相关字体(例如:Franklin family包括Franklin Gothic、Fran-klinHeavy和Franklin Compressed)
磅值(Weight):用于描述字体粗度。典型的磅值,从最粗到最细,有极细、细、book、中等、半粗、粗、较粗、极粗
样式(Style):字形有三种形式:Roman type是直体;oblique type是斜体;utakuc type是斜体兼曲线(比Roman type更像书法体)。
x高度(X height):指小写字母的平均高度(以x为基准)。磅值相同的两字母,x高度越大的字母看起来比x高度小的字母要大
Cap高度(Cap height):与x高度相似。指大写字母的平均高度(以C为基准)
下行字母(Descender):例如在字母q中,基线以下的字母部分叫下伸部分
上行字母(Ascender):x高度以上的部分(比如字母b)叫做上伸部分
基线(Baseline):通常在x、v、b、m下的那条线 描边(Stroke):组成字符的线或曲线。可以加粗或改变字符形状
CoreText定义的字体 样式
可以在 CTStringAttributes.h 中找到
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 const CFStringRef kCTCharacterShapeAttributeName;const CFStringRef kCTFontAttributeName;const CFStringRef kCTKernAttributeName;const CFStringRef kCTLigatureAttributeName;const CFStringRef kCTForegroundColorAttributeName;const CFStringRef kCTForegroundColorFromContextAttributeName; const CFStringRef kCTParagraphStyleAttributeName;const CFStringRef kCTStrokeWidthAttributeName;const CFStringRef kCTStrokeColorAttributeName;const CFStringRef kCTSuperscriptAttributeName;const CFStringRef kCTUnderlineColorAttributeName;const CFStringRef kCTUnderlineStyleAttributeName;const CFStringRef kCTVerticalFormsAttributeName;const CFStringRef kCTGlyphInfoAttributeName;const CFStringRef kCTRunDelegateAttributeName
CoreText坐标系
从图中可看出CoreText坐标系是以左下角为坐标原点,而我们常使用的UIKit是以左上角为坐标原点,因此在CoreText中的布局完成后需要对其坐标系进行转换,否则直接绘制出现位置反转的镜像情况。
CoreText 图文混排 CoreText实际上并没有相应API直接将一个图片转换为CTRun并进行绘制,它所能做的只是为图片预留相应的空白区域,而真正的绘制则是交由CoreGraphics完成。(像OSX就方便很多,直接将图片打包进NSTextAttachment即可,根本无须操心绘制的事情,所以基于这个想法,M80AttributedLabel的接口和实现也是使用了attachment这么个概念,图片或者UIView都是被当作文字段中的attachment。)
在CoreText中提供了CTRunDelegate这么个Core Foundation类,顾名思义它可以对CTRun进行拓展:AttributedString某个段设置kCTRunDelegateAttributeName属性之后,CoreText使用它生成CTRun是通过当前Delegate的回调来获取自己的ascent,descent和width,而不是根据字体信息。这样就给我们留下了可操作的空间:用一个空白字符作为图片的占位符,设好Delegate,占好位置,然后用CoreGraphics进行图片的绘制。
基本使用步骤
新建一个UIView类,在其drawRect方法中
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 -(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 ); CGMutablePathRef path = CGPathCreateMutable (); CGPathAddRect (path , NULL , self .bounds); NSAttributedString * attString = [[NSAttributedString alloc]initWithString:@"Hello CoreText" ]; CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString ((CFAttributedStringRef )attString); CTFrameRef frame = CTFramesetterCreateFrame (framesetter, CFRangeMake (0 , [attString length]), path , NULL ); CTFrameDraw (frame, context); CFRelease (frame); CFRelease (path); CFRelease (framesetter); }
Hello CoreText
就会显示在该View上了,这是CoreText的基本使用步骤。
简单的富文本
在上面代码的基础上,为NSAttributeString添加相关的字体信息
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 NSMutableAttributedString * mAttStr = [[NSMutableAttributedString alloc]initWithString:@"Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;Hello CoreText ;" ];CTFontRef font = CTFontCreateWithName (CFSTR ("Georgia" ), 40 , NULL );[mAttStr addAttribute:(id )kCTFontAttributeName value:(__bridge id _Nonnull)(font) range:NSMakeRange (0 , 4 )]; CTFontRef font2 = CTFontCreateWithName ((CFStringRef )[UIFont italicSystemFontOfSize:20 ].fontName, 14 , NULL );[mAttStr addAttribute:(id )kCTFontAttributeName value:(__bridge id _Nonnull)(font2) range:NSMakeRange (6 , 4 )]; [mAttStr addAttribute:(id )kCTUnderlineStyleAttributeName value:(id )[NSNumber numberWithInt:kCTUnderlineStyleDouble] range:NSMakeRange (18 , 4 )]; [mAttStr addAttribute:(id )kCTUnderlineStyleAttributeName value:(id )[NSNumber numberWithInt:kCTUnderlineStyleSingle] range:NSMakeRange (25 , 4 )]; [mAttStr addAttribute:(id )kCTUnderlineColorAttributeName value:(id )[UIColor redColor].CGColor range:NSMakeRange (25 , 4 )]; long number = 10 ;CFNumberRef num = CFNumberCreate (kCFAllocatorDefault,kCFNumberSInt8Type,&number);[mAttStr addAttribute:(id )kCTKernAttributeName value:(__bridge id )num range:NSMakeRange (36 , 4 )]; [mAttStr addAttribute:(id )kCTForegroundColorAttributeName value:(id )[UIColor redColor].CGColor range:NSMakeRange (43 , 4 )]; long number2 = 2 ;CFNumberRef num2 = CFNumberCreate (kCFAllocatorDefault,kCFNumberSInt8Type,&number2);[mAttStr addAttribute:(id )kCTStrokeWidthAttributeName value:(__bridge id )num2 range:NSMakeRange (54 , 4 )];
效果图:
更多的样式
在介绍如何绘制更多样式之前,需要介绍 按Line绘制 CTLineDraw
和按Run绘制 CTRunDraw
CTLineDraw 按行绘制 将上面的
1 CTFrameDraw (frame, context);
替换成:
1 2 3 4 5 6 7 8 9 10 11 12 13 CFArrayRef lines = CTFrameGetLines (frame);CFIndex numOfLines = CFArrayGetCount (lines);CGPoint lineOrigins[CFArrayGetCount (lines)];CTFrameGetLineOrigins (frame, CFRangeMake (0 , 0 ), lineOrigins);for (int i = 0 ; i < numOfLines; i++) { CTLineRef line = CFArrayGetValueAtIndex (lines, i); CGContextSetTextPosition (context, lineOrigins[i].x, lineOrigins[i].y); CTLineDraw (line , context); }
CTRunDraw 按Run绘制 将上面的
1 CTFrameDraw (frame, context);
替换成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 CFArrayRef lines = CTFrameGetLines (frame);CFIndex numOfLines = CFArrayGetCount (lines); CGPoint lineOrigins[CFArrayGetCount (lines)];CTFrameGetLineOrigins (frame, CFRangeMake (0 , 0 ), lineOrigins);for (int i = 0 ; i < numOfLines; i++) { CTLineRef line = CFArrayGetValueAtIndex (lines, i); CGContextSetTextPosition (context, lineOrigins[i].x, lineOrigins[i].y); CFArrayRef runs = CTLineGetGlyphRuns (line); for (int j = 0 ; j < CFArrayGetCount (runs); j++) { CTRunRef run = CFArrayGetValueAtIndex (runs, j); CTRunDraw (run, context, CFRangeMake (0 , 0 )); } }
最终 CTRunDraw CTLineDraw 和 CTFrameDraw的绘制结果都是相同的。
实现删除线效果
既然我们可以按照一小块CTRun的方式绘制,就可以对一些CTRun做一些自定义绘制
比如在 < CoreText/CTStringArrtibutes.h >中并没有找到删除线的定义,我们可以尝试去实现它。
**一、**在NSAttributedString中添加删除线属性(NSStrikethroughStyleAttributeName
字段定义在NSAttributedString.h中,coreText没有对应的实现)
1 2 [mAttStr addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInt:NSUnderlineStyleSingle ] range:NSMakeRange (72 ,18 )];
**二、**在上面按run绘制的基础上判断一下有无删除线属性,有就绘制。
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 CFArrayRef lines = CTFrameGetLines (frame);CFIndex numOfLines = CFArrayGetCount (lines); CGPoint lineOrigins[CFArrayGetCount (lines)];CTFrameGetLineOrigins (frame, CFRangeMake (0 , 0 ), lineOrigins);for (int i = 0 ; i < numOfLines; i++) { CTLineRef line = CFArrayGetValueAtIndex (lines, i); CGContextSetTextPosition (context, lineOrigins[i].x, lineOrigins[i].y); CFArrayRef runs = CTLineGetGlyphRuns (line); for (int j = 0 ; j < CFArrayGetCount (runs); j++) { CTRunRef run = CFArrayGetValueAtIndex (runs, j); CTRunDraw (run, context, CFRangeMake (0 , 0 )); NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes (run); if (runAttributes[NSStrikethroughStyleAttributeName ] != nil ){ [self drawStrikethroughStyleInRun:run attributes:runAttributes context:context]; } } }
三、 实现方法 -drawStrikethroughStyleInRun: attributes : context :
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 56 -(void )drawStrikethroughStyleInRun:(CTRunRef )run attributes: (NSDictionary *)attributes context:(CGContextRef )context { CFNumberRef styleRef = (__bridge CFNumberRef )(attributes[NSStrikethroughStyleAttributeName ]); NSUnderlineStyle style = NSUnderlineStyleNone ; CFNumberGetValue (styleRef, kCFNumberSInt64Type, &style); if (style == NSUnderlineStyleNone ) return ; CGFloat lineWidth = 1 ; if ((style & NSUnderlineStyleThick ) == NSUnderlineStyleThick ){ lineWidth = 2 ; } CGContextSetLineWidth (context, lineWidth); CGPoint firstP = CGPointZero ; size_t length = CTRunGetGlyphCount (run); const CGPoint *firstGP = CTRunGetPositionsPtr (run); if (!firstGP) { NSMutableData *tempBuffer = [[NSMutableData alloc] initWithLength:sizeof (CGPoint ) * length]; CTRunGetPositions (run, CFRangeMake (0 , length), (CGPoint *)tempBuffer.mutableBytes); firstGP = tempBuffer.mutableBytes; } firstP = *firstGP ; CGContextBeginPath (context); CGColorRef lineColor = (__bridge CGColorRef )attributes[NSStrikethroughColorAttributeName ]; if (lineColor == nil ){ CGContextSetStrokeColorWithColor (context, UIColor .blueColor.CGColor); } else { CGContextSetStrokeColorWithColor (context, lineColor); } UIFont * font = attributes[NSFontAttributeName ]; if (font == nil ){ font = [UIFont systemFontOfSize:UIFont .systemFontSize]; } CGFloat lineHeight = font.xHeight / 2.0 + firstP.y; CGPoint pt = CGContextGetTextPosition (context); lineHeight += pt.y; float w = CTRunGetTypographicBounds (run, CFRangeMake (0 , 0 ), nil , nil , nil ); CGContextMoveToPoint (context, pt.x + firstP.x, lineHeight); CGContextAddLineToPoint (context, pt.x + firstP.x + w, lineHeight); CGContextStrokePath (context); }
实现效果:
实现自定义样式-矩形框标注
自定义一个TSRectColor 一个包住文字的矩形框,尝试去实现它。
一、 在 NSAttributedString 中添加自定义属性:
1 2 3 [mAttStr addAttribute:@"TSRectColor" value:[UIColor yellowColor] range:NSMakeRange (90 , 18 )];
二、 修改一下上面的实现删除线的代码,添加实现矩形框。与实现删除线不一样的是,实现矩形框需要在CTRunDraw
之前:
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 CFArrayRef lines = CTFrameGetLines (frame);CFIndex numOfLines = CFArrayGetCount (lines); CGPoint lineOrigins[CFArrayGetCount (lines)];CTFrameGetLineOrigins (frame, CFRangeMake (0 , 0 ), lineOrigins);for (int i = 0 ; i < numOfLines; i++) { CTLineRef line = CFArrayGetValueAtIndex (lines, i); CGContextSetTextPosition (context, lineOrigins[i].x, lineOrigins[i].y); CFArrayRef runs = CTLineGetGlyphRuns (line); for (int j = 0 ; j < CFArrayGetCount (runs); j++) { CTRunRef run = CFArrayGetValueAtIndex (runs, j); NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes (run); if (runAttributes[@"TSRectColor" ]){ [self drawRectColorInRun:run attributes:runAttributes context:context]; } CTRunDraw (run, context, CFRangeMake (0 , 0 )); if (runAttributes[NSStrikethroughStyleAttributeName ] != nil ){ [self drawStrikethroughStyleInRun:run attributes:runAttributes context:context]; } } }
三、 实现 -drawRectColorInRun: attributes: context:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 -(void )drawRectColorInRun:(CTRunRef )run attributes: (NSDictionary *)attributes context:(CGContextRef )context { UIColor * color = attributes[@"TSRectColor" ]; if (!color)return ; CGPoint firstPoint = [self firstPointInRun:run]; CGFloat ascent,descent; double typographicBounds = CTRunGetTypographicBounds (run , CFRangeMake (0 , 0 ), &ascent, &descent, NULL ); CGPoint textP = CGContextGetTextPosition (context); CGRect rect = CGRectMake (firstPoint.x + textP.x, textP.y + firstPoint.y - descent, typographicBounds, ascent + descent); CGContextSetStrokeColorWithColor (context , color.CGColor); CGContextAddRect (context, rect); CGContextStrokePath (context); }
实现效果:
添加超链接
如何识别文字中的超链接 不同场景有不同的处理方式,这里就不处理了。
添加超链接 简单来讲就是重写touchesBegan方法 获取点击的点,再到CTFrameRef
中比较
重写touchesBegan方法 根据touch事件获取点point
1 2 3 4 5 6 7 8 -(void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event{ UITouch * touch = [touches anyObject]; CGPoint point = [touch locationInView:self ]; bool ifClicked = [self ifClickLink:point]; if (ifClicked) { NSLog (@"点击了连接" ); } }
判断touch点
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 -(bool )ifClickLink:(CGPoint )point{ if (!CGRectContainsPoint (self .bounds, point)) { return false ; } CFArrayRef lines = CTFrameGetLines (self .textFrame); if (!lines) return false ; CFIndex count = CFArrayGetCount (lines); CGPoint origins[count]; CTFrameGetLineOrigins (self .textFrame, CFRangeMake (0 , 0 ), origins); CGAffineTransform transform = CGAffineTransformScale (CGAffineTransformMakeTranslation (0 , self .bounds.size.height), 1. f, -1. f); for (int i = 0 ; i < count; i++) { CGPoint linePoint = origins[i]; CTLineRef line = CFArrayGetValueAtIndex (lines , i); CGFloat ascent = 0.0 f; CGFloat descent = 0.0 f; CGFloat leading = 0.0 f; CGFloat width = (CGFloat )CTLineGetTypographicBounds (line, &ascent, &descent, &leading); CGFloat height = ascent + descent; CGRect flippedLineRect = CGRectMake (linePoint.x, linePoint.y - descent, width, height); CGRect lineRect = CGRectApplyAffineTransform (flippedLineRect, transform); if (CGRectContainsPoint (lineRect, point)) { CFIndex idx = CTLineGetStringIndexForPosition (line, point); NSLog (@"%ld" ,idx); return [self ifClickRangeContainIndex:idx]; } } return false ; }
这样就监测到了点击事件,并且知道点击了哪个字符。再做相关超链接的点击逻辑,就添加超链接功能成功了。
以上代码都在 https://github.com/sunyanyan/TSCode/blob/master/CoreText/CoreText/TestView.m
图文混排
CoreText是不直接支持绘制图片的,但是我们可以先在需要显示图片的地方用一个特殊的空白占位符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度,这样绘制文字的时候就会先把图片的位置留出来,再在drawRect方法里面用CGContextDrawImage绘制图片。
在NSMutableString中添加空白字符为图片占位
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 -(void )addImageAttStr{ CTRunDelegateCallbacks callbacks; memset(&callbacks, 0 , sizeof (CTRunDelegateCallbacks )); callbacks.version = kCTRunDelegateVersion1; callbacks.getAscent = ascentCallback; callbacks.getDescent = descentCallback; callbacks.getWidth = widthCallback; NSDictionary *imgInfoDic = @{@"width" :@100 ,@"height" :@30 }; CTRunDelegateRef delegate = CTRunDelegateCreate (&callbacks, (__bridge void *)imgInfoDic); unichar objectReplacementChar = 0xFFFC ; NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1 ]; NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content]; CFAttributedStringSetAttribute ((CFMutableAttributedStringRef )space, CFRangeMake (0 , 1 ), kCTRunDelegateAttributeName, delegate); CFRelease (delegate); [self .sampleAttStr insertAttributedString:space atIndex:50 ]; }
其中用到的CTRun delegate回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static CGFloat ascentCallback(void *ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"height" ] floatValue]; } static CGFloat descentCallback(void *ref) { return 0 ; } static CGFloat widthCallback(void *ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref objectForKey:@"width" ] floatValue]; }
然后在CTFrameDraw之后用CGContextDrawImage绘制图片
1 2 3 UIImage *image = [UIImage imageNamed:@"one.png" ];CGContextDrawImage (context, [self calculateImagePositionInCTFrame:frame], image.CGImage);
用到的calculateImagePositionInCTFrame方法:
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 - (CGRect )calculateImagePositionInCTFrame:(CTFrameRef )ctFrame { NSArray *lines = (NSArray *)CTFrameGetLines (ctFrame); NSInteger lineCount = [lines count]; CGPoint lineOrigins[lineCount]; CTFrameGetLineOrigins (ctFrame, CFRangeMake (0 , 0 ), lineOrigins); for (NSInteger i = 0 ; i < lineCount; i++) { CTLineRef line = (__bridge CTLineRef )lines[i]; NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns (line); for (id runObj in runObjArray) { CTRunRef run = (__bridge CTRunRef )runObj; NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes (run); CTRunDelegateRef delegate = (__bridge CTRunDelegateRef )[runAttributes valueForKey:(id )kCTRunDelegateAttributeName]; if (delegate == nil ) { continue ; } NSDictionary *metaDic = CTRunDelegateGetRefCon (delegate); if (![metaDic isKindOfClass:[NSDictionary class ]]) { continue ; } CGRect runBounds; CGFloat ascent; CGFloat descent; runBounds.size.width = CTRunGetTypographicBounds (run, CFRangeMake (0 , 0 ), &ascent, &descent, NULL ); runBounds.size.height = ascent + descent; CGFloat xOffset = CTLineGetOffsetForStringIndex (line, CTRunGetStringRange (run).location, NULL ); runBounds.origin.x = lineOrigins[i].x + xOffset; runBounds.origin.y = lineOrigins[i].y; runBounds.origin.y -= descent; CGPathRef pathRef = CTFrameGetPath (ctFrame); CGRect colRect = CGPathGetBoundingBox (pathRef); CGRect delegateBounds = CGRectOffset (runBounds, colRect.origin.x, colRect.origin.y); return delegateBounds; } } return CGRectZero ; }
如图: