昨天研究coretext,在网上找了好多资料,然后总记录一下
CoreText 简介
CoreText 是用于处理文字和字体的底层技术。它直接和 Core Graphics(又被称为 Quartz)打交道。Quartz 是一个 2D 图形渲染引擎,能够处理 OSX 和 iOS 中的图形显示。
Quartz 能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此,CoreText 为了排版,需要将显示的文本内容、位置、字体、字形直接传递给 Quartz。相比其它 UI 组件,由于 CoreText 直接和 Quartz 来交互,所以它具有高速的排版效果。
下图是 CoreText 的架构图,可以看到,CoreText 处于非常底层的位置,上层的 UI 控件(包括 UILabel,UITextField 以及 UITextView)和 UIWebView 都是基于 CoreText 来实现的。
UIWebView和CoreText区别分析
CoreText 的好处
- CoreText 占用的内存更少,渲染速度快,UIWebView 占用的内存更多,渲染速度慢。
- CoreText 在渲染界面前就可以精确地获得显示内容的高度(只要有了 CTFrame 即可),而 UIWebView 只有渲染出内容后,才能获得内容的高度(而且还需要用 javascript 代码来获取)
- CoreText 的 CTFrame 可以在后台线程渲染,UIWebView 的内容只能在主线程(UI 线程)渲染。
- 基于 CoreText 可以做更好的原生交互效果,交互效果可以更细腻。而 UIWebView 的交互效果都是用 javascript 来实现的,在交互效果上会有一些卡顿存在。例如,在 UIWebView 下,一个简单的按钮按下效果,都无法做到原生按钮的即时和细腻的按下效果。
CoreText 的劣势:
- CoreText 渲染出来的内容不能像 UIWebView 那样方便地支持内容的复制。
- 基于 CoreText 来排版需要自己处理很多复杂逻辑,例如需要自己处理图片与文字混排相关的逻辑,也需要自己实现链接点击操作的支持。
首先要引用CoreText类库
#import "CoreTextView.h"
#import "CoreText/CoreText.h"
#import "CoreText/CTRunDelegate.h"
@interface CoreTextView()
@property (assign ,nonatomic) CTFrameRef frame;
@end
@implementation CoreTextView
-(void)drawRect:(CGRect)rect
{
[super drawRect:rect];
// 步骤1:得到当前用于绘制画布的上下文,用于后续将内容绘制在画布上
// 因为Core Text要配合Core Graphic 配合使用的,如Core Graphic一样,绘图的时候需要获得当前的上下文进行绘制
CGContextRef context = UIGraphicsGetCurrentContext();
// 步骤2:翻转当前的坐标系(因为对于底层绘制引擎来说,屏幕左下角为(0,0))
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// 步骤3:创建绘制区域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
// 步骤4:创建需要绘制的文字与计算需要绘制的区域
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:@"通过CoreText进行学习绘制图文混排功能,通过CoreText进行学习绘制图文混排功能通过CoreText进行学习绘制图文混排功能通过CoreText进行学习绘制图文混排功能通过CoreText进行学习绘制图文混排功能"];
// 步骤5:设置部分文字颜色
[attrString addAttribute:(id)kCTForegroundColorAttributeName value:[UIColor greenColor] range:NSMakeRange(10, 10)];
// 设置部分文字
CGFloat fontSize = 20;
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
[attrString addAttribute:(id)kCTFontAttributeName value:(__bridge id)fontRef range:NSMakeRange(15, 10)];
CFRelease(fontRef);
// 设置行间距
CGFloat lineSpacing = 10;
const CFIndex kNumberOfSettings = 5;
CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
{kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &lineSpacing},
{kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &lineSpacing}
};
CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
[attrString addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0, attrString.length)];
CFRelease(theParagraphRef);
// 步骤6:图文混排部分
// CTRunDelegateCallbacks:一个用于保存指针的结构体,由CTRun delegate进行回调
CTRunDelegateCallbacks callbacks;
memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;
// 图片信息字典
NSDictionary *imgInfoDic = @{@"width":@(self.bounds.size.width),@"height":@170};
// 设置CTRun的代理
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)imgInfoDic);
// 使用0xFFFC作为空白的占位符
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);
// 将创建的空白AttributedString插入进当前的attrString中,位置可以随便指定,不能越界
[attrString insertAttributedString:space atIndex:0];
// 步骤7:根据AttributedString生成CTFramesetterRef
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
_frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, [attrString length]), path, NULL);
// 步骤8:进行绘制
CTFrameDraw(_frame, context);
// 步骤9:绘制图片
UIImage *image = [UIImage imageNamed:@"image1.jpg"];
CGContextDrawImage(context, [self calculateImagePositionInCTFrame:_frame], image.CGImage);
// 步骤10.内存管理
// CFRelease(_frame);//点击区域用到frame
CFRelease(path);
CFRelease(frameSetter);
}
#pragma mark - CTRun delegate 回调方法
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];
}
/**
* 根据CTFrameRef获得绘制图片的区域
*
* @param ctFrame CTFrameRef对象
*
* @return绘制图片的区域
*/
- (CGRect)calculateImagePositionInCTFrame:(CTFrameRef)ctFrame {
// 获得CTLine数组
NSArray *lines = (NSArray *)CTFrameGetLines(ctFrame);
NSInteger lineCount = [lines count];
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), lineOrigins);
// 遍历每个CTLine
for (NSInteger i = 0 ; i < lineCount; i++) {
CTLineRef line = (__bridge CTLineRef)lines[i];
NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
// 遍历每个CTLine中的CTRun
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;
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//获取UITouch 对象
UITouch* touch = [touches anyObject];
//获取触摸点击当前view的坐标位置
CGPoint location = [touch locationInView:self];
//获取每一行
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);
CGFloat y = rect.origin.y +rect.size.height-origin.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);
if (index>=1&&index<=10) {
NSLog(@"自己设计的点击区域");
}
}
参考资料:http://www.saitjr.com/ios/use-coretext-make-typesetting-picture-and-text.html